diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index a76ce69..10baaa8 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -258,61 +258,60 @@ export function Sidebar({ setContactsCollapsed(prev.contacts); setRepeatersCollapsed(prev.repeaters); } - }, [ - isSearching, - favoritesCollapsed, - channelsCollapsed, - contactsCollapsed, - repeatersCollapsed, - ]); + }, [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) => - isFavorite(favorites, 'contact', c.public_key) - ); - const nonFavChannels = filteredChannels.filter((c) => !isFavorite(favorites, 'channel', c.key)); - const nonFavContacts = filteredNonRepeaterContacts.filter( - (c) => !isFavorite(favorites, 'contact', c.public_key) - ); - const nonFavRepeaters = filteredRepeaters.filter( - (c) => !isFavorite(favorites, 'contact', c.public_key) - ); + const { favoriteItems, nonFavoriteChannels, nonFavoriteContacts, nonFavoriteRepeaters } = + useMemo(() => { + const favChannels = filteredChannels.filter((c) => isFavorite(favorites, 'channel', c.key)); + const favContacts = [...filteredNonRepeaterContacts, ...filteredRepeaters].filter((c) => + isFavorite(favorites, 'contact', c.public_key) + ); + const nonFavChannels = filteredChannels.filter( + (c) => !isFavorite(favorites, 'channel', c.key) + ); + const nonFavContacts = filteredNonRepeaterContacts.filter( + (c) => !isFavorite(favorites, 'contact', c.public_key) + ); + const nonFavRepeaters = filteredRepeaters.filter( + (c) => !isFavorite(favorites, 'contact', c.public_key) + ); - const items: FavoriteItem[] = [ - ...favChannels.map((channel) => ({ type: 'channel' as const, channel })), - ...favContacts.map((contact) => ({ type: 'contact' as const, contact })), - ].sort((a, b) => { - const timeA = - a.type === 'channel' - ? getLastMessageTime('channel', a.channel.key) - : getLastMessageTime('contact', a.contact.public_key); - const timeB = - b.type === 'channel' - ? getLastMessageTime('channel', b.channel.key) - : getLastMessageTime('contact', b.contact.public_key); - if (timeA && timeB) return timeB - timeA; - if (timeA && !timeB) return -1; - if (!timeA && timeB) return 1; - const nameA = a.type === 'channel' ? a.channel.name : a.contact.name || a.contact.public_key; - const nameB = b.type === 'channel' ? b.channel.name : b.contact.name || b.contact.public_key; - return nameA.localeCompare(nameB); - }); + const items: FavoriteItem[] = [ + ...favChannels.map((channel) => ({ type: 'channel' as const, channel })), + ...favContacts.map((contact) => ({ type: 'contact' as const, contact })), + ].sort((a, b) => { + const timeA = + a.type === 'channel' + ? getLastMessageTime('channel', a.channel.key) + : getLastMessageTime('contact', a.contact.public_key); + const timeB = + b.type === 'channel' + ? getLastMessageTime('channel', b.channel.key) + : getLastMessageTime('contact', b.contact.public_key); + if (timeA && timeB) return timeB - timeA; + if (timeA && !timeB) return -1; + if (!timeA && timeB) return 1; + const nameA = + a.type === 'channel' ? a.channel.name : a.contact.name || a.contact.public_key; + const nameB = + b.type === 'channel' ? b.channel.name : b.contact.name || b.contact.public_key; + return nameA.localeCompare(nameB); + }); - return { - favoriteItems: items, - nonFavoriteChannels: nonFavChannels, - nonFavoriteContacts: nonFavContacts, - nonFavoriteRepeaters: nonFavRepeaters, - }; - }, [ - filteredChannels, - filteredNonRepeaterContacts, - filteredRepeaters, - favorites, - getLastMessageTime, - ]); + return { + favoriteItems: items, + nonFavoriteChannels: nonFavChannels, + nonFavoriteContacts: nonFavContacts, + nonFavoriteRepeaters: nonFavRepeaters, + }; + }, [ + filteredChannels, + filteredNonRepeaterContacts, + filteredRepeaters, + favorites, + getLastMessageTime, + ]); const buildChannelRow = (channel: Channel, keyPrefix: string): ConversationRow => ({ key: `${keyPrefix}-${channel.key}`, diff --git a/frontend/src/test/sidebar.test.tsx b/frontend/src/test/sidebar.test.tsx index c37c26c..a727e2e 100644 --- a/frontend/src/test/sidebar.test.tsx +++ b/frontend/src/test/sidebar.test.tsx @@ -38,10 +38,11 @@ function renderSidebar(overrides?: { favorites?: Favorite[]; lastMessageTimes?: ConversationTimes; }) { + const aliceName = 'Alice'; const publicChannel = makeChannel('AA'.repeat(16), 'Public'); const flightChannel = makeChannel('BB'.repeat(16), '#flight'); const opsChannel = makeChannel('CC'.repeat(16), '#ops'); - const alice = makeContact('11'.repeat(32), 'Alice'); + const alice = makeContact('11'.repeat(32), aliceName); const relay = makeContact('22'.repeat(32), 'Relay', CONTACT_TYPE_REPEATER); const unreadCounts = overrides?.unreadCounts ?? { @@ -73,7 +74,7 @@ function renderSidebar(overrides?: { /> ); - return { flightChannel, opsChannel, alice }; + return { flightChannel, opsChannel, aliceName }; } function getSectionHeaderContainer(title: string): HTMLElement { @@ -94,27 +95,26 @@ describe('Sidebar section summaries', () => { }); it('expands collapsed sections during search and restores collapse state after clearing search', async () => { - const { opsChannel, alice } = renderSidebar(); + const { opsChannel, aliceName } = renderSidebar(); fireEvent.click(screen.getByRole('button', { name: /Channels/i })); fireEvent.click(screen.getByRole('button', { name: /Contacts/i })); expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument(); - expect(screen.queryByText(alice.name)).not.toBeInTheDocument(); + expect(screen.queryByText(aliceName)).not.toBeInTheDocument(); const search = screen.getByPlaceholderText('Search...'); fireEvent.change(search, { target: { value: 'alice' } }); await waitFor(() => { - expect(screen.getByText(alice.name)).toBeInTheDocument(); + expect(screen.getByText(aliceName)).toBeInTheDocument(); }); fireEvent.change(search, { target: { value: '' } }); await waitFor(() => { expect(screen.queryByText(opsChannel.name)).not.toBeInTheDocument(); - expect(screen.queryByText(alice.name)).not.toBeInTheDocument(); + expect(screen.queryByText(aliceName)).not.toBeInTheDocument(); }); }); }); -