Address review feedback on reposts and quote posts

- Stop following a repost chain past MAX_REPOST_DEPTH: previously it fell
  through to rendering the repost event's own JSON content as if it were
  a note. Now it shows a "chain too deep" message instead.
- Validate that each embedded repost tag is itself an array of strings,
  not just that `tags` is an array, so a malformed embed (e.g. tags: [1])
  can't crash rendering.
- Propagate the quoted note's relay hints when opening it from the
  preview, so "Open" can still resolve notes that only exist on a
  hinted relay.
- Give useCreateQuotePost() the same friendly "sign in" error as
  useToggleRepost() instead of relying on useNostrPublish()'s generic one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYiUtZMQeA5RHggQw73wto
This commit is contained in:
2026-09-07 22:01:50 +02:00
parent 64280470b5
commit 8efe7e4020
3 changed files with 19 additions and 7 deletions

View File

@@ -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 <RepostedNote event={event} compact={compact} className={className} depth={depth} />;
}
@@ -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) {
<span>reposted</span>
</div>
{original ? (
{tooDeep ? (
<p className="px-4 pb-3 pl-11 text-xs text-muted-foreground">
Repost chain is too deep to display.
</p>
) : original ? (
<NoteCard event={original} compact={compact} className="border-b-0" depth={depth + 1} />
) : fetched.isLoading ? (
<div className="space-y-2 px-4 py-3 pl-11">

View File

@@ -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 (
<div className={cn('mt-2 rounded-lg border border-border p-3', className)}>

View File

@@ -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({