import { useState } from 'react'; import { nip19 } from 'nostr-tools'; import { Bookmark, ClipboardCopy, ExternalLink, AtSign, BellOff, VolumeX, Flag, Pin, FileJson, FileDigit, Trash2, } 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 { 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 ( ); } export function NoteMoreMenu({ event, open, onOpenChange }: NoteMoreMenuProps) { if (!open) return null; return ; } function NoteMoreMenuContent({ event, open, onOpenChange }: NoteMoreMenuProps) { 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 neventId = nip19.neventEncode({ id: event.id, author: event.pubkey }); const close = () => onOpenChange(false); const handleCopyLink = () => { const url = `${window.location.origin}/${neventId}`; navigator.clipboard.writeText(url); toast({ title: 'Link copied to clipboard' }); close(); }; const handleViewOnNjump = () => { window.open(`https://njump.me/${neventId}`, '_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(event.id); 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 = () => { // Mute the root event of the thread, or this event if it's the root 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 handleMention = () => { toast({ title: 'Mention is not yet implemented' }); 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 handleReport = () => { toast({ title: 'Report is not yet implemented' }); 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="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={handleMention} /> )}
{!isOwnPost && ( <>
} label={userMuted ? `Unmute @${displayName}` : `Mute @${displayName}`} onClick={handleMuteUser} /> } label={`Report @${displayName}`} onClick={handleReport} 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'}
); }