Browse Source

clean up articles

imwald
Silberengel 5 months ago
parent
commit
23402b9309
  1. 37
      src/components/Embedded/EmbeddedNote.tsx
  2. 38
      src/components/Note/Article/index.tsx
  3. 39
      src/components/Note/AsciidocArticle/AsciidocArticle.tsx
  4. 23
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  5. 47
      src/pages/secondary/NotePage/NotFound.tsx

37
src/components/Embedded/EmbeddedNote.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { Skeleton } from '@/components/ui/skeleton'
import { BIG_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { useFetchEvent } from '@/hooks'
import { normalizeUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
@ -94,17 +95,20 @@ function EmbeddedNoteNotFound({ @@ -94,17 +95,20 @@ function EmbeddedNoteNotFound({
const [isSearchingExternal, setIsSearchingExternal] = useState(false)
const [triedExternal, setTriedExternal] = useState(false)
const [externalRelays, setExternalRelays] = useState<string[]>([])
const [hexEventId, setHexEventId] = useState<string | null>(null)
// Calculate which external relays would be tried
useEffect(() => {
const getExternalRelays = async () => {
let relays: string[] = []
let extractedHexEventId: string | null = null
if (!/^[0-9a-f]{64}$/.test(noteId)) {
try {
const { type, data } = nip19.decode(noteId)
if (type === 'nevent') {
extractedHexEventId = data.id
if (data.relays) relays.push(...data.relays)
if (data.author) {
const authorRelayList = await client.fetchRelayList(data.author)
@ -114,6 +118,8 @@ function EmbeddedNoteNotFound({ @@ -114,6 +118,8 @@ function EmbeddedNoteNotFound({
if (data.relays) relays.push(...data.relays)
const authorRelayList = await client.fetchRelayList(data.pubkey)
relays.push(...authorRelayList.write.slice(0, 6))
} else if (type === 'note') {
extractedHexEventId = data
}
// Normalize and deduplicate relays
relays = relays.map(url => normalizeUrl(url) || url)
@ -121,13 +127,30 @@ function EmbeddedNoteNotFound({ @@ -121,13 +127,30 @@ function EmbeddedNoteNotFound({
} catch (err) {
console.error('Failed to parse external relays:', err)
}
} else {
extractedHexEventId = noteId
}
const seenOn = client.getSeenEventRelayUrls(noteId)
setHexEventId(extractedHexEventId)
const seenOn = extractedHexEventId ? client.getSeenEventRelayUrls(extractedHexEventId) : []
relays.push(...seenOn)
// Normalize and deduplicate final relay list
const normalizedRelays = relays.map(url => normalizeUrl(url) || url)
// Normalize all relays first
let normalizedRelays = relays.map(url => normalizeUrl(url) || url).filter(Boolean)
normalizedRelays = Array.from(new Set(normalizedRelays))
// If no external relays from hints, try SEARCHABLE_RELAY_URLS as fallback
// Filter out relays that overlap with BIG_RELAY_URLS (already tried first)
if (normalizedRelays.length === 0) {
const searchableRelays = SEARCHABLE_RELAY_URLS
.map(url => normalizeUrl(url) || url)
.filter((url): url is string => Boolean(url))
.filter(relay => !BIG_RELAY_URLS.includes(relay))
normalizedRelays.push(...searchableRelays)
}
// Deduplicate final relay list
setExternalRelays(Array.from(new Set(normalizedRelays)))
}
@ -135,11 +158,11 @@ function EmbeddedNoteNotFound({ @@ -135,11 +158,11 @@ function EmbeddedNoteNotFound({
}, [noteId])
const handleTryExternalRelays = async () => {
if (isSearchingExternal) return
if (!hexEventId || isSearchingExternal) return
setIsSearchingExternal(true)
try {
const event = await client.fetchEventWithExternalRelays(noteId, [])
const event = await client.fetchEventWithExternalRelays(hexEventId, externalRelays)
if (event && onEventFound) {
onEventFound(event)
}
@ -195,11 +218,11 @@ function EmbeddedNoteNotFound({ @@ -195,11 +218,11 @@ function EmbeddedNoteNotFound({
)}
{!triedExternal && !hasExternalRelays && (
<p className="text-xs text-center">{t('No external relay hints available')}</p>
<div className="text-xs text-center">{t('No external relay hints available')}</div>
)}
{triedExternal && (
<p className="text-xs text-center">{t('Note could not be found anywhere')}</p>
<div className="text-xs text-center">{t('Note could not be found anywhere')}</div>
)}
<ClientSelect className="w-full" originalNoteId={noteId} />

38
src/components/Note/Article/index.tsx

@ -6,7 +6,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react' @@ -6,7 +6,6 @@ import { ChevronDown, ChevronRight } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
import { useMemo, useState, useEffect, useRef } from 'react'
import { useEventFieldParser } from '@/hooks/useContentParser'
import WebPreview from '../../WebPreview'
import HighlightSourcePreview from '../../UniversalContent/HighlightSourcePreview'
import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
@ -249,7 +248,7 @@ export default function Article({ @@ -249,7 +248,7 @@ export default function Article({
/>
{/* Collapsible Article Info - only for article-type events */}
{isArticleType && (parsedContent?.media?.length > 0 || parsedContent?.links?.length > 0 || parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
{isArticleType && (parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
<Collapsible open={isInfoOpen} onOpenChange={setIsInfoOpen} className="mt-4">
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between">
@ -258,41 +257,6 @@ export default function Article({ @@ -258,41 +257,6 @@ export default function Article({
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-2">
{/* Media thumbnails */}
{parsedContent?.media?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">
<h4 className="text-sm font-semibold mb-3">Images in this article:</h4>
<div className="grid grid-cols-8 sm:grid-cols-12 md:grid-cols-16 gap-1">
{parsedContent?.media?.map((media, index) => (
<div key={index} className="aspect-square">
<ImageWithLightbox
image={media}
className="w-full h-full object-cover rounded cursor-pointer hover:opacity-80 transition-opacity"
classNames={{
wrapper: 'w-full h-full'
}}
/>
</div>
))}
</div>
</div>
)}
{/* Links summary with OpenGraph previews */}
{parsedContent?.links?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">
<h4 className="text-sm font-semibold mb-3">Links in this article:</h4>
<div className="space-y-3">
{parsedContent?.links?.map((link, index) => (
<WebPreview
key={index}
url={link.url}
className="w-full"
/>
))}
</div>
</div>
)}
{/* Nostr links summary */}
{parsedContent?.nostrLinks?.length > 0 && (

39
src/components/Note/AsciidocArticle/AsciidocArticle.tsx

@ -1,14 +1,11 @@ @@ -1,14 +1,11 @@
import { useSecondaryPage } from '@/PageManager'
import ImageWithLightbox from '@/components/ImageWithLightbox'
import ImageCarousel from '@/components/ImageCarousel/ImageCarousel'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNoteList } from '@/lib/link'
import { extractAllImagesFromEvent } from '@/lib/image-extraction'
import { ChevronDown, ChevronRight } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
import { useMemo, useState, useEffect, useRef } from 'react'
import { useEventFieldParser } from '@/hooks/useContentParser'
import WebPreview from '../../WebPreview'
import HighlightSourcePreview from '../../UniversalContent/HighlightSourcePreview'
import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
@ -26,10 +23,6 @@ export default function AsciidocArticle({ @@ -26,10 +23,6 @@ export default function AsciidocArticle({
const { push } = useSecondaryPage()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const [isInfoOpen, setIsInfoOpen] = useState(false)
const [isImagesOpen, setIsImagesOpen] = useState(false)
// Extract all images from the event
const allImages = useMemo(() => extractAllImagesFromEvent(event), [event])
// Determine if this is an article-type event that should show ToC and Article Info
const isArticleType = useMemo(() => {
@ -310,23 +303,9 @@ export default function AsciidocArticle({ @@ -310,23 +303,9 @@ export default function AsciidocArticle({
dangerouslySetInnerHTML={{ __html: parsedContent?.html || '' }}
/>
{/* Image Carousel - Collapsible */}
{!hideImagesAndInfo && allImages.length > 0 && (
<Collapsible open={isImagesOpen} onOpenChange={setIsImagesOpen} className="mt-8">
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between">
<span>Images in this article ({allImages.length})</span>
{isImagesOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-4">
<ImageCarousel images={allImages} />
</CollapsibleContent>
</Collapsible>
)}
{/* Collapsible Article Info - only for article-type events */}
{!hideImagesAndInfo && isArticleType && (parsedContent?.links?.length > 0 || parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
{!hideImagesAndInfo && isArticleType && (parsedContent?.nostrLinks?.length > 0 || parsedContent?.highlightSources?.length > 0 || parsedContent?.hashtags?.length > 0) && (
<Collapsible open={isInfoOpen} onOpenChange={setIsInfoOpen} className="mt-4">
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between">
@ -336,22 +315,6 @@ export default function AsciidocArticle({ @@ -336,22 +315,6 @@ export default function AsciidocArticle({
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 mt-2">
{/* Links summary with OpenGraph previews */}
{parsedContent?.links?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">
<h4 className="text-sm font-semibold mb-3">Links in this article:</h4>
<div className="space-y-3">
{parsedContent?.links?.map((link, index) => (
<WebPreview
key={index}
url={link.url}
className="w-full"
/>
))}
</div>
</div>
)}
{/* Nostr links summary */}
{parsedContent?.nostrLinks?.length > 0 && (
<div className="p-4 bg-muted rounded-lg">

23
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -1,15 +1,14 @@ @@ -1,15 +1,14 @@
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
import ImageWithLightbox from '@/components/ImageWithLightbox'
import ImageCarousel from '@/components/ImageCarousel/ImageCarousel'
import MediaPlayer from '@/components/MediaPlayer'
import Wikilink from '@/components/UniversalContent/Wikilink'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNote, toNoteList, toProfile } from '@/lib/link'
import { useMediaExtraction } from '@/hooks'
import { cleanUrl } from '@/lib/url'
import { ExternalLink, ChevronDown, ChevronRight } from 'lucide-react'
import { ExternalLink } from 'lucide-react'
import { Event, kinds } from 'nostr-tools'
import React, { useMemo, useEffect, useRef, useState } from 'react'
import React, { useMemo, useEffect, useRef } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
@ -17,8 +16,6 @@ import 'katex/dist/katex.min.css' @@ -17,8 +16,6 @@ import 'katex/dist/katex.min.css'
import NostrNode from './NostrNode'
import { remarkNostr } from './remarkNostr'
import { Components } from './types'
import { Button } from '@/components/ui/button'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
export default function MarkdownArticle({
event,
@ -31,7 +28,6 @@ export default function MarkdownArticle({ @@ -31,7 +28,6 @@ export default function MarkdownArticle({
}) {
const { push } = useSecondaryPage()
const metadata = useMemo(() => getLongFormArticleMetadataFromEvent(event), [event])
const [isImagesOpen, setIsImagesOpen] = useState(false)
const contentRef = useRef<HTMLDivElement>(null)
// Use unified media extraction service
@ -466,21 +462,6 @@ export default function MarkdownArticle({ @@ -466,21 +462,6 @@ export default function MarkdownArticle({
</div>
)}
{/* Image Carousel - Only show for article content (30023, 30041, 30817, 30818) */}
{/* Only show images that aren't already in the content (from tags only) */}
{showImageGallery && carouselImages.length > 0 && (
<Collapsible open={isImagesOpen} onOpenChange={setIsImagesOpen} className="mt-8">
<CollapsibleTrigger asChild>
<Button variant="outline" className="w-full justify-between">
<span>Images in this article ({carouselImages.length})</span>
{isImagesOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="mt-4">
<ImageCarousel images={carouselImages} />
</CollapsibleContent>
</Collapsible>
)}
{metadata.tags.filter(tag => !contentHashtags.has(tag.toLowerCase())).length > 0 && (
<div className="flex gap-2 flex-wrap pb-2 mt-4">
{metadata.tags

47
src/pages/secondary/NotePage/NotFound.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import ClientSelect from '@/components/ClientSelect'
import { Button } from '@/components/ui/button'
import { BIG_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service'
import { AlertCircle, Search } from 'lucide-react'
@ -18,6 +19,7 @@ export default function NotFound({ @@ -18,6 +19,7 @@ export default function NotFound({
const [isSearchingExternal, setIsSearchingExternal] = useState(false)
const [triedExternal, setTriedExternal] = useState(false)
const [externalRelays, setExternalRelays] = useState<string[]>([])
const [hexEventId, setHexEventId] = useState<string | null>(null)
// Calculate which external relays would be tried (excluding already-tried relays)
useEffect(() => {
@ -28,6 +30,7 @@ export default function NotFound({ @@ -28,6 +30,7 @@ export default function NotFound({
const alreadyTriedRelays: string[] = await client.getAlreadyTriedRelays()
let externalRelays: string[] = []
let extractedHexEventId: string | null = null
// Parse relay hints and author from bech32 ID
if (!/^[0-9a-f]{64}$/.test(bech32Id)) {
@ -35,6 +38,7 @@ export default function NotFound({ @@ -35,6 +38,7 @@ export default function NotFound({
const { type, data } = nip19.decode(bech32Id)
if (type === 'nevent') {
extractedHexEventId = data.id
if (data.relays) externalRelays.push(...data.relays)
if (data.author) {
const authorRelayList = await client.fetchRelayList(data.author)
@ -44,6 +48,8 @@ export default function NotFound({ @@ -44,6 +48,8 @@ export default function NotFound({
if (data.relays) externalRelays.push(...data.relays)
const authorRelayList = await client.fetchRelayList(data.pubkey)
externalRelays.push(...authorRelayList.write.slice(0, 6))
} else if (type === 'note') {
extractedHexEventId = data
}
// Normalize and deduplicate external relays
externalRelays = externalRelays.map(url => normalizeUrl(url) || url)
@ -51,28 +57,45 @@ export default function NotFound({ @@ -51,28 +57,45 @@ export default function NotFound({
} catch (err) {
console.error('Failed to parse external relays:', err)
}
} else {
extractedHexEventId = bech32Id
}
const seenOn = client.getSeenEventRelayUrls(bech32Id)
setHexEventId(extractedHexEventId)
const seenOn = extractedHexEventId ? client.getSeenEventRelayUrls(extractedHexEventId) : []
externalRelays.push(...seenOn)
// Normalize all relays first
let normalizedRelays = externalRelays.map(url => normalizeUrl(url) || url).filter(Boolean)
normalizedRelays = Array.from(new Set(normalizedRelays))
// If no external relays from hints, try SEARCHABLE_RELAY_URLS as fallback
// Filter out relays that overlap with BIG_RELAY_URLS (already tried first)
if (normalizedRelays.length === 0) {
const searchableRelays = SEARCHABLE_RELAY_URLS
.map(url => normalizeUrl(url) || url)
.filter((url): url is string => Boolean(url))
.filter(relay => !BIG_RELAY_URLS.includes(relay))
normalizedRelays.push(...searchableRelays)
}
// Filter out relays that were already tried in tiers 1-3
const newRelays = externalRelays.filter(relay => !alreadyTriedRelays.includes(relay))
const newRelays = normalizedRelays.filter(relay => !alreadyTriedRelays.includes(relay))
// Normalize and deduplicate final relay list
const normalizedRelays = newRelays.map(url => normalizeUrl(url) || url)
setExternalRelays(Array.from(new Set(normalizedRelays)))
// Deduplicate final relay list
setExternalRelays(Array.from(new Set(newRelays)))
}
getExternalRelays()
}, [bech32Id])
const handleTryExternalRelays = async () => {
if (!bech32Id || isSearchingExternal) return
if (!hexEventId || isSearchingExternal) return
setIsSearchingExternal(true)
try {
const event = await client.fetchEventWithExternalRelays(bech32Id, externalRelays)
const event = await client.fetchEventWithExternalRelays(hexEventId, externalRelays)
if (event && onEventFound) {
onEventFound(event)
}
@ -93,9 +116,9 @@ export default function NotFound({ @@ -93,9 +116,9 @@ export default function NotFound({
{bech32Id && !triedExternal && hasExternalRelays && (
<div className="flex flex-col items-center gap-3 max-w-md">
<p className="text-sm text-center text-muted-foreground">
<div className="text-sm text-center text-muted-foreground">
{t('The note was not found on your relays or default relays.')}
</p>
</div>
<Button
variant="default"
@ -132,13 +155,13 @@ export default function NotFound({ @@ -132,13 +155,13 @@ export default function NotFound({
)}
{bech32Id && !triedExternal && !hasExternalRelays && (
<p className="text-sm text-muted-foreground">
<div className="text-sm text-muted-foreground">
{t('No external relay hints available')}
</p>
</div>
)}
{triedExternal && (
<p className="text-sm">{t('Note could not be found anywhere')}</p>
<div className="text-sm">{t('Note could not be found anywhere')}</div>
)}
<ClientSelect originalNoteId={bech32Id} />

Loading…
Cancel
Save