Add sidebar toggle to external content pages and post 3-dots menu

ExternalContentPage (country, URL, ISBN pages) gains a 3-dots menu on
the right side of the action bar with Add/Remove from sidebar.

NoteMoreMenu gets an Add/Remove from sidebar item after 'Add to list',
so any post can be pinned to the sidebar from its options menu.

Both locations toggle between add (PanelLeft icon) and remove (Trash2
icon) based on whether the item is already in the sidebar.
This commit is contained in:
Alex Gleason
2026-03-23 00:02:00 -05:00
parent 3bbb6eae7e
commit 201dd60cb7
2 changed files with 67 additions and 1 deletions
+21
View File
@@ -14,6 +14,7 @@ import {
Trash2,
StickyNote,
ListPlus,
PanelLeft,
Copy,
Check,
} from 'lucide-react';
@@ -48,6 +49,7 @@ 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 { genUserName } from '@/lib/genUserName';
import { timeAgo } from '@/lib/timeAgo';
import { toast } from '@/hooks/useToast';
@@ -241,9 +243,12 @@ function NoteMoreMenuContent({ event, open, onOpenChange, onReport, onMention, o
const { addMute, removeMute, isMuted } = useMuteList();
const userMuted = isMuted('pubkey', event.pubkey);
const { mutate: deleteEvent, isPending: isDeleting } = useDeleteEvent();
const { addToSidebar, removeFromSidebar, orderedItems } = useFeedSettings();
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const nip19Id = encodeEventNip19(event);
const nostrUri = `nostr:${nip19Id}`;
const isInSidebar = orderedItems.includes(nostrUri);
const close = () => onOpenChange(false);
@@ -264,6 +269,17 @@ function NoteMoreMenuContent({ event, open, onOpenChange, onReport, onMention, o
close();
};
const handleToggleSidebar = () => {
if (isInSidebar) {
removeFromSidebar(nostrUri);
toast({ title: 'Removed from sidebar' });
} else {
addToSidebar(nostrUri);
toast({ title: 'Added to sidebar' });
}
close();
};
const handleTogglePin = () => {
togglePin.mutate(event.id, {
onSuccess: () => {
@@ -383,6 +399,11 @@ function NoteMoreMenuContent({ event, open, onOpenChange, onReport, onMention, o
onClick={() => { onAddToList(); }}
/>
)}
<MenuItem
icon={isInSidebar ? <Trash2 className="size-5" /> : <PanelLeft className="size-5" />}
label={isInSidebar ? 'Remove from sidebar' : 'Add to sidebar'}
onClick={handleToggleSidebar}
/>
</div>
<Separator />
+46 -1
View File
@@ -1,6 +1,6 @@
import { useCallback, useMemo, useRef, useState } from 'react';
import { useSeoMeta } from '@unhead/react';
import { ArrowLeft, Globe, Heart, MessageSquare, Repeat2, Star, AlertTriangle } from 'lucide-react';
import { ArrowLeft, Globe, Heart, MessageSquare, MoreHorizontal, Repeat2, Star, AlertTriangle, PanelLeft, Trash2 } from 'lucide-react';
import { Link, useLocation, useParams } from 'react-router-dom';
import { Skeleton } from '@/components/ui/skeleton';
import { Badge } from '@/components/ui/badge';
@@ -9,6 +9,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
import { getAvatarShape } from '@/lib/avatarShape';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { ThreadedReplyList } from '@/components/ThreadedReplyList';
import { ComposeBox } from '@/components/ComposeBox';
import { ReplyComposeModal } from '@/components/ReplyComposeModal';
@@ -36,6 +39,7 @@ import { useLinkPreview } from '@/hooks/useLinkPreview';
import { useLayoutOptions } from '@/contexts/LayoutContext';
import { useCurrentUser } from '@/hooks/useCurrentUser';
import { useNostrPublish } from '@/hooks/useNostrPublish';
import { useFeedSettings } from '@/hooks/useFeedSettings';
import { useQueryClient } from '@tanstack/react-query';
import { useToast } from '@/hooks/useToast';
import { getDisplayName } from '@/lib/getDisplayName';
@@ -73,6 +77,19 @@ function ExternalActionBar({ content }: { content: ExternalContent }) {
const queryClient = useQueryClient();
const { toast } = useToast();
const identifier = content.value;
const { addToSidebar, removeFromSidebar, orderedItems } = useFeedSettings();
const isInSidebar = orderedItems.includes(identifier);
const handleAddToSidebar = useCallback(() => {
addToSidebar(identifier);
toast({ title: 'Added to sidebar' });
}, [identifier, addToSidebar, toast]);
const handleRemoveFromSidebar = useCallback(() => {
removeFromSidebar(identifier);
toast({ title: 'Removed from sidebar' });
}, [identifier, removeFromSidebar, toast]);
const userReactionData = useExternalUserReaction(content);
const reactionCount = useExternalReactionCount(content);
@@ -224,6 +241,34 @@ function ExternalActionBar({ content }: { content: ExternalContent }) {
</BookReviewFormDialog>
)}
{/* Spacer pushes the 3-dots menu to the right */}
<div className="flex-1" />
{/* 3-dots menu with sidebar action */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
className="p-2 rounded-full transition-colors text-muted-foreground hover:text-primary hover:bg-primary/10"
title="More"
>
<MoreHorizontal className="size-5" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
{isInSidebar ? (
<DropdownMenuItem onClick={handleRemoveFromSidebar} className="gap-3">
<Trash2 className="size-4" />
Remove from sidebar
</DropdownMenuItem>
) : (
<DropdownMenuItem onClick={handleAddToSidebar} className="gap-3">
<PanelLeft className="size-4" />
Add to sidebar
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
{shareOpen && (
<ReplyComposeModal
open={shareOpen}