diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 9a04888..4b0c7b2 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,4 +1,4 @@ -import { useState, useMemo, useCallback } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CONTACT_TYPE_REPEATER, type Contact, @@ -16,6 +16,25 @@ import { cn } from '@/lib/utils'; type SortOrder = 'alpha' | 'recent'; +type FavoriteItem = { type: 'channel'; channel: Channel } | { type: 'contact'; contact: Contact }; + +type ConversationRow = { + key: string; + type: 'channel' | 'contact'; + id: string; + name: string; + unreadCount: number; + isMention: boolean; + contact?: Contact; +}; + +type CollapseState = { + favorites: boolean; + channels: boolean; + contacts: boolean; + repeaters: boolean; +}; + interface SidebarProps { contacts: Contact[]; channels: Channel[]; @@ -60,6 +79,7 @@ export function Sidebar({ const [channelsCollapsed, setChannelsCollapsed] = useState(false); const [contactsCollapsed, setContactsCollapsed] = useState(false); const [repeatersCollapsed, setRepeatersCollapsed] = useState(false); + const collapseSnapshotRef = useRef(null); const handleSortToggle = () => { const newOrder = sortOrder === 'alpha' ? 'recent' : 'alpha'; @@ -175,6 +195,8 @@ export function Sidebar({ // Filter by search query const query = searchQuery.toLowerCase().trim(); + const isSearching = query.length > 0; + const filteredChannels = useMemo( () => query @@ -207,9 +229,44 @@ export function Sidebar({ [sortedRepeaters, query] ); - // Separate favorites from regular items, and build combined favorites list - type FavoriteItem = { type: 'channel'; channel: Channel } | { type: 'contact'; contact: Contact }; + // Expand sections while searching; restore prior collapse state when search ends. + useEffect(() => { + if (isSearching) { + if (!collapseSnapshotRef.current) { + collapseSnapshotRef.current = { + favorites: favoritesCollapsed, + channels: channelsCollapsed, + contacts: contactsCollapsed, + repeaters: repeatersCollapsed, + }; + } + if (favoritesCollapsed || channelsCollapsed || contactsCollapsed || repeatersCollapsed) { + setFavoritesCollapsed(false); + setChannelsCollapsed(false); + setContactsCollapsed(false); + setRepeatersCollapsed(false); + } + return; + } + + if (collapseSnapshotRef.current) { + const prev = collapseSnapshotRef.current; + collapseSnapshotRef.current = null; + setFavoritesCollapsed(prev.favorites); + setChannelsCollapsed(prev.channels); + setContactsCollapsed(prev.contacts); + setRepeatersCollapsed(prev.repeaters); + } + }, [ + isSearching, + favoritesCollapsed, + channelsCollapsed, + contactsCollapsed, + repeatersCollapsed, + ]); + + // Separate favorites from regular items, and build combined favorites list const { favoriteItems, nonFavoriteChannels, nonFavoriteContacts, nonFavoriteRepeaters } = useMemo(() => { const favChannels = filteredChannels.filter((c) => isFavorite(favorites, 'channel', c.key)); const favContacts = [...filteredNonRepeaterContacts, ...filteredRepeaters].filter((c) => @@ -257,32 +314,100 @@ export function Sidebar({ getLastMessageTime, ]); + const buildChannelRow = (channel: Channel, keyPrefix: string): ConversationRow => ({ + key: `${keyPrefix}-${channel.key}`, + type: 'channel', + id: channel.key, + name: channel.name, + unreadCount: getUnreadCount('channel', channel.key), + isMention: hasMention('channel', channel.key), + }); + + const buildContactRow = (contact: Contact, keyPrefix: string): ConversationRow => ({ + key: `${keyPrefix}-${contact.public_key}`, + type: 'contact', + id: contact.public_key, + name: getContactDisplayName(contact.name, contact.public_key), + unreadCount: getUnreadCount('contact', contact.public_key), + isMention: hasMention('contact', contact.public_key), + contact, + }); + + const renderConversationRow = (row: ConversationRow) => ( +
0 && '[&_.name]:font-bold [&_.name]:text-foreground' + )} + onClick={() => + handleSelectConversation({ + type: row.type, + id: row.id, + name: row.name, + }) + } + > + {row.type === 'contact' && row.contact && ( + + )} + {row.name} + {row.unreadCount > 0 && ( + + {row.unreadCount} + + )} +
+ ); + const renderSectionHeader = ( title: string, collapsed: boolean, onToggle: () => void, showSortToggle = false - ) => ( -
- - {showSortToggle && ( + ) => { + const effectiveCollapsed = isSearching ? false : collapsed; + + return ( +
- )} -
- ); + {showSortToggle && ( + + )} +
+ ); + }; return (
@@ -426,88 +551,12 @@ export function Sidebar({ () => setFavoritesCollapsed((prev) => !prev), false )} - {!favoritesCollapsed && - favoriteItems.map((item) => { - if (item.type === 'channel') { - const channel = item.channel; - const unreadCount = getUnreadCount('channel', channel.key); - const isMention = hasMention('channel', channel.key); - return ( -
0 && '[&_.name]:font-bold [&_.name]:text-foreground' - )} - onClick={() => - handleSelectConversation({ - type: 'channel', - id: channel.key, - name: channel.name, - }) - } - > - {channel.name} - {unreadCount > 0 && ( - - {unreadCount} - - )} -
- ); - } else { - const contact = item.contact; - const unreadCount = getUnreadCount('contact', contact.public_key); - const isMention = hasMention('contact', contact.public_key); - return ( -
0 && '[&_.name]:font-bold [&_.name]:text-foreground' - )} - onClick={() => - handleSelectConversation({ - type: 'contact', - id: contact.public_key, - name: getContactDisplayName(contact.name, contact.public_key), - }) - } - > - - - {getContactDisplayName(contact.name, contact.public_key)} - - {unreadCount > 0 && ( - - {unreadCount} - - )} -
- ); - } - })} + {(isSearching || !favoritesCollapsed) && + favoriteItems.map((item) => + item.type === 'channel' + ? renderConversationRow(buildChannelRow(item.channel, 'fav-chan')) + : renderConversationRow(buildContactRow(item.contact, 'fav-contact')) + )} )} @@ -520,42 +569,10 @@ export function Sidebar({ () => setChannelsCollapsed((prev) => !prev), true )} - {!channelsCollapsed && - nonFavoriteChannels.map((channel) => { - const unreadCount = getUnreadCount('channel', channel.key); - const isMention = hasMention('channel', channel.key); - return ( -
0 && '[&_.name]:font-bold [&_.name]:text-foreground' - )} - onClick={() => - handleSelectConversation({ - type: 'channel', - id: channel.key, - name: channel.name, - }) - } - > - {channel.name} - {unreadCount > 0 && ( - - {unreadCount} - - )} -
- ); - })} + {(isSearching || !channelsCollapsed) && + nonFavoriteChannels.map((channel) => + renderConversationRow(buildChannelRow(channel, 'chan')) + )} )} @@ -568,50 +585,10 @@ export function Sidebar({ () => setContactsCollapsed((prev) => !prev), true )} - {!contactsCollapsed && - nonFavoriteContacts.map((contact) => { - const unreadCount = getUnreadCount('contact', contact.public_key); - const isMention = hasMention('contact', contact.public_key); - return ( -
0 && '[&_.name]:font-bold [&_.name]:text-foreground' - )} - onClick={() => - handleSelectConversation({ - type: 'contact', - id: contact.public_key, - name: getContactDisplayName(contact.name, contact.public_key), - }) - } - > - - - {getContactDisplayName(contact.name, contact.public_key)} - - {unreadCount > 0 && ( - - {unreadCount} - - )} -
- ); - })} + {(isSearching || !contactsCollapsed) && + nonFavoriteContacts.map((contact) => + renderConversationRow(buildContactRow(contact, 'contact')) + )} )} @@ -624,50 +601,10 @@ export function Sidebar({ () => setRepeatersCollapsed((prev) => !prev), true )} - {!repeatersCollapsed && - nonFavoriteRepeaters.map((contact) => { - const unreadCount = getUnreadCount('contact', contact.public_key); - const isMention = hasMention('contact', contact.public_key); - return ( -
0 && '[&_.name]:font-bold [&_.name]:text-foreground' - )} - onClick={() => - handleSelectConversation({ - type: 'contact', - id: contact.public_key, - name: getContactDisplayName(contact.name, contact.public_key), - }) - } - > - - - {getContactDisplayName(contact.name, contact.public_key)} - - {unreadCount > 0 && ( - - {unreadCount} - - )} -
- ); - })} + {(isSearching || !repeatersCollapsed) && + nonFavoriteRepeaters.map((contact) => + renderConversationRow(buildContactRow(contact, 'repeater')) + )} )}