Restrict unknown-kind previews to the NIP-31 alt tag
The alt-tag fallback shipped in9813a226let several display paths fall through to other tags (title, name, summary, description, d) or to the raw content when alt was absent. For an unknown kind like attestr.xyz (31871), that surfaced the opaque d-tag identifier 'e5272de9:289bce03a0b7:1777206698' as the preview and leaked raw content into the hover-card reply indicator — both worse than the 'This event kind is not supported' tombstone the feature was meant to produce. Tighten the fallback everywhere an unknown kind might render: - getEventFallbackText: only the NIP-31 alt tag; no title/name/d. - CommentContext.getEventDisplayName: known kinds keep title/name/d, unknown kinds consider only alt. getKindLabel returns 'an unsupported event' instead of 'a post', so 'Commenting on ...' never implies the root is a text note. - EmbeddedNote tagMeta: alt only, no title/name/description fallback. - ExternalContentHeader.AddressableEventPreview: drop d-tag fallback. - EmbeddedNaddr: gate rich title/description/content rendering behind a known-kind check; unknown kinds render UnknownKindContent instead of extractMetadata, which was leaking plaintext content as the description when the body wasn't JSON. Regression-of:9813a226
This commit is contained in:
Generated
+2
-2
@@ -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",
|
||||
|
||||
@@ -208,9 +208,11 @@ const KIND_ICONS: Partial<Record<number, React.ComponentType<{ className?: strin
|
||||
* Get a singular comment-context label for a kind number.
|
||||
* Only uses KIND_LABELS (which has proper singular forms with articles).
|
||||
* Never falls through to EXTRA_KINDS labels since those are plural/categorical.
|
||||
* Unknown kinds render as "an unsupported event" — never as "a post", which
|
||||
* would misrepresent arbitrary event kinds as text notes.
|
||||
*/
|
||||
function getKindLabel(kind: number): string {
|
||||
return KIND_LABELS[kind] ?? 'a post';
|
||||
return KIND_LABELS[kind] ?? 'an unsupported event';
|
||||
}
|
||||
|
||||
/** Parse a rootKind string into a label, handling both numeric and external content kinds. */
|
||||
@@ -277,6 +279,7 @@ function getEventDisplayName(event: NostrEvent): { text: string; icon?: React.Co
|
||||
const title = event.tags.find(([name]) => 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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<EmbeddedCardShell
|
||||
@@ -376,18 +386,24 @@ function EmbeddedNaddrCard({ event, className, disableHoverCards }: { event: Nos
|
||||
className={className}
|
||||
disableHoverCards={disableHoverCards}
|
||||
>
|
||||
{/* Title */}
|
||||
{title && (
|
||||
<p className="text-sm font-semibold leading-snug line-clamp-2">
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
{isKnownKind ? (
|
||||
<>
|
||||
{/* Title */}
|
||||
{title && (
|
||||
<p className="text-sm font-semibold leading-snug line-clamp-2">
|
||||
{title}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{truncatedDesc && (
|
||||
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-3">
|
||||
{truncatedDesc}
|
||||
</p>
|
||||
{/* Description */}
|
||||
{truncatedDesc && (
|
||||
<p className="text-xs text-muted-foreground leading-relaxed line-clamp-3">
|
||||
{truncatedDesc}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<UnknownKindContent event={event} className="mt-0" />
|
||||
)}
|
||||
|
||||
{/* Kind label and attachment indicators */}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
+9
-22
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user