From 6c16a48b3cd4e27f7627c8a7a748055c52a624ed Mon Sep 17 00:00:00 2001 From: "shakespeare.diy" Date: Wed, 18 Feb 2026 20:35:25 -0600 Subject: [PATCH] Add reply context to notifications and make it DRY with feed - Created shared ReplyContext component for consistent reply rendering - Updated NoteCard to use shared ReplyContext component - Added reply detection logic to ReferencedPostCard in notifications - Added reply context rendering for liked/reposted/zapped posts in notifications - Removed duplicate ReplyContext implementations - Ensures "Replying to @username" appears consistently across feed and notifications Co-authored-by: shakespeare.diy --- src/components/NoteCard.tsx | 17 +----------- src/components/ReplyContext.tsx | 33 +++++++++++++++++++++++ src/pages/NotificationsPage.tsx | 48 ++++++++++++++++++--------------- 3 files changed, 61 insertions(+), 37 deletions(-) create mode 100644 src/components/ReplyContext.tsx diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index ff57abd1..7536ddb9 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -13,6 +13,7 @@ import { FoundLogContent } from '@/components/FoundLogContent'; import { ColorMomentContent } from '@/components/ColorMomentContent'; import { FollowPackContent } from '@/components/FollowPackContent'; import { ChestIcon } from '@/components/icons/ChestIcon'; +import { ReplyContext } from '@/components/ReplyContext'; import { useAuthor } from '@/hooks/useAuthor'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useEventStats } from '@/hooks/useTrending'; @@ -508,20 +509,4 @@ function TreasureHeader({ pubkey, variant }: { pubkey: string; variant: 'hid' | ); } -function ReplyContext({ pubkey }: { pubkey: string }) { - const author = useAuthor(pubkey); - const name = author.data?.metadata?.name || genUserName(pubkey); - return ( -
- Replying to - {author.isLoading ? ( - - ) : ( - e.stopPropagation()}> - @{name} - - )} -
- ); -} diff --git a/src/components/ReplyContext.tsx b/src/components/ReplyContext.tsx new file mode 100644 index 00000000..9a9c1450 --- /dev/null +++ b/src/components/ReplyContext.tsx @@ -0,0 +1,33 @@ +import { Link } from 'react-router-dom'; +import { nip19 } from 'nostr-tools'; + +import { Skeleton } from '@/components/ui/skeleton'; +import { useAuthor } from '@/hooks/useAuthor'; +import { genUserName } from '@/lib/genUserName'; + +interface ReplyContextProps { + pubkey: string; + className?: string; +} + +/** + * Displays "Replying to @username" context for reply posts. + * Used consistently across NoteCard and notification views. + */ +export function ReplyContext({ pubkey, className }: ReplyContextProps) { + const author = useAuthor(pubkey); + const name = author.data?.metadata?.name || genUserName(pubkey); + + return ( +
+ Replying to + {author.isLoading ? ( + + ) : ( + e.stopPropagation()}> + @{name} + + )} +
+ ); +} diff --git a/src/pages/NotificationsPage.tsx b/src/pages/NotificationsPage.tsx index 6eb0d63d..cd931fac 100644 --- a/src/pages/NotificationsPage.tsx +++ b/src/pages/NotificationsPage.tsx @@ -16,6 +16,7 @@ import { RepostMenu } from '@/components/RepostMenu'; import { NoteMoreMenu } from '@/components/NoteMoreMenu'; import { ReplyComposeModal } from '@/components/ReplyComposeModal'; import { ZapDialog } from '@/components/ZapDialog'; +import { ReplyContext } from '@/components/ReplyContext'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useEvent } from '@/hooks/useEvent'; @@ -321,6 +322,27 @@ function ReferencedPostCard({ event }: { event: NostrEvent }) { const [moreMenuOpen, setMoreMenuOpen] = useState(false); const [replyOpen, setReplyOpen] = useState(false); + // Check if this is a reply and find the person being replied to + const isReply = event.tags.some(([name]) => name === 'e'); + const replyTo = isReply ? (() => { + // First try to find a p tag that's not a mention + const directReply = event.tags.find(([name, , , marker]) => name === 'p' && marker !== 'mention'); + if (directReply) return directReply; + + // If no direct reply p tag, check if there's a root or reply e tag with a pubkey + const rootOrReply = event.tags.find(([name, , , marker]) => + name === 'e' && (marker === 'root' || marker === 'reply') + ); + if (rootOrReply?.[4]) { + // Return a p tag format with the pubkey from the e tag + return ['p', rootOrReply[4]]; + } + + // Fallback: if this is a reply but all p tags are mentions, use the first p tag anyway + const firstP = event.tags.find(([name]) => name === 'p'); + return firstP; + })() : undefined; + const handleNavigate = useCallback(() => { navigate(`/${encodedId}`); }, [navigate, encodedId]); @@ -330,6 +352,11 @@ function ReferencedPostCard({ event }: { event: NostrEvent }) { className="cursor-pointer" onClick={handleNavigate} > + {/* Reply context */} + {isReply && replyTo?.[1] && ( + + )} + {/* Author row */}
e.stopPropagation()}> @@ -584,27 +611,6 @@ function ActionButtons({ ); } -// ────────────────────────────────────── -// Reply Context: "Replying to @{name} and @{name}" -// ────────────────────────────────────── -function ReplyContext({ pubkey }: { pubkey: string }) { - const author = useAuthor(pubkey); - const name = author.data?.metadata?.name || genUserName(pubkey); - - return ( -
- Replying to - e.stopPropagation()} - > - @{name} - -
- ); -} - // ────────────────────────────────────── // Skeleton Loader // ──────────────────────────────────────