import { useState } from 'react'; import { nip19 } from 'nostr-tools'; import { useNavigate } from 'react-router-dom'; import { Bookmark, ClipboardCopy, ExternalLink, AtSign, BellOff, VolumeX, Flag, Pin, FileJson, FileDigit, Trash2, StickyNote, } from 'lucide-react'; import { Dialog, DialogContent, DialogTitle, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { Separator } from '@/components/ui/separator'; import { Button } from '@/components/ui/button'; import { NoteContent } from '@/components/NoteContent'; import { EmojifiedText } from '@/components/CustomEmoji'; import { ReplyComposeModal } from '@/components/ReplyComposeModal'; import { ReportDialog } from '@/components/ReportDialog'; import { useBookmarks } from '@/hooks/useBookmarks'; import { usePinnedNotes } from '@/hooks/usePinnedNotes'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useAuthor } from '@/hooks/useAuthor'; import { useMuteList } from '@/hooks/useMuteList'; import { useDeleteEvent } from '@/hooks/useDeleteEvent'; import { genUserName } from '@/lib/genUserName'; import { timeAgo } from '@/lib/timeAgo'; import { toast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; import type { NostrEvent } from '@nostrify/nostrify'; interface NoteMoreMenuProps { event: NostrEvent; open: boolean; onOpenChange: (open: boolean) => void; } interface MenuItemProps { icon: React.ReactNode; label: string; onClick: () => void; destructive?: boolean; } function MenuItem({ icon, label, onClick, destructive }: MenuItemProps) { return ( ); } /** Encode the NIP-19 identifier for an event — naddr for addressable events, nevent otherwise. */ function encodeEventNip19(event: NostrEvent): string { if (event.kind >= 30000 && event.kind < 40000) { const dTag = event.tags.find(([n]) => n === 'd')?.[1]; if (dTag) { return nip19.naddrEncode({ kind: event.kind, pubkey: event.pubkey, identifier: dTag }); } } return nip19.neventEncode({ id: event.id, author: event.pubkey }); } export function NoteMoreMenu({ event, open, onOpenChange }: NoteMoreMenuProps) { // These states live here (not in NoteMoreMenuContent) so they persist after the menu closes const [reportOpen, setReportOpen] = useState(false); const [mentionComposeOpen, setMentionComposeOpen] = useState(false); const mentionContent = `nostr:${nip19.npubEncode(event.pubkey)} `; return ( <> {open && ( { onOpenChange(false); setTimeout(() => setReportOpen(true), 150); }} onMention={() => { onOpenChange(false); setTimeout(() => setMentionComposeOpen(true), 150); }} /> )} ); } interface NoteMoreMenuContentProps extends NoteMoreMenuProps { onReport: () => void; onMention: () => void; } function NoteMoreMenuContent({ event, open, onOpenChange, onReport, onMention }: NoteMoreMenuContentProps) { const navigate = useNavigate(); const { user } = useCurrentUser(); const { isBookmarked, toggleBookmark } = useBookmarks(); const bookmarked = isBookmarked(event.id); const { isPinned, togglePin } = usePinnedNotes(user?.pubkey); const pinned = isPinned(event.id); const isOwnPost = user?.pubkey === event.pubkey; const author = useAuthor(event.pubkey); const metadata = author.data?.metadata; const displayName = metadata?.name || genUserName(event.pubkey); const { addMute, removeMute, isMuted } = useMuteList(); const userMuted = isMuted('pubkey', event.pubkey); const { mutate: deleteEvent, isPending: isDeleting } = useDeleteEvent(); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const nip19Id = encodeEventNip19(event); const close = () => onOpenChange(false); const handleViewPostDetails = () => { navigate(`/${nip19Id}`); close(); }; const handleCopyLink = () => { const url = `${window.location.origin}/${nip19Id}`; navigator.clipboard.writeText(url); toast({ title: 'Link copied to clipboard' }); close(); }; const handleViewOnNjump = () => { window.open(`https://njump.me/${nip19Id}`, '_blank', 'noopener,noreferrer'); close(); }; const handleBookmark = () => { toggleBookmark.mutate(event.id); close(); }; const handleTogglePin = () => { togglePin.mutate(event.id, { onSuccess: () => { toast({ title: pinned ? 'Unpinned from profile' : 'Pinned to profile' }); }, onError: () => { toast({ title: 'Failed to update pinned posts', variant: 'destructive' }); }, }); close(); }; const handleCopyEventId = () => { navigator.clipboard.writeText(nip19Id); toast({ title: 'Event ID copied to clipboard' }); close(); }; const handleCopyEventJson = () => { navigator.clipboard.writeText(JSON.stringify(event, null, 2)); toast({ title: 'Event JSON copied to clipboard' }); close(); }; const handleMuteConversation = () => { const rootTag = event.tags.find(([name, , , marker]) => name === 'e' && marker === 'root'); const threadId = rootTag?.[1] ?? event.id; addMute.mutate( { type: 'thread', value: threadId }, { onSuccess: () => { toast({ title: 'Conversation muted' }); }, onError: () => { toast({ title: 'Failed to mute conversation', variant: 'destructive' }); }, }, ); close(); }; const handleMuteUser = () => { const muteItem = { type: 'pubkey' as const, value: event.pubkey }; const mutation = userMuted ? removeMute : addMute; mutation.mutate(muteItem, { onSuccess: () => { toast({ title: userMuted ? `Unmuted @${displayName}` : `Muted @${displayName}` }); }, onError: () => { toast({ title: userMuted ? 'Failed to unmute user' : 'Failed to mute user', variant: 'destructive' }); }, }); close(); }; const handleDelete = () => { deleteEvent( { eventId: event.id, eventKind: event.kind }, { onSuccess: () => { toast({ title: 'Post deleted' }); close(); }, onError: () => { toast({ title: 'Failed to delete post', variant: 'destructive' }); }, }, ); }; return ( Post options {/* Post preview */}
{displayName[0].toUpperCase()}
{author.data?.event ? ( {displayName} ) : displayName} · {timeAgo(event.created_at)}
} label="View post details" onClick={handleViewPostDetails} /> } label="Copy Link to Post" onClick={handleCopyLink} /> } label="Copy Event ID" onClick={handleCopyEventId} /> } label="Copy Event JSON" onClick={handleCopyEventJson} /> } label="View post on njump.me" onClick={handleViewOnNjump} /> } label={bookmarked ? 'Remove Bookmark' : 'Bookmark'} onClick={handleBookmark} />
{!isOwnPost && ( } label="Mute Conversation" onClick={handleMuteConversation} /> )} {isOwnPost && ( } label={pinned ? 'Unpin from profile' : 'Pin on profile'} onClick={handleTogglePin} /> )} {isOwnPost && ( } label="Delete post" onClick={() => setDeleteConfirmOpen(true)} destructive /> )} {!isOwnPost && ( } label={`Mention @${displayName}`} onClick={onMention} /> )}
{!isOwnPost && ( <>
} label={userMuted ? `Unmute @${displayName}` : `Mute @${displayName}`} onClick={handleMuteUser} /> } label={`Report @${displayName}`} onClick={onReport} destructive />
)}
Delete post? This will request deletion from relays. Some relays may still keep a copy of the original event. This action cannot be undone. Cancel {isDeleting ? 'Deleting...' : 'Delete'}
); }