import { useState } from 'react' import { useResizableWidth } from '../hooks/useResizableWidth' import type { MyRoomItem } from '../types' import { RoomRow } from './RoomRow' import './Sidebar.css' // #62: which of the two sections (keyed 'dm'/'rooms') are collapsed -- // persisted the same way sidebar-width already is (see useResizableWidth), // a per-viewer cosmetic preference with no reason to live server-side. const COLLAPSED_SECTIONS_KEY = 'sidebar-collapsed-sections' function loadCollapsedSections(): Set { try { const raw = localStorage.getItem(COLLAPSED_SECTIONS_KEY) if (!raw) return new Set() const parsed = JSON.parse(raw) return Array.isArray(parsed) ? new Set(parsed.filter((s) => typeof s === 'string')) : new Set() } catch { return new Set() } } function saveCollapsedSections(sections: Set): void { try { localStorage.setItem(COLLAPSED_SECTIONS_KEY, JSON.stringify([...sections])) } catch { // storage unavailable (private browsing, quota) -- collapse state just // won't persist this session, not fatal. } } interface SidebarSectionHeaderProps { label: string collapsed: boolean onToggle: () => void } function SidebarSectionHeader({ label, collapsed, onToggle }: SidebarSectionHeaderProps) { return ( ) } interface SidebarProps { rooms: MyRoomItem[] activeRoomId: string | undefined searchQuery: string onSearchChange: (value: string) => void onOpenNewRoom: () => void onOpenBrowse: () => void onOpenPeople: () => void unavailableOffline?: boolean } export function Sidebar({ rooms, activeRoomId, searchQuery, onSearchChange, onOpenNewRoom, onOpenBrowse, onOpenPeople, unavailableOffline, }: SidebarProps) { const [collapsedSections, setCollapsedSections] = useState>(loadCollapsedSections) function toggleSection(key: string) { setCollapsedSections((prev) => { const next = new Set(prev) if (next.has(key)) next.delete(key) else next.add(key) saveCollapsedSections(next) return next }) } const query = searchQuery.trim().toLowerCase() // A DM's `name` is an internal token, never what a user would search for // -- matched against the partner's display name/username instead. function matchesQuery(room: MyRoomItem): boolean { if (!query) return true if (room.is_dm && room.dm_partner) { return ( (room.dm_partner.display_name ?? '').toLowerCase().includes(query) || room.dm_partner.username.toLowerCase().includes(query) ) } return room.name.toLowerCase().includes(query) } // #57: archived rooms keep flowing through in `rooms` (so a member who // still has one open via a direct link resolves fine -- see ChatPane), // but they're a dead end going forward, so they don't belong in the list // you'd browse/search from. const filtered = rooms.filter((r) => !r.is_archived).filter(matchesQuery) const directMessages = filtered.filter((r) => r.is_dm) const regularRooms = filtered.filter((r) => !r.is_dm) const { width, startResize } = useResizableWidth({ storageKey: 'sidebar-width', defaultWidth: 300, min: 220, max: 480, anchor: 'left', }) return ( ) }