From dd6aeb6bb2119541b11085b7dac8d7319863d907 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 11 Oct 2025 11:26:09 +0200 Subject: [PATCH] fix highlights and deal with authorizations from extension --- src/components/Note/Highlight/index.tsx | 115 ++++++++++++ src/components/PostEditor/HighlightEditor.tsx | 166 ++++++++++++++++++ src/components/PostEditor/PostContent.tsx | 89 +++++++--- src/components/PostEditor/PostOptions.tsx | 3 +- src/lib/draft-event.ts | 153 ++++++++++++++++ .../DiscussionsPage/CreateThreadDialog.tsx | 4 +- .../primary/DiscussionsPage/ThreadCard.tsx | 10 -- src/pages/primary/DiscussionsPage/index.tsx | 21 +-- src/providers/NostrProvider/index.tsx | 55 +++++- src/services/client.service.ts | 19 +- 10 files changed, 562 insertions(+), 73 deletions(-) create mode 100644 src/components/Note/Highlight/index.tsx create mode 100644 src/components/PostEditor/HighlightEditor.tsx diff --git a/src/components/Note/Highlight/index.tsx b/src/components/Note/Highlight/index.tsx new file mode 100644 index 0000000..67e5b3c --- /dev/null +++ b/src/components/Note/Highlight/index.tsx @@ -0,0 +1,115 @@ +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' + +export default function Highlight({ + event, + className +}: { + event: Event + className?: string +}) { + const { t } = useTranslation() + + // Extract the source (e-tag, a-tag, or r-tag) + const source = useMemo(() => { + const eTag = event.tags.find(tag => tag[0] === 'e') + if (eTag) { + const eventId = eTag[1] + return { + 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] + } + } + + return null + }, [event.tags]) + + // Extract the context (optional comment/surrounding context) + const context = useMemo(() => { + const contextTag = event.tags.find(tag => tag[0] === 'context') + return contextTag?.[1] || '' + }, [event.tags]) + + return ( +
+
+ +
+ {/* Highlighted text */} + {event.content && ( +
+ "{event.content}" +
+ )} + + {/* 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)}...` + } + + )} +
+ )} +
+
+
+ ) +} + diff --git a/src/components/PostEditor/HighlightEditor.tsx b/src/components/PostEditor/HighlightEditor.tsx new file mode 100644 index 0000000..0954c95 --- /dev/null +++ b/src/components/PostEditor/HighlightEditor.tsx @@ -0,0 +1,166 @@ +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Textarea } from '@/components/ui/textarea' +import { X } from 'lucide-react' +import { useState, useEffect } from 'react' +import { useTranslation } from 'react-i18next' +import { nip19 } from 'nostr-tools' + +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 +} + +interface HighlightEditorProps { + highlightData: HighlightData + setHighlightData: (data: HighlightData) => void + setIsHighlight: (value: boolean) => void +} + +export default function HighlightEditor({ + highlightData, + setHighlightData, + setIsHighlight +}: HighlightEditorProps) { + const { t } = useTranslation() + const [sourceInput, setSourceInput] = useState(highlightData.sourceValue) + const [description, setDescription] = useState(highlightData.description || '') + const [error, setError] = useState('') + + // Validate and parse the source input + useEffect(() => { + if (!sourceInput.trim()) { + setError('') + return + } + + // Check if it's a URL + if (sourceInput.startsWith('https://')) { + setError('') + setHighlightData({ + sourceType: 'url', + sourceValue: sourceInput, + description + }) + return + } + + // Try to parse as nostr identifier + try { + let hexId: string | undefined + + // Check if it's already a hex ID (64 char hex string) + if (/^[a-f0-9]{64}$/i.test(sourceInput)) { + hexId = sourceInput.toLowerCase() + setError('') + setHighlightData({ + sourceType: 'nostr', + sourceValue: sourceInput, + sourceHexId: hexId, + description + }) + return + } + + // Try to decode as nip19 identifier + const decoded = nip19.decode(sourceInput) + + if (decoded.type === 'note') { + hexId = decoded.data + setError('') + setHighlightData({ + sourceType: 'nostr', + sourceValue: sourceInput, // Keep original for reference + sourceHexId: hexId, // Store the hex ID + description + }) + } else if (decoded.type === 'nevent') { + hexId = decoded.data.id + setError('') + setHighlightData({ + sourceType: 'nostr', + sourceValue: sourceInput, // Keep the nevent for relay info + sourceHexId: hexId, // Store the hex ID + description + }) + } else if (decoded.type === 'naddr') { + // For naddr, we need to keep the full naddr string to extract kind:pubkey:identifier + setError('') + setHighlightData({ + sourceType: 'nostr', + sourceValue: sourceInput, // Keep the naddr for a-tag building + sourceHexId: undefined, // No hex ID for addressable events + description + }) + } else { + setError(t('Invalid source. Please enter a note ID, nevent, naddr, hex ID, or URL.')) + return + } + } catch (err) { + setError(t('Invalid source. Please enter a note ID, nevent, naddr, hex ID, or URL.')) + } + }, [sourceInput, description, setHighlightData, t]) + + return ( +
+
+
{t('Highlight Settings')}
+ +
+ +
+ + setSourceInput(e.target.value)} + placeholder={t('nevent1..., naddr1..., note1..., hex ID, or https://...')} + className={error ? 'border-destructive' : ''} + /> + {error && ( +

{error}

+ )} +

+ {t('Enter a Nostr event identifier (nevent, naddr, note, or hex ID) OR a web URL (https://). Not both.')} +

+
+ +
+ +