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 <assistant@shakespeare.diy>
This commit is contained in:
shakespeare.diy
2026-02-18 20:35:25 -06:00
parent 69bca8eb58
commit 6c16a48b3c
3 changed files with 61 additions and 37 deletions
+1 -16
View File
@@ -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 (
<div className="flex items-center text-sm text-muted-foreground mt-2 mb-1">
<span className="mr-1">Replying to</span>
{author.isLoading ? (
<Skeleton className="h-3.5 w-20 inline-block" />
) : (
<Link to={`/${nip19.npubEncode(pubkey)}`} className="text-primary hover:underline" onClick={(e) => e.stopPropagation()}>
@{name}
</Link>
)}
</div>
);
}
+33
View File
@@ -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 (
<div className={className || 'flex items-center text-sm text-muted-foreground mt-2 mb-1'}>
<span className="mr-1">Replying to</span>
{author.isLoading ? (
<Skeleton className="h-3.5 w-20 inline-block" />
) : (
<Link to={`/${nip19.npubEncode(pubkey)}`} className="text-primary hover:underline" onClick={(e) => e.stopPropagation()}>
@{name}
</Link>
)}
</div>
);
}
+27 -21
View File
@@ -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] && (
<ReplyContext pubkey={replyTo[1]} />
)}
{/* Author row */}
<div className="flex items-center gap-3">
<Link to={`/${npub}`} className="shrink-0" onClick={(e) => 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 (
<div className="flex items-center gap-1.5 text-sm text-muted-foreground mb-1">
<span>Replying to</span>
<Link
to={`/${nip19.npubEncode(pubkey)}`}
className="text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
@{name}
</Link>
</div>
);
}
// ──────────────────────────────────────
// Skeleton Loader
// ──────────────────────────────────────