From 2558743dae1237000c4d5fa8312eee8ece45edd5 Mon Sep 17 00:00:00 2001 From: Lemon Date: Sat, 11 Apr 2026 22:57:24 -0700 Subject: [PATCH] Deduplicate buildKindOptions, AVAILABLE_FONTS, and sidebar editing logic into shared modules --- src/components/LeftSidebar.tsx | 28 +++---------- src/components/MobileDrawer.tsx | 28 +++---------- src/components/SavedFeedFiltersEditor.tsx | 48 ++-------------------- src/components/SpellContent.tsx | 2 +- src/hooks/useSidebarEditing.ts | 49 +++++++++++++++++++++++ src/lib/aiChatSystemPrompt.ts | 4 +- src/lib/feedFilterUtils.ts | 2 +- 7 files changed, 68 insertions(+), 93 deletions(-) create mode 100644 src/hooks/useSidebarEditing.ts diff --git a/src/components/LeftSidebar.tsx b/src/components/LeftSidebar.tsx index b7c33356..27f2fa37 100644 --- a/src/components/LeftSidebar.tsx +++ b/src/components/LeftSidebar.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback } from 'react'; import { Link, useLocation, useNavigate } from 'react-router-dom'; import { @@ -12,7 +12,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { DittoLogo } from '@/components/DittoLogo'; import { EmojifiedText } from '@/components/CustomEmoji'; import { ProfileSearchDropdown } from '@/components/ProfileSearchDropdown'; -import { SidebarNavList, MORE_SEPARATOR_ID } from '@/components/SidebarNavItem'; +import { SidebarNavList } from '@/components/SidebarNavItem'; import { SidebarMoreMenu } from '@/components/SidebarMoreMenu'; import LoginDialog from '@/components/auth/LoginDialog'; @@ -23,6 +23,7 @@ import { useLoggedInAccounts, type Account } from '@/hooks/useLoggedInAccounts'; import { useLoginActions } from '@/hooks/useLoginActions'; import { useFeedSettings } from '@/hooks/useFeedSettings'; +import { useSidebarEditing } from '@/hooks/useSidebarEditing'; import { useAppContext } from '@/hooks/useAppContext'; import { useHasUnreadNotifications } from '@/hooks/useHasUnreadNotifications'; import { genUserName } from '@/lib/genUserName'; @@ -68,26 +69,9 @@ export function LeftSidebar() { const homePage = config.homePage; - // In editing mode, build a combined list: visible + __more__ + hidden - const editingItems = useMemo(() => { - if (!editing) return []; - return [...orderedItems, MORE_SEPARATOR_ID, ...hiddenItems.map((h) => h.id)]; - }, [editing, orderedItems, hiddenItems]); - - const handleEditReorder = useCallback((newOrder: string[]) => { - const moreIdx = newOrder.indexOf(MORE_SEPARATOR_ID); - if (moreIdx === -1) return; - const newVisible = newOrder.slice(0, moreIdx); - updateSidebarOrder(newVisible); - }, [updateSidebarOrder]); - - const handleEditRemove = useCallback((id: string, index?: number) => { - if (id === 'divider' && index !== undefined) { - removeFromSidebar(id, index); - } else { - removeFromSidebar(id); - } - }, [removeFromSidebar]); + const { editingItems, handleEditReorder, handleEditRemove } = useSidebarEditing({ + editing, items: orderedItems, hiddenItems, updateSidebarOrder, removeFromSidebar, + }); const scrollToTopIfCurrent = useCallback((to: string) => (e: React.MouseEvent) => { if (location.pathname === to) { diff --git a/src/components/MobileDrawer.tsx b/src/components/MobileDrawer.tsx index a4b1a9bb..0785991e 100644 --- a/src/components/MobileDrawer.tsx +++ b/src/components/MobileDrawer.tsx @@ -1,10 +1,11 @@ -import { useState, useId, useMemo, useCallback } from 'react'; +import { useState, useId, useMemo } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { ChevronDown, ChevronUp, LogOut, UserPlus, Loader2, QrCode } from 'lucide-react'; import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'; import { getAvatarShape } from '@/lib/avatarShape'; import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet'; -import { SidebarNavList, MORE_SEPARATOR_ID } from '@/components/SidebarNavItem'; +import { SidebarNavList } from '@/components/SidebarNavItem'; +import { useSidebarEditing } from '@/hooks/useSidebarEditing'; import { SidebarMoreMenu } from '@/components/SidebarMoreMenu'; import { LoginArea } from '@/components/auth/LoginArea'; @@ -98,26 +99,9 @@ export function MobileDrawer({ open, onOpenChange }: MobileDrawerProps) { - // In editing mode, build a combined list: visible + __more__ + hidden - const editingItems = useMemo(() => { - if (!editing) return []; - return [...visibleItems, MORE_SEPARATOR_ID, ...hiddenItems.map((h) => h.id)]; - }, [editing, visibleItems, hiddenItems]); - - const handleEditReorder = useCallback((newOrder: string[]) => { - const moreIdx = newOrder.indexOf(MORE_SEPARATOR_ID); - if (moreIdx === -1) return; - const newVisible = newOrder.slice(0, moreIdx); - updateSidebarOrder(newVisible); - }, [updateSidebarOrder]); - - const handleEditRemove = useCallback((id: string, index?: number) => { - if (id === 'divider' && index !== undefined) { - removeFromSidebar(id, index); - } else { - removeFromSidebar(id); - } - }, [removeFromSidebar]); + const { editingItems, handleEditReorder, handleEditRemove } = useSidebarEditing({ + editing, items: visibleItems, hiddenItems, updateSidebarOrder, removeFromSidebar, + }); const handleClose = () => { onOpenChange(false); }; const handleLogout = async () => { await logout(); handleClose(); navigate('/'); }; diff --git a/src/components/SavedFeedFiltersEditor.tsx b/src/components/SavedFeedFiltersEditor.tsx index ccb2a27a..f29c58f1 100644 --- a/src/components/SavedFeedFiltersEditor.tsx +++ b/src/components/SavedFeedFiltersEditor.tsx @@ -14,56 +14,16 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrig import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { ProfileSearchDropdown } from '@/components/ProfileSearchDropdown'; import { useAuthor } from '@/hooks/useAuthor'; -import { EXTRA_KINDS } from '@/lib/extraKinds'; -import { CONTENT_KIND_ICONS } from '@/lib/sidebarItems'; import { cn } from '@/lib/utils'; import type { SearchProfile } from '@/hooks/useSearchProfiles'; import type { UserList } from '@/hooks/useUserLists'; import type { FollowPack } from '@/hooks/useFollowPacks'; +import type { KindOption } from '@/lib/feedFilterUtils'; -// ─── Types ─────────────────────────────────────────────────────────────────── - -type KindOption = { - value: string; - label: string; - description: string; - parentId: string; - icon: React.ComponentType<{ className?: string }> | undefined; -}; - -// ─── Kind options (built once) ─────────────────────────────────────────────── - -export function buildKindOptions(): KindOption[] { - const options: KindOption[] = []; - for (const def of EXTRA_KINDS) { - if (def.subKinds) { - for (const sub of def.subKinds) { - options.push({ - value: String(sub.kind), - label: `${sub.label} (${sub.kind})`, - description: sub.description, - parentId: def.id, - icon: CONTENT_KIND_ICONS[def.id], - }); - } - } else { - options.push({ - value: String(def.kind), - label: `${def.label} (${def.kind})`, - description: def.description, - parentId: def.id, - icon: CONTENT_KIND_ICONS[def.id], - }); - } - } - const seen = new Set(); - return options.filter((o) => { - if (seen.has(o.value)) return false; - seen.add(o.value); - return true; - }); -} +// Re-export for consumers that were importing from this file +export { buildKindOptions } from '@/lib/feedFilterUtils'; +export type { KindOption } from '@/lib/feedFilterUtils'; // ─── useScrollCarets ───────────────────────────────────────────────────────── diff --git a/src/components/SpellContent.tsx b/src/components/SpellContent.tsx index cffa2dae..ad08afde 100644 --- a/src/components/SpellContent.tsx +++ b/src/components/SpellContent.tsx @@ -1,7 +1,7 @@ import type { NostrEvent } from '@nostrify/nostrify'; import { Badge } from '@/components/ui/badge'; import { Clock, Globe, Image, Languages, MessageSquareOff, Radio, Search, SortDesc, Terminal, Users, Video, WandSparkles } from 'lucide-react'; -import { buildKindOptions } from '@/components/SavedFeedFiltersEditor'; +import { buildKindOptions } from '@/lib/feedFilterUtils'; /** Map from kind number string to friendly label like "Posts (1)". */ const KIND_LABEL_MAP: Map = new Map( diff --git a/src/hooks/useSidebarEditing.ts b/src/hooks/useSidebarEditing.ts new file mode 100644 index 00000000..91ec4518 --- /dev/null +++ b/src/hooks/useSidebarEditing.ts @@ -0,0 +1,49 @@ +import { useMemo, useCallback } from 'react'; +import { MORE_SEPARATOR_ID } from '@/components/SidebarNavItem'; +import type { HiddenSidebarItem } from '@/hooks/useFeedSettings'; + +/** + * Shared sidebar editing logic used by both LeftSidebar and MobileDrawer. + * + * Builds the combined editing list (visible + __more__ separator + hidden) + * and provides handlers for reordering and removing items. + */ +export function useSidebarEditing({ + editing, + items, + hiddenItems, + updateSidebarOrder, + removeFromSidebar, +}: { + editing: boolean; + /** Visible sidebar item IDs (may be pre-filtered, e.g. MobileDrawer strips leading dividers). */ + items: string[]; + hiddenItems: HiddenSidebarItem[]; + updateSidebarOrder: (newOrder: string[]) => void; + removeFromSidebar: (id: string, index?: number) => void; +}) { + /** Combined list for the drag-and-drop editor: visible items + separator + hidden items. */ + const editingItems = useMemo(() => { + if (!editing) return []; + return [...items, MORE_SEPARATOR_ID, ...hiddenItems.map((h) => h.id)]; + }, [editing, items, hiddenItems]); + + /** Handle drag-and-drop reorder — extract items above the __more__ separator. */ + const handleEditReorder = useCallback((newOrder: string[]) => { + const moreIdx = newOrder.indexOf(MORE_SEPARATOR_ID); + if (moreIdx === -1) return; + const newVisible = newOrder.slice(0, moreIdx); + updateSidebarOrder(newVisible); + }, [updateSidebarOrder]); + + /** Remove a sidebar item; dividers require an index to identify which one. */ + const handleEditRemove = useCallback((id: string, index?: number) => { + if (id === 'divider' && index !== undefined) { + removeFromSidebar(id, index); + } else { + removeFromSidebar(id); + } + }, [removeFromSidebar]); + + return { editingItems, handleEditReorder, handleEditRemove }; +} diff --git a/src/lib/aiChatSystemPrompt.ts b/src/lib/aiChatSystemPrompt.ts index 2793be24..309a6411 100644 --- a/src/lib/aiChatSystemPrompt.ts +++ b/src/lib/aiChatSystemPrompt.ts @@ -1,8 +1,6 @@ -import { bundledFonts } from '@/lib/fonts'; +import { AVAILABLE_FONTS } from '@/lib/aiChatTools'; import type { ChatMessage } from '@/hooks/useShakespeare'; -const AVAILABLE_FONTS = bundledFonts.map((f) => f.family).join(', '); - /** Minimal profile fields injected into the system prompt so the AI knows who it's talking to. */ export interface UserIdentity { /** The user's npub (bech32 public key). */ diff --git a/src/lib/feedFilterUtils.ts b/src/lib/feedFilterUtils.ts index e1d569e4..59d1107b 100644 --- a/src/lib/feedFilterUtils.ts +++ b/src/lib/feedFilterUtils.ts @@ -1,7 +1,7 @@ import { EXTRA_KINDS } from '@/lib/extraKinds'; import { CONTENT_KIND_ICONS } from '@/lib/sidebarItems'; -type KindOption = { +export type KindOption = { value: string; label: string; description: string;