Files
eranos/src/components/NoteMoreMenu.tsx
T
shakespeare.diy 858ec3beaa Add post preview to More menu, blur modal backdrops, polish compose modal
- NoteMoreMenu now shows a compact post preview at the top (avatar,
  author name, timestamp, and 3-line-clamped content) before menu items
- Dialog overlay updated globally: bg-black/60 + backdrop-blur-sm for a
  frosted-glass effect behind all modals
- FloatingComposeButton modal reworked to match the same presentation
  pattern: rounded-2xl, hidden default close button replaced with a
  centered header (X / "New post" title), separator, compose area

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-02-16 17:50:46 -06:00

209 lines
6.0 KiB
TypeScript

import { nip19 } from 'nostr-tools';
import {
ArrowUpDown,
Bookmark,
ClipboardCopy,
ExternalLink,
AtSign,
BellOff,
VolumeX,
Flag,
} from 'lucide-react';
import {
Dialog,
DialogContent,
DialogTitle,
} from '@/components/ui/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 { useBookmarks } from '@/hooks/useBookmarks';
import { useAuthor } from '@/hooks/useAuthor';
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 (
<button
onClick={onClick}
className={cn(
'flex items-center gap-4 w-full px-5 py-3 text-[15px] transition-colors hover:bg-secondary/60',
destructive ? 'text-destructive' : 'text-muted-foreground',
)}
>
<span className="shrink-0">{icon}</span>
<span>{label}</span>
</button>
);
}
export function NoteMoreMenu({ event, open, onOpenChange }: NoteMoreMenuProps) {
const { isBookmarked, toggleBookmark } = useBookmarks();
const bookmarked = isBookmarked(event.id);
const author = useAuthor(event.pubkey);
const metadata = author.data?.metadata;
const displayName = metadata?.name || genUserName(event.pubkey);
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 handleShowDetails = () => {
const url = `/${neventId}`;
window.location.href = url;
close();
};
const handleMuteConversation = () => {
toast({ title: 'Mute conversation is not yet implemented' });
close();
};
const handleMention = () => {
toast({ title: 'Mention is not yet implemented' });
close();
};
const handleMuteUser = () => {
toast({ title: 'Mute user is not yet implemented' });
close();
};
const handleReport = () => {
toast({ title: 'Report is not yet implemented' });
close();
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md p-0 gap-0 rounded-2xl overflow-hidden [&>button]:hidden">
<DialogTitle className="sr-only">Post options</DialogTitle>
{/* Post preview */}
<div className="px-4 pt-4 pb-3">
<div className="flex gap-3">
<Avatar className="size-10 shrink-0">
<AvatarImage src={metadata?.picture} alt={displayName} />
<AvatarFallback className="bg-primary/20 text-primary text-sm">
{displayName[0].toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 text-sm">
<span className="font-bold truncate">{displayName}</span>
<span className="text-muted-foreground shrink-0">·</span>
<span className="text-muted-foreground shrink-0 text-xs">{timeAgo(event.created_at)}</span>
</div>
<div className="mt-0.5 text-sm text-muted-foreground line-clamp-3">
<NoteContent event={event} className="text-sm leading-relaxed" />
</div>
</div>
</div>
</div>
<Separator />
<div className="py-1">
<MenuItem
icon={<ArrowUpDown className="size-5" />}
label="Show Post Details"
onClick={handleShowDetails}
/>
<MenuItem
icon={<ClipboardCopy className="size-5" />}
label="Copy Link to Post"
onClick={handleCopyLink}
/>
<MenuItem
icon={<ExternalLink className="size-5" />}
label="View post on njump.me"
onClick={handleViewOnNjump}
/>
<MenuItem
icon={<Bookmark className={cn("size-5", bookmarked && "fill-current")} />}
label={bookmarked ? 'Remove Bookmark' : 'Bookmark'}
onClick={handleBookmark}
/>
</div>
<Separator />
<div className="py-1">
<MenuItem
icon={<BellOff className="size-5" />}
label="Mute Conversation"
onClick={handleMuteConversation}
/>
<MenuItem
icon={<AtSign className="size-5" />}
label={`Mention @${displayName}`}
onClick={handleMention}
/>
</div>
<Separator />
<div className="py-1">
<MenuItem
icon={<VolumeX className="size-5" />}
label={`Mute @${displayName}`}
onClick={handleMuteUser}
/>
<MenuItem
icon={<Flag className="size-5" />}
label={`Report @${displayName}`}
onClick={handleReport}
destructive
/>
</div>
<Separator />
<div className="py-1">
<Button
variant="ghost"
className="w-full h-auto py-3 text-[15px] font-medium text-muted-foreground hover:bg-secondary/60 rounded-none"
onClick={close}
>
Close
</Button>
</div>
</DialogContent>
</Dialog>
);
}