diff --git a/src/components/nostr/NoteCard.tsx b/src/components/nostr/NoteCard.tsx index 5db8052..442a10e 100644 --- a/src/components/nostr/NoteCard.tsx +++ b/src/components/nostr/NoteCard.tsx @@ -51,7 +51,7 @@ export function NoteCard({ event, compact, className, depth = 0 }: NoteCardProps // until this note is actually looked at instead of firing on every mount. const [revealed, setRevealed] = useState(false); - if ((event.kind === REPOST_KIND || event.kind === GENERIC_REPOST_KIND) && depth < MAX_REPOST_DEPTH) { + if (event.kind === REPOST_KIND || event.kind === GENERIC_REPOST_KIND) { return ; } @@ -130,12 +130,15 @@ function RepostedNote({ event, compact, className, depth }: RepostedNoteProps) { const { data: author } = useAuthor(event.pubkey); const name = displayName(event.pubkey, author?.metadata); - const embedded = useMemo(() => parseEmbeddedRepost(event), [event]); + const tooDeep = depth >= MAX_REPOST_DEPTH; + const embedded = useMemo(() => (tooDeep ? null : parseEmbeddedRepost(event)), [event, tooDeep]); const reference = repostReference(event); // A malformed, self-referential repost (its `e` tag points at itself) must // not be followed, or fetching "the original" would just re-render this - // same repost forever. - const fetchId = !embedded && reference && reference.id !== event.id ? reference.id : undefined; + // same repost forever. Once MAX_REPOST_DEPTH is hit, stop following + // reposts altogether rather than falling through to rendering the repost + // event's own (JSON) content as if it were a note. + const fetchId = !tooDeep && !embedded && reference && reference.id !== event.id ? reference.id : undefined; const fetched = useNote(fetchId, reference?.relay ? [reference.relay] : undefined); const original = embedded ?? fetched.data; @@ -153,7 +156,11 @@ function RepostedNote({ event, compact, className, depth }: RepostedNoteProps) { reposted - {original ? ( + {tooDeep ? ( +

+ Repost chain is too deep to display. +

+ ) : original ? ( ) : fetched.isLoading ? (
diff --git a/src/components/nostr/QuotedNotePreview.tsx b/src/components/nostr/QuotedNotePreview.tsx index ef70ab6..d258fce 100644 --- a/src/components/nostr/QuotedNotePreview.tsx +++ b/src/components/nostr/QuotedNotePreview.tsx @@ -3,6 +3,7 @@ import { NoteContent } from './NoteContent'; import { Skeleton } from '@/components/ui/skeleton'; import { useNote } from '@/hooks/useNote'; import { useWindowManager } from '@/os/useWindowManager'; +import { encodeRelayHints } from '@/lib/nostrUtils'; import { cn } from '@/lib/utils'; interface QuotedNotePreviewProps { @@ -24,7 +25,8 @@ export function QuotedNotePreview({ id, relays, className }: QuotedNotePreviewPr const { openApp } = useWindowManager(); const note = useNote(id, relays); - const open = () => openApp('notes', { id }); + const relayHints = encodeRelayHints(relays); + const open = () => openApp('notes', relayHints ? { id, relays: relayHints } : { id }); return (
diff --git a/src/hooks/useReposts.ts b/src/hooks/useReposts.ts index 1c6605d..5dc36c0 100644 --- a/src/hooks/useReposts.ts +++ b/src/hooks/useReposts.ts @@ -73,7 +73,8 @@ export function parseEmbeddedRepost(event: NostrEvent): NostrEvent | null { typeof parsed.content === 'string' && typeof parsed.created_at === 'number' && typeof parsed.kind === 'number' && - Array.isArray(parsed.tags) + Array.isArray(parsed.tags) && + parsed.tags.every((tag) => Array.isArray(tag) && tag.every((value) => typeof value === 'string')) ) { return parsed as NostrEvent; } @@ -176,11 +177,13 @@ interface CreateQuotePostInput { * embedded `nostr:` reference in the content for wider compatibility. */ export function useCreateQuotePost() { + const { user } = useCurrentUser(); const publish = useNostrPublish(); const hints = useRelayHints(); return useMutation({ mutationFn: async ({ target, content }: CreateQuotePostInput) => { + if (!user) throw new Error('Sign in to quote this note'); const trimmed = content.trim(); const reference = buildQuoteReference(target, hints); return publish.mutateAsync({