Consolidate 4 duplicate vanish compact cards into VanishCardCompact

The same stripes + glitch icon + title + npub + reason layout was
duplicated across EmbeddedVanishCard, EmbeddedVanishPost,
VanishEventContent compact mode, and an inline version in NoteCard's
threaded branch. Extract a single VanishCardCompact component and
replace all four copies with it.

Net reduction of 76 lines.
This commit is contained in:
Alex Gleason
2026-03-21 22:39:13 -05:00
parent 58b28f7c1a
commit 85b47a7a62
4 changed files with 94 additions and 170 deletions
+10 -52
View File
@@ -8,6 +8,7 @@ import { Skeleton } from '@/components/ui/skeleton';
import { EmojifiedText } from '@/components/CustomEmoji';
import { NoteCard } from '@/components/NoteCard';
import { ProfileHoverCard } from '@/components/ProfileHoverCard';
import { VanishCardCompact } from '@/components/VanishEventContent';
import { useEvent } from '@/hooks/useEvent';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
@@ -105,7 +106,7 @@ export function EmbeddedNote({ eventId, relays, authorHint, className, disableHo
// NIP-62 vanish events get their own dramatic inline card
if (event.kind === VANISH_KIND) {
return <EmbeddedVanishCard event={event} className={className} />;
return <EmbeddedVanishCardWrapper event={event} className={className} />;
}
return <EmbeddedNoteCard event={event} className={className} disableHoverCards={disableHoverCards} />;
@@ -356,8 +357,8 @@ function MaybeProfileHoverCard({ pubkey, disabled, children }: { pubkey: string;
);
}
/** Inline embedded card for NIP-62 Request to Vanish events. */
function EmbeddedVanishCard({
/** Clickable wrapper around VanishCardCompact for embedded/quoted vanish events. */
function EmbeddedVanishCardWrapper({
event,
className,
}: {
@@ -365,22 +366,14 @@ function EmbeddedVanishCard({
className?: string;
}) {
const navigate = useNavigate();
const npub = useMemo(() => nip19.npubEncode(event.pubkey), [event.pubkey]);
const neventId = useMemo(
() => nip19.neventEncode({ id: event.id, author: event.pubkey }),
[event.id, event.pubkey],
);
const isGlobal = event.tags.some(([n, v]) => n === 'relay' && v === 'ALL_RELAYS');
const reason = event.content || undefined;
return (
<div
className={cn(
'group block rounded-2xl border-2 border-red-500/30 overflow-hidden',
'hover:border-red-500/50 transition-colors cursor-pointer',
className,
)}
className={cn('group cursor-pointer', className)}
role="link"
tabIndex={0}
onClick={(e) => {
@@ -395,46 +388,11 @@ function EmbeddedVanishCard({
}
}}
>
{/* Top caution stripe */}
<div className="vanish-stripes h-1.5" />
<div className="px-3.5 py-3 space-y-2 bg-red-500/[0.04] dark:bg-red-500/[0.06]">
{/* Header row */}
<div className="flex items-center gap-2.5 min-w-0">
{/* Glitch icon */}
<div className="relative shrink-0">
<div className="size-8 rounded-full bg-red-500/10 border border-red-500/30 flex items-center justify-center">
<span className="text-sm font-black vanish-glitch-text text-red-500 dark:text-red-400" data-text="///">///</span>
</div>
<div className="absolute -bottom-0.5 -right-0.5 size-3.5 rounded-full bg-red-600 flex items-center justify-center">
<span className="text-[7px] font-black text-white leading-none">!</span>
</div>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-red-500 dark:text-red-400 leading-tight">
{isGlobal ? 'Global Request to Vanish' : 'Request to Vanish'}
</p>
<p className="text-[11px] text-muted-foreground font-mono truncate mt-0.5">
{npub}
</p>
</div>
<span className="text-[11px] text-muted-foreground shrink-0">
{timeAgo(event.created_at)}
</span>
</div>
{/* Reason quote if available */}
{reason && (
<p className="text-xs text-muted-foreground italic line-clamp-2 pl-[42px]">
&ldquo;{reason}&rdquo;
</p>
)}
</div>
{/* Bottom caution stripe */}
<div className="vanish-stripes h-1.5" />
<VanishCardCompact
event={event}
timestamp={timeAgo(event.created_at)}
className="rounded-2xl group-hover:border-red-500/50 transition-colors"
/>
</div>
);
}
+5 -30
View File
@@ -62,7 +62,7 @@ import { ReplyComposeModal } from "@/components/ReplyComposeModal";
import { ReplyContext } from "@/components/ReplyContext";
import { RepostMenu } from "@/components/RepostMenu";
import { ThemeContent } from "@/components/ThemeContent";
import { VanishEventContent } from "@/components/VanishEventContent";
import { VanishCardCompact } from "@/components/VanishEventContent";
import { ZapstoreAppContent } from "@/components/ZapstoreAppContent";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { getAvatarShape } from "@/lib/avatarShape";
@@ -632,7 +632,7 @@ export const NoteCard = memo(function NoteCard({
// ── Vanish layout (kind 62) — dramatic card, no author row ──
if (isVanish) {
// Threaded vanish (ancestor in a reply thread — needs connector line)
// Threaded vanish (ancestor in a reply thread — needs connector line + avatar column)
if (threaded || threadedLast) {
return (
<article
@@ -646,38 +646,13 @@ export const NoteCard = memo(function NoteCard({
>
<div className="flex gap-3">
<div className="flex flex-col items-center">
{/* Glitch icon matching VanishEventContent compact style */}
<div className="relative shrink-0">
<div className="size-10 rounded-full bg-red-500/10 border-2 border-red-500/30 flex items-center justify-center">
<span className="text-lg vanish-glitch-text" data-text="///">///</span>
</div>
<div className="absolute -bottom-0.5 -right-0.5 size-4 rounded-full bg-red-600 flex items-center justify-center">
<span className="text-[8px] font-black text-white leading-none">!</span>
</div>
</div>
{avatarElement}
{threaded && (
<div className="w-0.5 flex-1 mt-2 bg-foreground/20 rounded-full" />
)}
</div>
<div className={cn("flex-1 min-w-0", threaded && "pb-3")}>
{/* Stripes are inside the bordered container so they don't escape visually */}
<div className="rounded-xl border-2 border-red-500/30 bg-red-500/5 overflow-hidden">
<div className="vanish-stripes h-1.5" />
<div className="p-3">
<p className="text-sm font-bold text-red-500 dark:text-red-400">
{event.tags.some(([n, v]) => n === 'relay' && v === 'ALL_RELAYS') ? 'Global Request to Vanish' : 'Request to Vanish'}
</p>
<p className="text-xs text-muted-foreground mt-0.5 font-mono truncate">
{nip19.npubEncode(event.pubkey)}
</p>
{event.content && (
<p className="text-xs text-muted-foreground mt-1 line-clamp-2 italic">
&ldquo;{event.content}&rdquo;
</p>
)}
</div>
<div className="vanish-stripes h-1.5" />
</div>
<VanishCardCompact event={event} timestamp={timeAgo(event.created_at)} />
{!compact && (
<>
{actionButtons}
@@ -700,7 +675,7 @@ export const NoteCard = memo(function NoteCard({
onClick={handleCardClick}
onAuxClick={handleAuxClick}
>
<VanishEventContent event={event} compact />
<VanishCardCompact event={event} />
{!compact && (
<>
{actionButtons}
+2 -50
View File
@@ -14,6 +14,7 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { getAvatarShape } from '@/lib/avatarShape';
import { NoteContent } from '@/components/NoteContent';
import { ComposeBox } from '@/components/ComposeBox';
import { VanishCardCompact } from '@/components/VanishEventContent';
import { ProfilePreview } from '@/components/ExternalContentHeader';
import { useAuthor } from '@/hooks/useAuthor';
import { genUserName } from '@/lib/genUserName';
@@ -160,62 +161,13 @@ function EmbeddedPost({ event }: { event: NostrEvent }) {
// Kind 62 (Request to Vanish) — show a compact vanish preview
if (event.kind === 62) {
return <EmbeddedVanishPost event={event} />;
return <VanishCardCompact event={event} timestamp={timeAgo(event.created_at)} className="mx-4 mb-2" />;
}
return <EmbeddedNote event={event} />;
}
/** Compact embedded preview for NIP-62 vanish events in the reply composer. */
function EmbeddedVanishPost({ event }: { event: NostrEvent }) {
const npub = useMemo(() => nip19.npubEncode(event.pubkey), [event.pubkey]);
const isGlobal = event.tags.some(([n, v]) => n === 'relay' && v === 'ALL_RELAYS');
const reason = event.content || undefined;
return (
<div className="mx-4 mb-2 rounded-xl border-2 border-red-500/30 overflow-hidden">
{/* Top caution stripe */}
<div className="vanish-stripes h-1.5" />
<div className="px-3 py-2.5 bg-red-500/[0.04] dark:bg-red-500/[0.06] space-y-1.5">
{/* Header row */}
<div className="flex items-center gap-2.5 min-w-0">
<div className="relative shrink-0">
<div className="size-8 rounded-full bg-red-500/10 border border-red-500/30 flex items-center justify-center">
<span className="text-sm font-black vanish-glitch-text text-red-500 dark:text-red-400" data-text="///">///</span>
</div>
<div className="absolute -bottom-0.5 -right-0.5 size-3.5 rounded-full bg-red-600 flex items-center justify-center">
<span className="text-[7px] font-black text-white leading-none">!</span>
</div>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-red-500 dark:text-red-400 leading-tight">
{isGlobal ? 'Global Request to Vanish' : 'Request to Vanish'}
</p>
<p className="text-[11px] text-muted-foreground font-mono truncate mt-0.5">
{npub}
</p>
</div>
<span className="text-[11px] text-muted-foreground shrink-0">
{timeAgo(event.created_at)}
</span>
</div>
{/* Reason if available */}
{reason && (
<p className="text-xs text-muted-foreground italic line-clamp-2 pl-[42px]">
&ldquo;{reason}&rdquo;
</p>
)}
</div>
{/* Bottom caution stripe */}
<div className="vanish-stripes h-1.5" />
</div>
);
}
/** Compact embedded preview for regular note events. */
function EmbeddedNote({ event }: { event: NostrEvent }) {
+77 -38
View File
@@ -2,21 +2,97 @@ import type { NostrEvent } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools';
import { useMemo } from 'react';
import { cn } from '@/lib/utils';
interface VanishEventContentProps {
event: NostrEvent;
/** Compact mode for feed cards — shorter layout */
compact?: boolean;
}
interface VanishCardCompactProps {
event: { pubkey: string; content: string; tags: string[][] };
/** Optional className for the outer container */
className?: string;
/** Optional timestamp text shown to the right of the title */
timestamp?: string;
}
/**
* Compact card for NIP-62 Request to Vanish events.
*
* Used everywhere a vanish event needs a small inline preview: feed cards,
* embedded quotes, reply composer, threaded ancestors, etc. Stripes render
* inside the bordered container so they stay visually contained.
*/
export function VanishCardCompact({ event, className, timestamp }: VanishCardCompactProps) {
const npub = useMemo(() => nip19.npubEncode(event.pubkey), [event.pubkey]);
const isGlobal = event.tags.some(([n, v]) => n === 'relay' && v === 'ALL_RELAYS');
const reason = event.content || undefined;
return (
<div className={cn('rounded-xl border-2 border-red-500/30 overflow-hidden', className)}>
{/* Top caution stripe */}
<div className="vanish-stripes h-1.5" />
<div className="px-3 py-2.5 bg-red-500/[0.04] dark:bg-red-500/[0.06] space-y-1.5">
{/* Header row */}
<div className="flex items-center gap-2.5 min-w-0">
<div className="relative shrink-0">
<div className="size-8 rounded-full bg-red-500/10 border border-red-500/30 flex items-center justify-center">
<span className="text-sm font-black vanish-glitch-text text-red-500 dark:text-red-400" data-text="///">///</span>
</div>
<div className="absolute -bottom-0.5 -right-0.5 size-3.5 rounded-full bg-red-600 flex items-center justify-center">
<span className="text-[7px] font-black text-white leading-none">!</span>
</div>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-red-500 dark:text-red-400 leading-tight">
{isGlobal ? 'Global Request to Vanish' : 'Request to Vanish'}
</p>
<p className="text-[11px] text-muted-foreground font-mono truncate mt-0.5">
{npub}
</p>
</div>
{timestamp && (
<span className="text-[11px] text-muted-foreground shrink-0">
{timestamp}
</span>
)}
</div>
{/* Reason quote if available */}
{reason && (
<p className="text-xs text-muted-foreground italic line-clamp-2 pl-[42px]">
&ldquo;{reason}&rdquo;
</p>
)}
</div>
{/* Bottom caution stripe */}
<div className="vanish-stripes h-1.5" />
</div>
);
}
/**
* Dramatic display for NIP-62 Request to Vanish (kind 62) events.
*
* These events represent a user permanently erasing their entire identity
* from the Nostr network. The display is intentionally theatrical — red
* caution stripes, glitch effects, and a "grand exit" aesthetic.
*
* This is the full detail-page view. For compact inline previews, use
* {@link VanishCardCompact} instead.
*/
export function VanishEventContent({ event, compact }: VanishEventContentProps) {
const npub = useMemo(() => nip19.npubEncode(event.pubkey), [event.pubkey]);
if (compact) {
return <VanishCardCompact event={event} className="mt-2" />;
}
const npub = nip19.npubEncode(event.pubkey);
const relayTags = event.tags.filter(([n]) => n === 'relay');
const isGlobal = relayTags.some(([, v]) => v === 'ALL_RELAYS');
const relayList = relayTags.map(([, v]) => v).filter((v) => v !== 'ALL_RELAYS');
@@ -31,43 +107,6 @@ export function VanishEventContent({ event, compact }: VanishEventContentProps)
hour12: true,
});
if (compact) {
return (
<div className="mt-2 space-y-2">
{/* Caution stripe header */}
<div className="vanish-stripes h-2 rounded-full" />
<div className="flex items-start gap-3 rounded-xl border-2 border-red-500/30 bg-red-500/5 p-3">
{/* Glitch icon */}
<div className="relative shrink-0 mt-0.5">
<div className="size-10 rounded-full bg-red-500/10 border-2 border-red-500/30 flex items-center justify-center">
<span className="text-lg vanish-glitch-text" data-text="///">///</span>
</div>
<div className="absolute -bottom-0.5 -right-0.5 size-4 rounded-full bg-red-600 flex items-center justify-center">
<span className="text-[8px] font-black text-white leading-none">!</span>
</div>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-bold text-red-500 dark:text-red-400">
{isGlobal ? 'Global Request to Vanish' : 'Request to Vanish'}
</p>
<p className="text-xs text-muted-foreground mt-0.5 font-mono truncate">
{npub}
</p>
{reason && (
<p className="text-xs text-muted-foreground mt-1 line-clamp-2 italic">
&ldquo;{reason}&rdquo;
</p>
)}
</div>
</div>
<div className="vanish-stripes h-2 rounded-full" />
</div>
);
}
return (
<div className="mt-4 space-y-0">
{/* Top caution stripe band */}