diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index b6dab4d..9a04888 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -56,6 +56,10 @@ export function Sidebar({
}: SidebarProps) {
const sortOrder = sortOrderProp;
const [searchQuery, setSearchQuery] = useState('');
+ const [favoritesCollapsed, setFavoritesCollapsed] = useState(false);
+ const [channelsCollapsed, setChannelsCollapsed] = useState(false);
+ const [contactsCollapsed, setContactsCollapsed] = useState(false);
+ const [repeatersCollapsed, setRepeatersCollapsed] = useState(false);
const handleSortToggle = () => {
const newOrder = sortOrder === 'alpha' ? 'recent' : 'alpha';
@@ -143,20 +147,9 @@ export function Sidebar({
[uniqueChannels, sortOrder, getLastMessageTime]
);
- // Sort contacts: non-repeaters first (by recent or alpha), then repeaters (always alpha)
- const sortedContacts = useMemo(
- () =>
- [...uniqueContacts].sort((a, b) => {
- const aIsRepeater = a.type === CONTACT_TYPE_REPEATER;
- const bIsRepeater = b.type === CONTACT_TYPE_REPEATER;
-
- if (aIsRepeater && !bIsRepeater) return 1;
- if (!aIsRepeater && bIsRepeater) return -1;
-
- if (aIsRepeater && bIsRepeater) {
- return (a.name || a.public_key).localeCompare(b.name || b.public_key);
- }
-
+ const sortContactsByOrder = useCallback(
+ (items: Contact[]) =>
+ [...items].sort((a, b) => {
if (sortOrder === 'recent') {
const timeA = getLastMessageTime('contact', a.public_key);
const timeB = getLastMessageTime('contact', b.public_key);
@@ -166,7 +159,18 @@ export function Sidebar({
}
return (a.name || a.public_key).localeCompare(b.name || b.public_key);
}),
- [uniqueContacts, sortOrder, getLastMessageTime]
+ [sortOrder, getLastMessageTime]
+ );
+
+ // Split non-repeater contacts and repeater contacts into separate sorted lists
+ const sortedNonRepeaterContacts = useMemo(
+ () => sortContactsByOrder(uniqueContacts.filter((c) => c.type !== CONTACT_TYPE_REPEATER)),
+ [uniqueContacts, sortContactsByOrder]
+ );
+
+ const sortedRepeaters = useMemo(
+ () => sortContactsByOrder(uniqueContacts.filter((c) => c.type === CONTACT_TYPE_REPEATER)),
+ [uniqueContacts, sortContactsByOrder]
);
// Filter by search query
@@ -180,27 +184,42 @@ export function Sidebar({
: sortedChannels,
[sortedChannels, query]
);
- const filteredContacts = useMemo(
+
+ const filteredNonRepeaterContacts = useMemo(
() =>
query
- ? sortedContacts.filter(
+ ? sortedNonRepeaterContacts.filter(
(c) =>
c.name?.toLowerCase().includes(query) || c.public_key.toLowerCase().includes(query)
)
- : sortedContacts,
- [sortedContacts, query]
+ : sortedNonRepeaterContacts,
+ [sortedNonRepeaterContacts, query]
+ );
+
+ const filteredRepeaters = useMemo(
+ () =>
+ query
+ ? sortedRepeaters.filter(
+ (c) =>
+ c.name?.toLowerCase().includes(query) || c.public_key.toLowerCase().includes(query)
+ )
+ : sortedRepeaters,
+ [sortedRepeaters, query]
);
// Separate favorites from regular items, and build combined favorites list
type FavoriteItem = { type: 'channel'; channel: Channel } | { type: 'contact'; contact: Contact };
- const { favoriteItems, nonFavoriteChannels, nonFavoriteContacts } = useMemo(() => {
+ const { favoriteItems, nonFavoriteChannels, nonFavoriteContacts, nonFavoriteRepeaters } = useMemo(() => {
const favChannels = filteredChannels.filter((c) => isFavorite(favorites, 'channel', c.key));
- const favContacts = filteredContacts.filter((c) =>
+ const favContacts = [...filteredNonRepeaterContacts, ...filteredRepeaters].filter((c) =>
isFavorite(favorites, 'contact', c.public_key)
);
const nonFavChannels = filteredChannels.filter((c) => !isFavorite(favorites, 'channel', c.key));
- const nonFavContacts = filteredContacts.filter(
+ const nonFavContacts = filteredNonRepeaterContacts.filter(
+ (c) => !isFavorite(favorites, 'contact', c.public_key)
+ );
+ const nonFavRepeaters = filteredRepeaters.filter(
(c) => !isFavorite(favorites, 'contact', c.public_key)
);
@@ -228,8 +247,42 @@ export function Sidebar({
favoriteItems: items,
nonFavoriteChannels: nonFavChannels,
nonFavoriteContacts: nonFavContacts,
+ nonFavoriteRepeaters: nonFavRepeaters,
};
- }, [filteredChannels, filteredContacts, favorites, getLastMessageTime]);
+ }, [
+ filteredChannels,
+ filteredNonRepeaterContacts,
+ filteredRepeaters,
+ favorites,
+ getLastMessageTime,
+ ]);
+
+ const renderSectionHeader = (
+ title: string,
+ collapsed: boolean,
+ onToggle: () => void,
+ showSortToggle = false
+ ) => (
+
+
+ {showSortToggle && (
+
+ )}
+
+ );
return (
@@ -367,17 +420,113 @@ export function Sidebar({
{/* Favorites */}
{favoriteItems.length > 0 && (
<>
-
- Favorites
-
- {favoriteItems.map((item) => {
- if (item.type === 'channel') {
- const channel = item.channel;
+ {renderSectionHeader(
+ 'Favorites',
+ favoritesCollapsed,
+ () => 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}
+
+ )}
+
+ );
+ }
+ })}
+ >
+ )}
+
+ {/* Channels */}
+ {nonFavoriteChannels.length > 0 && (
+ <>
+ {renderSectionHeader(
+ 'Channels',
+ channelsCollapsed,
+ () => setChannelsCollapsed((prev) => !prev),
+ true
+ )}
+ {!channelsCollapsed &&
+ nonFavoriteChannels.map((channel) => {
const unreadCount = getUnreadCount('channel', channel.key);
const isMention = hasMention('channel', channel.key);
return (
);
- } else {
- const contact = item.contact;
+ })}
+ >
+ )}
+
+ {/* Contacts */}
+ {nonFavoriteContacts.length > 0 && (
+ <>
+ {renderSectionHeader(
+ 'Contacts',
+ contactsCollapsed,
+ () => setContactsCollapsed((prev) => !prev),
+ true
+ )}
+ {!contactsCollapsed &&
+ nonFavoriteContacts.map((contact) => {
const unreadCount = getUnreadCount('contact', contact.public_key);
const isMention = hasMention('contact', contact.public_key);
return (
);
- }
- })}
+ })}
>
)}
- {/* Channels */}
- {nonFavoriteChannels.length > 0 && (
+ {/* Repeaters */}
+ {nonFavoriteRepeaters.length > 0 && (
<>
-
- Channels
-
-
- {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}
+ {renderSectionHeader(
+ 'Repeaters',
+ repeatersCollapsed,
+ () => 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)}
- )}
-
- );
- })}
- >
- )}
-
- {/* Contacts */}
- {nonFavoriteContacts.length > 0 && (
- <>
-
- Contacts
- {nonFavoriteChannels.length === 0 && (
-
- )}
-
- {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}
-
- )}
-
- );
- })}
+ {unreadCount > 0 && (
+
+ {unreadCount}
+
+ )}
+
+ );
+ })}
>
)}
{/* Empty state */}
{nonFavoriteContacts.length === 0 &&
nonFavoriteChannels.length === 0 &&
+ nonFavoriteRepeaters.length === 0 &&
favoriteItems.length === 0 && (
{query ? 'No matches found' : 'No conversations yet'}