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:
@@ -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]">
|
||||
“{reason}”
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
“{event.content}”
|
||||
</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}
|
||||
|
||||
@@ -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]">
|
||||
“{reason}”
|
||||
</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 }) {
|
||||
|
||||
@@ -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]">
|
||||
“{reason}”
|
||||
</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">
|
||||
“{reason}”
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="vanish-stripes h-2 rounded-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-0">
|
||||
{/* Top caution stripe band */}
|
||||
|
||||
Reference in New Issue
Block a user