From 52a31d1c21cc31bace02c5f696537fcc2d0e4f4b Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 11 Oct 2025 12:23:31 +0200 Subject: [PATCH] display context and handle text highlighting --- src/components/Note/Highlight.tsx | 132 ------------- src/components/Note/Highlight/index.tsx | 187 ++++++++++-------- src/components/Note/index.tsx | 39 ++-- src/components/NoteList/index.tsx | 7 +- src/components/PostEditor/HighlightEditor.tsx | 38 ++-- src/components/PostEditor/PostContent.tsx | 24 +-- src/lib/draft-event.ts | 11 +- 7 files changed, 176 insertions(+), 262 deletions(-) delete mode 100644 src/components/Note/Highlight.tsx diff --git a/src/components/Note/Highlight.tsx b/src/components/Note/Highlight.tsx deleted file mode 100644 index eca44c6..0000000 --- a/src/components/Note/Highlight.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { useFetchEvent, useTranslatedEvent } from '@/hooks' -import { createFakeEvent } from '@/lib/event' -import { toNote } from '@/lib/link' -import { isValidPubkey } from '@/lib/pubkey' -import { generateBech32IdFromATag, generateBech32IdFromETag } from '@/lib/tag' -import { cn } from '@/lib/utils' -import { useSecondaryPage } from '@/PageManager' -import { Event } from 'nostr-tools' -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' -import Content from '../Content' -import ContentPreview from '../ContentPreview' -import UserAvatar from '../UserAvatar' - -export default function Highlight({ event, className }: { event: Event; className?: string }) { - const translatedEvent = useTranslatedEvent(event.id) - const comment = useMemo( - () => (translatedEvent?.tags ?? event.tags).find((tag) => tag[0] === 'comment')?.[1], - [event, translatedEvent] - ) - - return ( -
- {comment && } -
-
-
- {translatedEvent?.content ?? event.content} -
-
- -
- ) -} - -function HighlightSource({ event }: { event: Event }) { - const { t } = useTranslation() - const { push } = useSecondaryPage() - const sourceTag = useMemo(() => { - let sourceTag: string[] | undefined - for (const tag of event.tags) { - if (tag[2] === 'source') { - sourceTag = tag - break - } - if (tag[0] === 'r') { - sourceTag = tag - continue - } else if (tag[0] === 'a') { - if (!sourceTag || sourceTag[0] !== 'r') { - sourceTag = tag - } - continue - } else if (tag[0] === 'e') { - if (!sourceTag || sourceTag[0] === 'e') { - sourceTag = tag - } - continue - } - } - - return sourceTag - }, [event]) - const { event: referenceEvent } = useFetchEvent( - sourceTag - ? sourceTag[0] === 'e' - ? generateBech32IdFromETag(sourceTag) - : sourceTag[0] === 'a' - ? generateBech32IdFromATag(sourceTag) - : undefined - : undefined - ) - const referenceEventId = useMemo(() => { - if (!sourceTag || sourceTag[0] === 'r') return - if (sourceTag[0] === 'e') { - return sourceTag[1] - } - if (sourceTag[0] === 'a') { - return generateBech32IdFromATag(sourceTag) - } - }, [sourceTag]) - const pubkey = useMemo(() => { - if (referenceEvent) { - return referenceEvent.pubkey - } - if (sourceTag && sourceTag[0] === 'a') { - const [, pubkey] = sourceTag[1].split(':') - if (isValidPubkey(pubkey)) { - return pubkey - } - } - }, [sourceTag, referenceEvent]) - - if (!sourceTag) { - return null - } - - if (sourceTag[0] === 'r') { - return ( -
- {t('From')}{' '} - e.stopPropagation()} - > - {sourceTag[1]} - -
- ) - } - - return ( -
-
{t('From')}
- {pubkey && } - {referenceEventId && ( -
{ - e.stopPropagation() - push(toNote(referenceEvent ?? referenceEventId)) - }} - > - {referenceEvent ? : referenceEventId} -
- )} -
- ) -} diff --git a/src/components/Note/Highlight/index.tsx b/src/components/Note/Highlight/index.tsx index 67e5b3c..5e928ab 100644 --- a/src/components/Note/Highlight/index.tsx +++ b/src/components/Note/Highlight/index.tsx @@ -1,7 +1,6 @@ import { SecondaryPageLink } from '@/PageManager' import { Event } from 'nostr-tools' import { ExternalLink, Highlighter } from 'lucide-react' -import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { nip19 } from 'nostr-tools' import { toNote } from '@/lib/link' @@ -13,103 +12,127 @@ export default function Highlight({ event: Event className?: string }) { - const { t } = useTranslation() + try { + const { t } = useTranslation() - // Extract the source (e-tag, a-tag, or r-tag) - const source = useMemo(() => { + // Extract the source (e-tag, a-tag, or r-tag) - simplified without useMemo + let source = null const eTag = event.tags.find(tag => tag[0] === 'e') if (eTag) { const eventId = eTag[1] - return { + source = { type: 'event' as const, value: eventId, bech32: nip19.noteEncode(eventId) } - } - - const aTag = event.tags.find(tag => tag[0] === 'a') - if (aTag) { - const [kind, pubkey, identifier] = aTag[1].split(':') - const relay = aTag[2] - return { - type: 'addressable' as const, - value: aTag[1], - bech32: nip19.naddrEncode({ - kind: parseInt(kind), - pubkey, - identifier: identifier || '', - relays: relay ? [relay] : [] - }) - } - } - - const rTag = event.tags.find(tag => tag[0] === 'r' && tag[2] === 'source') - if (rTag) { - return { - type: 'url' as const, - value: rTag[1], - bech32: rTag[1] + } else { + const aTag = event.tags.find(tag => tag[0] === 'a') + if (aTag) { + const [kind, pubkey, identifier] = aTag[1].split(':') + const relay = aTag[2] + source = { + type: 'addressable' as const, + value: aTag[1], + bech32: nip19.naddrEncode({ + kind: parseInt(kind), + pubkey, + identifier: identifier || '', + relays: relay ? [relay] : [] + }) + } + } else { + const rTag = event.tags.find(tag => tag[0] === 'r' && tag[2] === 'source') + if (rTag) { + source = { + type: 'url' as const, + value: rTag[1], + bech32: rTag[1] + } + } } } - return null - }, [event.tags]) - - // Extract the context (optional comment/surrounding context) - const context = useMemo(() => { + // Extract the context (the main quote/full text being highlighted from) const contextTag = event.tags.find(tag => tag[0] === 'context') - return contextTag?.[1] || '' - }, [event.tags]) + const context = contextTag?.[1] || event.content // Default to content if no context + + // The event.content is the highlighted portion + const highlightedText = event.content - return ( -
-
- + return ( +
- {/* Highlighted text */} - {event.content && ( -
- "{event.content}" -
- )} + {/* Full quoted text with highlighted portion */} + {context && ( +
+ {contextTag && highlightedText ? ( + // If we have both context and highlighted text, show the highlight within the context +
+ {context.split(highlightedText).map((part, index) => ( + + {part} + {index < context.split(highlightedText).length - 1 && ( + + {highlightedText} + + )} + + ))} +
+ ) : ( + // If no context tag, just show the content as a regular quote +
+ "{context}" +
+ )} +
+ )} - {/* Context (user's comment or surrounding context) - rendered as plaintext */} - {context && ( -
- {context} -
- )} - - {/* Source link */} - {source && ( -
- {t('Source')}: - {source.type === 'url' ? ( - - {source.value.length > 50 ? source.value.substring(0, 50) + '...' : source.value} - - - ) : ( - - {source.type === 'event' - ? `note1${source.bech32.substring(5, 13)}...` - : `naddr1${source.bech32.substring(6, 14)}...` - } - - )} -
- )} + {/* Source link */} + {source && ( +
+ {t('Source')}: + {source.type === 'url' ? ( + + {source.value.length > 50 ? source.value.substring(0, 50) + '...' : source.value} + + + ) : ( + + {source.type === 'event' + ? `note1${source.bech32.substring(5, 13)}...` + : `naddr1${source.bech32.substring(6, 14)}...` + } + + )} +
+ )} +
+
+ ) + } catch (error) { + console.error('Highlight component error:', error) + return ( +
+
+ +
+
Highlight Error:
+
{String(error)}
+
Content: {event.content}
+
Context: {event.tags.find(tag => tag[0] === 'context')?.[1] || 'No context found'}
+
-
- ) + ) + } } diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index 126d414..672706f 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -22,6 +22,7 @@ import CommunityDefinition from './CommunityDefinition' import DiscussionContent from './DiscussionContent' import GroupMetadata from './GroupMetadata' import Highlight from './Highlight' + import IValue from './IValue' import LiveEvent from './LiveEvent' import LongFormArticle from './LongFormArticle' @@ -62,24 +63,38 @@ export default function Note({ const [showMuted, setShowMuted] = useState(false) let content: React.ReactNode - if ( - ![ - ...SUPPORTED_KINDS, - kinds.CommunityDefinition, - kinds.LiveEvent, - ExtendedKind.GROUP_METADATA, - ExtendedKind.PUBLIC_MESSAGE, - ExtendedKind.ZAP_REQUEST, - ExtendedKind.ZAP_RECEIPT - ].includes(event.kind) - ) { + + const supportedKindsList = [ + ...SUPPORTED_KINDS, + kinds.CommunityDefinition, + kinds.LiveEvent, + ExtendedKind.GROUP_METADATA, + ExtendedKind.PUBLIC_MESSAGE, + ExtendedKind.ZAP_REQUEST, + ExtendedKind.ZAP_RECEIPT + ] + + + if (!supportedKindsList.includes(event.kind)) { + console.log('Note component - rendering UnknownNote for unsupported kind:', event.kind) content = } else if (mutePubkeySet.has(event.pubkey) && !showMuted) { content = setShowMuted(true)} /> } else if (!defaultShowNsfw && isNsfwEvent(event) && !showNsfw) { content = setShowNsfw(true)} /> } else if (event.kind === kinds.Highlights) { - content = + // Try to render the Highlight component with error boundary + try { + content = + } catch (error) { + console.error('Note component - Error rendering Highlight component:', error) + content =
+
HIGHLIGHT ERROR:
+
Error: {String(error)}
+
Content: {event.content}
+
Context: {event.tags.find(tag => tag[0] === 'context')?.[1] || 'No context found'}
+
+ } } else if (event.kind === kinds.LongFormArticle) { content = showFull ? ( diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 49a5734..dacddb0 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -153,10 +153,11 @@ const NoteList = forwardRef( useImperativeHandle(ref, () => ({ scrollToTop, refresh }), []) - useEffect(() => { - if (!subRequests.length) return + useEffect(() => { + if (!subRequests.length) return - async function init() { + async function init() { + setLoading(true) setEvents([]) setNewEvents([]) diff --git a/src/components/PostEditor/HighlightEditor.tsx b/src/components/PostEditor/HighlightEditor.tsx index 0954c95..5a22b99 100644 --- a/src/components/PostEditor/HighlightEditor.tsx +++ b/src/components/PostEditor/HighlightEditor.tsx @@ -11,7 +11,7 @@ export interface HighlightData { sourceType: 'nostr' | 'url' sourceValue: string // nevent/naddr/note/hex for nostr, https:// URL for url sourceHexId?: string // converted hex ID for nostr sources - description?: string // optional comment/description + context?: string // the full text/quote that the highlight is from } interface HighlightEditorProps { @@ -27,7 +27,7 @@ export default function HighlightEditor({ }: HighlightEditorProps) { const { t } = useTranslation() const [sourceInput, setSourceInput] = useState(highlightData.sourceValue) - const [description, setDescription] = useState(highlightData.description || '') + const [context, setContext] = useState(highlightData.context || '') const [error, setError] = useState('') // Validate and parse the source input @@ -43,7 +43,7 @@ export default function HighlightEditor({ setHighlightData({ sourceType: 'url', sourceValue: sourceInput, - description + context }) return } @@ -60,7 +60,7 @@ export default function HighlightEditor({ sourceType: 'nostr', sourceValue: sourceInput, sourceHexId: hexId, - description + context }) return } @@ -75,7 +75,7 @@ export default function HighlightEditor({ sourceType: 'nostr', sourceValue: sourceInput, // Keep original for reference sourceHexId: hexId, // Store the hex ID - description + context }) } else if (decoded.type === 'nevent') { hexId = decoded.data.id @@ -84,7 +84,7 @@ export default function HighlightEditor({ sourceType: 'nostr', sourceValue: sourceInput, // Keep the nevent for relay info sourceHexId: hexId, // Store the hex ID - description + context }) } else if (decoded.type === 'naddr') { // For naddr, we need to keep the full naddr string to extract kind:pubkey:identifier @@ -93,7 +93,7 @@ export default function HighlightEditor({ sourceType: 'nostr', sourceValue: sourceInput, // Keep the naddr for a-tag building sourceHexId: undefined, // No hex ID for addressable events - description + context }) } else { setError(t('Invalid source. Please enter a note ID, nevent, naddr, hex ID, or URL.')) @@ -102,7 +102,7 @@ export default function HighlightEditor({ } catch (err) { setError(t('Invalid source. Please enter a note ID, nevent, naddr, hex ID, or URL.')) } - }, [sourceInput, description, setHighlightData, t]) + }, [sourceInput, context, setHighlightData, t]) return (
@@ -139,26 +139,28 @@ export default function HighlightEditor({