diff --git a/package-lock.json b/package-lock.json index 51ee3c6c..1c60cfb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ditto", - "version": "2.11.0", + "version": "2.11.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ditto", - "version": "2.11.0", + "version": "2.11.1", "dependencies": { "@capacitor/app": "^8.0.0", "@capacitor/core": "^8.1.0", diff --git a/src/components/CommentContext.tsx b/src/components/CommentContext.tsx index a8ebb646..a9f6181f 100644 --- a/src/components/CommentContext.tsx +++ b/src/components/CommentContext.tsx @@ -208,9 +208,11 @@ const KIND_ICONS: Partial name === 'title')?.[1]; const name = event.tags.find(([name]) => name === 'name')?.[1]; const dTag = event.tags.find(([name]) => name === 'd')?.[1]; + const alt = event.tags.find(([name]) => name === 'alt')?.[1]?.trim(); const displayTitle = title || name || dTag; // Kinds with a custom postfix (e.g. "Ditto on Zapstore") @@ -291,10 +294,17 @@ function getEventDisplayName(event: NostrEvent): { text: string; icon?: React.Co return { text: `${displayTitle} ${suffix}`, icon }; } - // Generic: just use the title if available - if (displayTitle) return { text: displayTitle, icon }; + // Known kinds: use the conventional title/name/d tag if available. + if (KIND_LABELS[event.kind] && displayTitle) { + return { text: displayTitle, icon }; + } - // Fall back to kind label + // Unknown kinds: only trust the NIP-31 `alt` tag. title/name/d have + // kind-specific semantics we can't interpret; `d` in particular is often + // an opaque compound identifier. + if (alt) return { text: alt, icon }; + + // Fall back to kind label ("an unsupported event" for unknown kinds). return { text: getKindLabel(event.kind), icon }; } diff --git a/src/components/EmbeddedNaddr.tsx b/src/components/EmbeddedNaddr.tsx index 407606c9..455306fd 100644 --- a/src/components/EmbeddedNaddr.tsx +++ b/src/components/EmbeddedNaddr.tsx @@ -25,6 +25,7 @@ import { isProfileBadgesKind } from '@/lib/badgeUtils'; import { timeAgo } from '@/lib/timeAgo'; import { cn } from '@/lib/utils'; import { getKindLabel, getKindIcon } from '@/lib/extraKinds'; +import { UnknownKindContent } from '@/components/UnknownKindContent'; interface EmbeddedNaddrProps { /** The decoded naddr coordinates. */ @@ -353,7 +354,17 @@ function EmbeddedNaddrCard({ event, className, disableHoverCards }: { event: Nos return nip19.naddrEncode({ kind: event.kind, pubkey: event.pubkey, identifier: dTag }); }, [event]); - const { title, description, image } = useMemo(() => extractMetadata(event), [event]); + // Known kinds (articles, streams, themes, etc.) get rich title/description/ + // content rendering. Unknown kinds never do — we can't assume arbitrary + // content is safe user-facing text, so we fall back to a NIP-31 `alt` + // preview or a tombstone. + const kindLabel = getKindLabel(event.kind); + const isKnownKind = kindLabel !== undefined; + + const { title, description, image } = useMemo( + () => (isKnownKind ? extractMetadata(event) : { title: undefined, description: undefined, image: undefined }), + [isKnownKind, event], + ); const truncatedDesc = useMemo(() => { if (!description) return undefined; @@ -363,10 +374,9 @@ function EmbeddedNaddrCard({ event, className, disableHoverCards }: { event: Nos // Kind label for context (e.g. "nsite" with icon) const kindMeta = useMemo(() => { - const label = getKindLabel(event.kind); - if (!label) return undefined; - return { label, Icon: getKindIcon(event.kind) }; - }, [event.kind]); + if (!kindLabel) return undefined; + return { label: kindLabel, Icon: getKindIcon(event.kind) }; + }, [kindLabel, event.kind]); return ( - {/* Title */} - {title && ( -

- {title} -

- )} + {isKnownKind ? ( + <> + {/* Title */} + {title && ( +

+ {title} +

+ )} - {/* Description */} - {truncatedDesc && ( -

- {truncatedDesc} -

+ {/* Description */} + {truncatedDesc && ( +

+ {truncatedDesc} +

+ )} + + ) : ( + )} {/* Kind label and attachment indicators */} diff --git a/src/components/EmbeddedNote.tsx b/src/components/EmbeddedNote.tsx index 7e636209..ca357ec1 100644 --- a/src/components/EmbeddedNote.tsx +++ b/src/components/EmbeddedNote.tsx @@ -346,17 +346,13 @@ function EmbeddedNoteCard({ const tagMeta = useMemo(() => { // Content kinds with real content always render that content below. if (isContentKind && hasContent) return undefined; - const getTag = (name: string) => { - const v = event.tags.find(([n]) => n === name)?.[1]; - return v && v.trim().length > 0 ? v.trim() : undefined; - }; - // NIP-31 `alt` is the author's own fallback. Prefer title/name for - // addressable content that has those conventional tags; otherwise - // use alt or the d-tag identifier. - const title = getTag('title') || getTag('name') || getTag('alt') || getTag('d'); - const description = getTag('summary') || getTag('description'); - if (!title && !description) return undefined; - return { title, description }; + // NIP-31 `alt` is the author's own fallback for clients that can't + // render the kind. Other tags (title, name, d, …) have kind-specific + // semantics and are not reliably safe as user-facing preview text. + const altTag = event.tags.find(([n]) => n === 'alt')?.[1]; + const title = altTag && altTag.trim().length > 0 ? altTag.trim() : undefined; + if (!title) return undefined; + return { title, description: undefined as string | undefined }; }, [isContentKind, hasContent, event.tags]); // Unknown/unsupported kind with no displayable tags and no content-kind body. diff --git a/src/components/ExternalContentHeader.tsx b/src/components/ExternalContentHeader.tsx index b7d561ad..25775733 100644 --- a/src/components/ExternalContentHeader.tsx +++ b/src/components/ExternalContentHeader.tsx @@ -1255,10 +1255,7 @@ export function AddressableEventPreview({ addr }: { addr: { kind: number; pubkey return FileText; }, [kindDef, addr.kind]); - const title = event?.tags.find(([n]) => n === 'title')?.[1] - || event?.tags.find(([n]) => n === 'name')?.[1] - || event?.tags.find(([n]) => n === 'alt')?.[1] - || event?.tags.find(([n]) => n === 'd')?.[1] + const title = event?.tags.find(([n]) => n === 'alt')?.[1] || kindLabel; const thumbnail = event ? extractThumbnail(event.tags) : undefined; const isVideo = event ? hasVideo(event.tags) : false; diff --git a/src/lib/extraKinds.ts b/src/lib/extraKinds.ts index 95fa312f..81b20e85 100644 --- a/src/lib/extraKinds.ts +++ b/src/lib/extraKinds.ts @@ -693,30 +693,17 @@ export function getAllExtraKindNumbers(): number[] { } /** - * Extract a human-readable fallback label from an event for display by clients - * that don't know how to render the event's kind. + * Extract the NIP-31 `alt` tag — the author's own human-readable fallback + * text for clients that don't know how to render the event's kind. * - * Resolution order (per NIP-31): - * 1. NIP-31 `alt` tag — the author's own fallback text - * 2. `title` tag — common on articles, calendar events, streams, podcasts, etc. - * 3. `name` tag — common on badges, emoji packs, people lists - * 4. `summary` or `description` tag - * 5. `d` tag — the addressable identifier (last resort, often a slug) + * Only `alt` is consulted. Other tags (`title`, `name`, `summary`, + * `description`, `d`) are intentionally excluded: they have kind-specific + * semantics and are not guaranteed to be safe user-facing text. When `alt` + * is missing, callers should render a neutral "unsupported kind" tombstone. * - * Returns `undefined` if the event carries no displayable text at all. + * Returns `undefined` if the event has no `alt` tag (or it's blank). */ export function getEventFallbackText(event: NostrEvent): string | undefined { - const getTag = (name: string) => { - const value = event.tags.find(([n]) => n === name)?.[1]; - return value && value.trim().length > 0 ? value.trim() : undefined; - }; - - return ( - getTag('alt') || - getTag('title') || - getTag('name') || - getTag('summary') || - getTag('description') || - getTag('d') - ); + const alt = event.tags.find(([n]) => n === 'alt')?.[1]; + return alt && alt.trim().length > 0 ? alt.trim() : undefined; }