f6b209949a
The previous attempt replaced NoteContent with plain text, losing rich
rendering of hashtags, mentions, custom emoji, and nostr identifiers.
Reverting to NoteContent while keeping the overflow fix needed two
changes to make line-clamp-3 work:
- Render NoteContent as a <span> (as="span") so it participates in
the parent's -webkit-box line counting. The default <div> wrapper
with overflow-hidden created a separate block formatting context
that defeated line-clamp entirely.
- Add disableNoteEmbeds to prevent block-level EmbeddedNote/
EmbeddedNaddr cards from appearing inside the compact preview.
The outer container keeps overflow-wrap-anywhere so long URLs break
safely within the clamped area.
Regression-of: fc950865
641 lines
21 KiB
TypeScript
641 lines
21 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { nip19 } from 'nostr-tools';
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
import {
|
|
Bookmark,
|
|
BellOff,
|
|
VolumeX,
|
|
Flag,
|
|
Pin,
|
|
FileJson,
|
|
Trash2,
|
|
StickyNote,
|
|
ListPlus,
|
|
PanelLeft,
|
|
Copy,
|
|
Check,
|
|
Radio,
|
|
ShieldBan,
|
|
Ban,
|
|
} from 'lucide-react';
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogTitle,
|
|
DialogHeader,
|
|
} 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 { BanConfirmDialog } from '@/components/BanConfirmDialog';
|
|
import { NoteContent } from '@/components/NoteContent';
|
|
import { EmojifiedText } from '@/components/CustomEmoji';
|
|
import { ReportDialog } from '@/components/ReportDialog';
|
|
import { CommunityReportDialog } from '@/components/CommunityReportDialog';
|
|
import { AddToListDialog } from '@/components/AddToListDialog';
|
|
import { useNostr } from '@nostrify/react';
|
|
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 { useFeedSettings } from '@/hooks/useFeedSettings';
|
|
import { useOrganizers } from '@/hooks/useOrganizers';
|
|
import { usePinnedPosts } from '@/hooks/usePinnedPosts';
|
|
import { useCountryFeed } from '@/contexts/CountryFeedContext';
|
|
import { useCommunityModerationContext } from '@/contexts/CommunityModerationContext';
|
|
import { type CommunityMenuContext, canBanTarget, getViewerAuthority } from '@/lib/communityUtils';
|
|
// NOTE: `CommunityMenuContext` is derived automatically from
|
|
// `useCommunityModerationContext()`. Parents install a
|
|
// `CommunityModerationContext.Provider` to enable community-aware menu items.
|
|
import { isAdmin } from '@/lib/admins';
|
|
import { genUserName } from '@/lib/genUserName';
|
|
import { timeAgo } from '@/lib/timeAgo';
|
|
import { toast } from '@/hooks/useToast';
|
|
import { impactLight } from '@/lib/haptics';
|
|
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={(e) => {
|
|
e.stopPropagation();
|
|
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>
|
|
);
|
|
}
|
|
|
|
/** 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 });
|
|
}
|
|
|
|
interface EventJsonDialogProps {
|
|
event: NostrEvent;
|
|
nip19Id: string;
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
}
|
|
|
|
function CopyButton({ text, label }: { text: string; label: string }) {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const handleCopy = () => {
|
|
navigator.clipboard.writeText(text);
|
|
setCopied(true);
|
|
toast({ title: `${label} copied to clipboard` });
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleCopy}
|
|
className="size-6 shrink-0 text-muted-foreground hover:text-foreground"
|
|
>
|
|
{copied ? <Check className="size-3.5 text-green-500" /> : <Copy className="size-3.5" />}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
function EventJsonDialog({ event, nip19Id, open, onOpenChange }: EventJsonDialogProps) {
|
|
const { nostr } = useNostr();
|
|
const [broadcasting, setBroadcasting] = useState(false);
|
|
|
|
const jsonText = JSON.stringify(event, null, 2);
|
|
|
|
const handleBroadcast = async () => {
|
|
setBroadcasting(true);
|
|
try {
|
|
await nostr.event(event, { signal: AbortSignal.timeout(5000) });
|
|
toast({ title: 'Event broadcast to relays' });
|
|
} catch {
|
|
toast({ title: 'Failed to broadcast event', variant: 'destructive' });
|
|
} finally {
|
|
setBroadcasting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-2xl max-h-[85dvh] flex flex-col gap-0 p-0 rounded-2xl overflow-hidden">
|
|
<DialogHeader className="px-5 pt-5 pb-3 shrink-0">
|
|
<DialogTitle className="text-base font-semibold">Event Details</DialogTitle>
|
|
</DialogHeader>
|
|
|
|
<div className="px-5 pb-3 shrink-0">
|
|
<p className="text-xs font-medium text-muted-foreground mb-1">Event ID</p>
|
|
<div className="relative flex items-center bg-muted rounded-lg px-3 py-2">
|
|
<p className="font-mono text-xs break-all text-foreground/80 flex-1 pr-2 select-all">
|
|
{nip19Id}
|
|
</p>
|
|
<CopyButton text={nip19Id} label="Event ID" />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-5 pb-5 flex flex-col flex-1 min-h-0">
|
|
<p className="text-xs font-medium text-muted-foreground mb-1">Raw JSON</p>
|
|
<div className="relative flex-1 min-h-0 overflow-auto rounded-lg bg-muted border border-border">
|
|
<div className="sticky top-2 right-2 float-right mr-2">
|
|
<CopyButton text={jsonText} label="Event JSON" />
|
|
</div>
|
|
<pre className="p-4 text-xs font-mono text-foreground/80 whitespace-pre leading-relaxed">
|
|
{jsonText}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-5 pb-5 shrink-0">
|
|
<Button
|
|
variant="outline"
|
|
className="w-full gap-2"
|
|
onClick={handleBroadcast}
|
|
disabled={broadcasting}
|
|
>
|
|
<Radio className="size-4" />
|
|
{broadcasting ? 'Broadcasting...' : 'Broadcast Event'}
|
|
</Button>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
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 [banContentOpen, setBanContentOpen] = useState(false);
|
|
const [banMemberOpen, setBanMemberOpen] = useState(false);
|
|
const [addToListOpen, setAddToListOpen] = useState(false);
|
|
const [eventJsonOpen, setEventJsonOpen] = useState(false);
|
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
|
|
// Resolve community context from the React Context. Parents install a
|
|
// `CommunityModerationContext.Provider` (activity feed, community detail
|
|
// page, post detail page) to enable community-aware menu items.
|
|
const { user } = useCurrentUser();
|
|
const communityModCtx = useCommunityModerationContext();
|
|
const communityContext = useMemo<CommunityMenuContext | undefined>(() => {
|
|
if (!communityModCtx || !user) return undefined;
|
|
const viewerMember = getViewerAuthority(user.pubkey, communityModCtx.rankMap, communityModCtx.moderation);
|
|
if (!viewerMember) return undefined;
|
|
return {
|
|
communityATag: communityModCtx.communityATag,
|
|
canBan: canBanTarget(viewerMember, communityModCtx.rankMap.get(event.pubkey)),
|
|
};
|
|
}, [communityModCtx, user, event.pubkey]);
|
|
|
|
const { mutate: deleteEvent, isPending: isDeleting } = useDeleteEvent();
|
|
|
|
const nip19Id = encodeEventNip19(event);
|
|
|
|
const handleDelete = () => {
|
|
const dTag = event.tags.find(([name]) => name === 'd')?.[1];
|
|
deleteEvent(
|
|
{ eventId: event.id, eventKind: event.kind, eventPubkey: event.pubkey, eventDTag: dTag },
|
|
{
|
|
onSuccess: () => {
|
|
setDeleteConfirmOpen(false);
|
|
toast({ title: 'Post deleted' });
|
|
},
|
|
onError: () => {
|
|
toast({ title: 'Failed to delete post', variant: 'destructive' });
|
|
},
|
|
},
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{open && (
|
|
<NoteMoreMenuContent
|
|
event={event}
|
|
open={open}
|
|
onOpenChange={onOpenChange}
|
|
communityContext={communityContext}
|
|
onReport={() => {
|
|
onOpenChange(false);
|
|
setTimeout(() => setReportOpen(true), 150);
|
|
}}
|
|
onBanContent={() => {
|
|
onOpenChange(false);
|
|
setTimeout(() => setBanContentOpen(true), 150);
|
|
}}
|
|
onBanMember={() => {
|
|
onOpenChange(false);
|
|
setTimeout(() => setBanMemberOpen(true), 150);
|
|
}}
|
|
onAddToList={() => {
|
|
onOpenChange(false);
|
|
setTimeout(() => setAddToListOpen(true), 150);
|
|
}}
|
|
onViewEventJson={() => {
|
|
onOpenChange(false);
|
|
setTimeout(() => setEventJsonOpen(true), 150);
|
|
}}
|
|
onDelete={() => {
|
|
onOpenChange(false);
|
|
setTimeout(() => setDeleteConfirmOpen(true), 150);
|
|
}}
|
|
/>
|
|
)}
|
|
|
|
{communityContext ? (
|
|
<CommunityReportDialog
|
|
event={event}
|
|
communityATag={communityContext.communityATag}
|
|
open={reportOpen}
|
|
onOpenChange={setReportOpen}
|
|
/>
|
|
) : (
|
|
<ReportDialog event={event} open={reportOpen} onOpenChange={setReportOpen} />
|
|
)}
|
|
|
|
{communityContext?.canBan && (
|
|
<>
|
|
<BanConfirmDialog
|
|
mode="content"
|
|
eventId={event.id}
|
|
targetPubkey={event.pubkey}
|
|
communityATag={communityContext.communityATag}
|
|
open={banContentOpen}
|
|
onOpenChange={setBanContentOpen}
|
|
/>
|
|
<BanConfirmDialog
|
|
mode="member"
|
|
targetPubkey={event.pubkey}
|
|
communityATag={communityContext.communityATag}
|
|
open={banMemberOpen}
|
|
onOpenChange={setBanMemberOpen}
|
|
/>
|
|
</>
|
|
)}
|
|
|
|
<AddToListDialog
|
|
pubkey={event.pubkey}
|
|
open={addToListOpen}
|
|
onOpenChange={setAddToListOpen}
|
|
/>
|
|
|
|
<EventJsonDialog
|
|
event={event}
|
|
nip19Id={nip19Id}
|
|
open={eventJsonOpen}
|
|
onOpenChange={setEventJsonOpen}
|
|
/>
|
|
|
|
<AlertDialog open={deleteConfirmOpen} onOpenChange={setDeleteConfirmOpen}>
|
|
<AlertDialogContent onClick={(e) => e.stopPropagation()}>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete post?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This will request deletion from relays. Some relays may still keep a copy of the original event. This action cannot be undone.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={isDeleting}>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
handleDelete();
|
|
}}
|
|
disabled={isDeleting}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
{isDeleting ? 'Deleting...' : 'Delete'}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</>
|
|
);
|
|
}
|
|
|
|
interface NoteMoreMenuContentProps extends NoteMoreMenuProps {
|
|
/** Resolved community context (authored upstream from `CommunityModerationContext`). */
|
|
communityContext?: CommunityMenuContext;
|
|
onReport: () => void;
|
|
onBanContent: () => void;
|
|
onBanMember: () => void;
|
|
onAddToList: () => void;
|
|
onViewEventJson: () => void;
|
|
onDelete: () => void;
|
|
}
|
|
|
|
function NoteMoreMenuContent({ event, open, onOpenChange, communityContext, onReport, onBanContent, onBanMember, onAddToList, onViewEventJson, onDelete }: 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;
|
|
|
|
// Country-feed pin/unpin context (organizer/admin action). `useCountryFeed`
|
|
// returns null outside of a country page; we only enable usePinnedPosts when
|
|
// the viewer is actually authorized to pin so we avoid extra relay traffic
|
|
// for read-only viewers.
|
|
const countryFeed = useCountryFeed();
|
|
const countryCode = countryFeed?.countryCode;
|
|
const userIsAdmin = !!user && isAdmin(user.pubkey);
|
|
const { isOrganizer } = useOrganizers();
|
|
const userIsOrganizerHere =
|
|
!!user && !!countryCode && isOrganizer(user.pubkey, countryCode);
|
|
const canPinHere = userIsAdmin || userIsOrganizerHere;
|
|
const {
|
|
isPinned: isPinnedInCountry,
|
|
pinPost,
|
|
unpinPost,
|
|
} = usePinnedPosts(canPinHere ? countryCode : undefined);
|
|
const postIsPinnedInCountry = !!countryCode && isPinnedInCountry(event.id);
|
|
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 { addToSidebar, removeFromSidebar, orderedItems } = useFeedSettings();
|
|
|
|
const nip19Id = encodeEventNip19(event);
|
|
const nostrUri = `nostr:${nip19Id}`;
|
|
const isInSidebar = orderedItems.includes(nostrUri);
|
|
|
|
const close = () => onOpenChange(false);
|
|
|
|
const handleViewPostDetails = () => {
|
|
navigate(`/${nip19Id}`);
|
|
close();
|
|
};
|
|
|
|
const handleBookmark = () => {
|
|
impactLight();
|
|
toggleBookmark.mutate(event.id);
|
|
close();
|
|
};
|
|
|
|
const handleToggleSidebar = () => {
|
|
if (isInSidebar) {
|
|
removeFromSidebar(nostrUri);
|
|
toast({ title: 'Removed from sidebar' });
|
|
} else {
|
|
addToSidebar(nostrUri);
|
|
toast({ title: 'Added to sidebar' });
|
|
}
|
|
close();
|
|
};
|
|
|
|
const handleTogglePin = () => {
|
|
impactLight();
|
|
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 handleToggleCountryPin = () => {
|
|
if (!countryCode) return;
|
|
impactLight();
|
|
const mutation = postIsPinnedInCountry ? unpinPost : pinPost;
|
|
mutation.mutate(
|
|
{ eventId: event.id, countryCode },
|
|
{
|
|
onSuccess: () => {
|
|
toast({
|
|
title: postIsPinnedInCountry
|
|
? 'Unpinned from country feed'
|
|
: 'Pinned to country feed',
|
|
});
|
|
},
|
|
onError: () => {
|
|
toast({
|
|
title: postIsPinnedInCountry
|
|
? 'Failed to unpin from country feed'
|
|
: 'Failed to pin to country feed',
|
|
variant: 'destructive',
|
|
});
|
|
},
|
|
},
|
|
);
|
|
close();
|
|
};
|
|
|
|
const handleMuteConversation = () => {
|
|
impactLight();
|
|
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();
|
|
};
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-md max-h-[85dvh] p-0 gap-0 rounded-2xl overflow-y-auto [&>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">
|
|
{author.data?.event ? (
|
|
<EmojifiedText tags={author.data.event.tags}>{displayName}</EmojifiedText>
|
|
) : 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 overflow-wrap-anywhere">
|
|
{/^[A-Za-z0-9+/=_-]{20,}$/.test(event.content.trim()) ? (
|
|
<span className="italic">Encrypted content</span>
|
|
) : (
|
|
<NoteContent event={event} className="text-sm leading-snug whitespace-normal" disableEmbeds disableNoteEmbeds as="span" />
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="py-1">
|
|
<MenuItem
|
|
icon={<StickyNote className="size-5" />}
|
|
label="View post details"
|
|
onClick={handleViewPostDetails}
|
|
/>
|
|
<MenuItem
|
|
icon={<FileJson className="size-5" />}
|
|
label="View Event JSON"
|
|
onClick={onViewEventJson}
|
|
/>
|
|
<MenuItem
|
|
icon={<Bookmark className={cn("size-5", bookmarked && "fill-current")} />}
|
|
label={bookmarked ? 'Remove Bookmark' : 'Bookmark'}
|
|
onClick={handleBookmark}
|
|
/>
|
|
{user && (
|
|
<MenuItem
|
|
icon={<ListPlus className="size-5" />}
|
|
label="Add to list"
|
|
onClick={() => { onAddToList(); }}
|
|
/>
|
|
)}
|
|
<MenuItem
|
|
icon={isInSidebar ? <Trash2 className="size-5" /> : <PanelLeft className="size-5" />}
|
|
label={isInSidebar ? 'Remove from sidebar' : 'Add to sidebar'}
|
|
onClick={handleToggleSidebar}
|
|
/>
|
|
{isOwnPost && (
|
|
<MenuItem
|
|
icon={<Pin className={cn("size-5", pinned && "fill-current")} />}
|
|
label={pinned ? 'Unpin from profile' : 'Pin on profile'}
|
|
onClick={handleTogglePin}
|
|
/>
|
|
)}
|
|
{canPinHere && (
|
|
<MenuItem
|
|
icon={<Pin className={cn('size-5', postIsPinnedInCountry && 'fill-current')} />}
|
|
label={postIsPinnedInCountry ? 'Unpin from country feed' : 'Pin to country feed'}
|
|
onClick={handleToggleCountryPin}
|
|
/>
|
|
)}
|
|
{!isOwnPost && (
|
|
<MenuItem
|
|
icon={<BellOff className="size-5" />}
|
|
label="Mute Conversation"
|
|
onClick={handleMuteConversation}
|
|
/>
|
|
)}
|
|
{!isOwnPost && (
|
|
<MenuItem
|
|
icon={<VolumeX className="size-5" />}
|
|
label={userMuted ? `Unmute @${displayName}` : `Mute @${displayName}`}
|
|
onClick={handleMuteUser}
|
|
/>
|
|
)}
|
|
{!isOwnPost && (
|
|
<MenuItem
|
|
icon={<Flag className="size-5" />}
|
|
label={communityContext ? 'Report post to community' : `Report @${displayName}`}
|
|
onClick={onReport}
|
|
destructive
|
|
/>
|
|
)}
|
|
{!isOwnPost && communityContext?.canBan && (
|
|
<>
|
|
<MenuItem
|
|
icon={<ShieldBan className="size-5" />}
|
|
label="Remove from community"
|
|
onClick={onBanContent}
|
|
destructive
|
|
/>
|
|
<MenuItem
|
|
icon={<Ban className="size-5" />}
|
|
label={`Ban @${displayName} from community`}
|
|
onClick={onBanMember}
|
|
destructive
|
|
/>
|
|
</>
|
|
)}
|
|
{isOwnPost && (
|
|
<MenuItem
|
|
icon={<Trash2 className="size-5" />}
|
|
label="Delete post"
|
|
onClick={onDelete}
|
|
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>
|
|
);
|
|
}
|