From 6dde7bba0ff403e05a005d63e3e1d42eb47dab82 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 25 Mar 2026 00:26:07 -0500 Subject: [PATCH] Improve kind 4 DM display: simplify card style and add compact view for embeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove gradient background and encryption footer from the full card, use a clean bordered card with centered sender description. Add EncryptedMessageCompact for quote posts, reply indicators, and the reply composer — dispatched from EmbeddedNote like vanish events. --- src/components/EmbeddedNote.tsx | 6 + src/components/EncryptedMessageContent.tsx | 184 +++++++++++++++------ 2 files changed, 142 insertions(+), 48 deletions(-) diff --git a/src/components/EmbeddedNote.tsx b/src/components/EmbeddedNote.tsx index 5eb1742e..1ab8aee5 100644 --- a/src/components/EmbeddedNote.tsx +++ b/src/components/EmbeddedNote.tsx @@ -8,6 +8,7 @@ import { Skeleton } from '@/components/ui/skeleton'; import { EmojifiedText } from '@/components/CustomEmoji'; import { ProfileHoverCard } from '@/components/ProfileHoverCard'; import { VanishCardCompact } from '@/components/VanishEventContent'; +import { EncryptedMessageCompact } from '@/components/EncryptedMessageContent'; import { useEvent } from '@/hooks/useEvent'; import { useAuthor } from '@/hooks/useAuthor'; import { genUserName } from '@/lib/genUserName'; @@ -97,6 +98,11 @@ export function EmbeddedNote({ eventId, relays, authorHint, className, disableHo return ; } + // Kind 4 encrypted DMs get a compact card instead of rendering ciphertext + if (event.kind === 4) { + return ; + } + return ; } diff --git a/src/components/EncryptedMessageContent.tsx b/src/components/EncryptedMessageContent.tsx index 906edec2..eb916469 100644 --- a/src/components/EncryptedMessageContent.tsx +++ b/src/components/EncryptedMessageContent.tsx @@ -1,6 +1,8 @@ import type { NostrEvent } from '@nostrify/nostrify'; -import { Lock, Mail } from 'lucide-react'; -import { Link } from 'react-router-dom'; +import { useMemo } from 'react'; +import { Mail } from 'lucide-react'; +import { Link, useNavigate } from 'react-router-dom'; +import { nip19 } from 'nostr-tools'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { ProfileHoverCard } from '@/components/ProfileHoverCard'; @@ -8,7 +10,9 @@ import { useAuthor } from '@/hooks/useAuthor'; import { useProfileUrl } from '@/hooks/useProfileUrl'; import { getAvatarShape } from '@/lib/avatarShape'; import { getDisplayName } from '@/lib/getDisplayName'; +import { genUserName } from '@/lib/genUserName'; import { Skeleton } from '@/components/ui/skeleton'; +import { timeAgo } from '@/lib/timeAgo'; import { cn } from '@/lib/utils'; interface EncryptedMessageContentProps { @@ -18,21 +22,8 @@ interface EncryptedMessageContentProps { className?: string; } -/** - * Detect whether the content uses NIP-04 (AES-256-CBC with ?iv=) or NIP-44 encryption. - * NIP-04 content contains a base64 body followed by `?iv=`. - * NIP-44 uses a versioned binary blob encoded as base64 without the ?iv= suffix. - */ -function detectEncryptionType(content: string): 'NIP-04' | 'NIP-44' | 'Unknown' { - if (!content) return 'Unknown'; - if (/\?iv=/.test(content)) return 'NIP-04'; - // NIP-44 payloads are pure base64 without the ?iv= marker - if (/^[A-Za-z0-9+/]+=*$/.test(content.trim()) && content.length > 20) return 'NIP-44'; - return 'Unknown'; -} - /** Renders a single participant avatar with name and link. */ -function Participant({ pubkey, side }: { pubkey: string; side: 'sender' | 'recipient' }) { +function Participant({ pubkey }: { pubkey: string }) { const author = useAuthor(pubkey); const metadata = author.data?.metadata; const avatarShape = getAvatarShape(metadata); @@ -49,7 +40,7 @@ function Participant({ pubkey, side }: { pubkey: string; side: 'sender' | 'recip } return ( -
+
e.stopPropagation()}> @@ -74,19 +65,19 @@ function Participant({ pubkey, side }: { pubkey: string; side: 'sender' | 'recip } /** - * Compact card for kind 4 (NIP-04 encrypted DM) events. + * Visual display for kind 4 (NIP-04 encrypted DM) events. * - * Instead of rendering the encrypted ciphertext, it shows a visual - * "mail in transit" display: sender avatar -> animated path with lock -> recipient avatar. - * Detects and labels the encryption type (NIP-04 vs NIP-44). + * Instead of rendering the encrypted ciphertext, it shows a + * "mail in transit" visualization: sender avatar -> dashed path with mail icon -> recipient avatar. */ export function EncryptedMessageContent({ event, compact: _compact, className }: EncryptedMessageContentProps) { const recipientPubkey = event.tags.find(([n]) => n === 'p')?.[1]; - const encryptionType = detectEncryptionType(event.content); + const senderAuthor = useAuthor(event.pubkey); + const senderName = getDisplayName(senderAuthor.data?.metadata, event.pubkey); if (!recipientPubkey) { return ( -
+

Encrypted message (no recipient found)

); @@ -94,39 +85,136 @@ export function EncryptedMessageContent({ event, compact: _compact, className }: return (
-
- {/* Main transit visualization */} -
-
- {/* Sender */} - +
+ {/* Description */} +

+ {senderName} sent a direct message +

- {/* Animated transit path */} -
- {/* Dotted line */} -
+
+ {/* Sender */} + - {/* Mail icon in the center */} -
-
- -
+ {/* Transit path */} +
+ {/* Dotted line */} +
+ + {/* Mail icon in the center */} +
+
+
- - {/* Recipient */} -
-
- {/* Footer with encryption badge */} -
- - - Encrypted with {encryptionType} - + {/* Recipient */} +
); } + +interface EncryptedMessageCompactProps { + event: { id: string; kind: number; pubkey: string; content: string; created_at: number; tags: string[][] }; + className?: string; +} + +/** + * Compact inline card for kind 4 events used in quote posts, reply indicators, + * and the reply composer. Matches the style of EmbeddedNoteCard. + */ +export function EncryptedMessageCompact({ event, className }: EncryptedMessageCompactProps) { + const navigate = useNavigate(); + const author = useAuthor(event.pubkey); + const metadata = author.data?.metadata; + const avatarShape = getAvatarShape(metadata); + const displayName = metadata?.name || genUserName(event.pubkey); + const recipientPubkey = event.tags.find(([n]) => n === 'p')?.[1]; + const recipientAuthor = useAuthor(recipientPubkey ?? ''); + const recipientName = recipientPubkey + ? getDisplayName(recipientAuthor.data?.metadata, recipientPubkey) + : undefined; + + const neventId = useMemo( + () => nip19.neventEncode({ id: event.id, author: event.pubkey }), + [event.id, event.pubkey], + ); + + const profileUrl = useProfileUrl(event.pubkey, metadata); + + return ( +
{ + e.stopPropagation(); + navigate(`/${neventId}`); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + e.stopPropagation(); + navigate(`/${neventId}`); + } + }} + > +
+ {/* Author row */} +
+ {author.isLoading ? ( + <> + + + + ) : ( + <> + + e.stopPropagation()} + > + + + + {displayName[0]?.toUpperCase()} + + + + + + + e.stopPropagation()} + > + {displayName} + + + + )} + + + · {timeAgo(event.created_at)} + +
+ + {/* Content line */} +

+ + + Sent a direct message{recipientName ? <> to {recipientName} : ''} + +

+
+
+ ); +}