diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index f5d2309..4de31d7 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -314,6 +314,36 @@ export function Sidebar({
[getContactHeardTime]
);
+ const getFavoriteItemName = useCallback(
+ (item: FavoriteItem) =>
+ item.type === 'channel'
+ ? item.channel.name
+ : getContactDisplayName(item.contact.name, item.contact.public_key, item.contact.last_advert),
+ []
+ );
+
+ const sortFavoriteItemsByOrder = useCallback(
+ (items: FavoriteItem[], order: SortOrder) =>
+ [...items].sort((a, b) => {
+ if (order === 'recent') {
+ const timeA =
+ a.type === 'channel'
+ ? getLastMessageTime('channel', a.channel.key)
+ : getContactRecentTime(a.contact);
+ const timeB =
+ b.type === 'channel'
+ ? getLastMessageTime('channel', b.channel.key)
+ : getContactRecentTime(b.contact);
+ if (timeA && timeB) return timeB - timeA;
+ if (timeA && !timeB) return -1;
+ if (!timeA && timeB) return 1;
+ }
+
+ return getFavoriteItemName(a).localeCompare(getFavoriteItemName(b));
+ }),
+ [getContactRecentTime, getFavoriteItemName, getLastMessageTime]
+ );
+
// Split non-repeater contacts and repeater contacts into separate sorted lists
const sortedNonRepeaterContacts = useMemo(
() =>
@@ -461,27 +491,10 @@ export function Sidebar({
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)
- : getContactRecentTime(a.contact);
- const timeB =
- b.type === 'channel'
- ? getLastMessageTime('channel', b.channel.key)
- : getContactRecentTime(b.contact);
- 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,
+ favoriteItems: sortFavoriteItemsByOrder(items, sectionSortOrders.favorites),
nonFavoriteChannels: nonFavChannels,
nonFavoriteContacts: nonFavContacts,
nonFavoriteRepeaters: nonFavRepeaters,
@@ -493,6 +506,8 @@ export function Sidebar({
favorites,
getContactRecentTime,
getLastMessageTime,
+ sectionSortOrders.favorites,
+ sortFavoriteItemsByOrder,
]);
const buildChannelRow = (channel: Channel, keyPrefix: string): ConversationRow => ({
@@ -841,7 +856,7 @@ export function Sidebar({
'Favorites',
favoritesCollapsed,
() => setFavoritesCollapsed((prev) => !prev),
- null,
+ 'favorites',
favoritesUnreadCount,
favoritesHasMention
)}
diff --git a/frontend/src/test/sidebar.test.tsx b/frontend/src/test/sidebar.test.tsx
index dde9e63..e889c6e 100644
--- a/frontend/src/test/sidebar.test.tsx
+++ b/frontend/src/test/sidebar.test.tsx
@@ -336,16 +336,17 @@ describe('Sidebar section summaries', () => {
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
fireEvent.click(screen.getByRole('button', { name: 'Sort Channels alphabetically' }));
+ fireEvent.click(screen.getByRole('button', { name: 'Sort Contacts alphabetically' }));
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
- expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
+ expect(getContactsOrder()).toEqual(['Amy', 'Zed']);
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
unmount();
render();
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
- expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
+ expect(getContactsOrder()).toEqual(['Amy', 'Zed']);
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
});
@@ -466,4 +467,51 @@ describe('Sidebar section summaries', () => {
name: 'Public',
});
});
+
+ it('sorts favorites independently and persists the favorites sort preference', () => {
+ const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
+ const zed = makeContact('11'.repeat(32), 'Zed', 1, { last_advert: 150 });
+ const amy = makeContact('22'.repeat(32), 'Amy');
+
+ const props = {
+ contacts: [zed, amy],
+ channels: [publicChannel],
+ activeConversation: null,
+ onSelectConversation: vi.fn(),
+ onNewMessage: vi.fn(),
+ lastMessageTimes: {
+ [getStateKey('contact', zed.public_key)]: 200,
+ },
+ unreadCounts: {},
+ mentions: {},
+ showCracker: false,
+ crackerRunning: false,
+ onToggleCracker: vi.fn(),
+ onMarkAllRead: vi.fn(),
+ favorites: [
+ { type: 'contact', id: zed.public_key },
+ { type: 'contact', id: amy.public_key },
+ ] satisfies Favorite[],
+ legacySortOrder: 'recent' as const,
+ };
+
+ const getFavoritesOrder = () =>
+ screen
+ .getAllByText(/^(Amy|Zed)$/)
+ .map((node) => node.textContent)
+ .filter((text): text is string => Boolean(text));
+
+ const { unmount } = render();
+
+ expect(getFavoritesOrder()).toEqual(['Zed', 'Amy']);
+
+ fireEvent.click(screen.getByRole('button', { name: 'Sort Favorites alphabetically' }));
+
+ expect(getFavoritesOrder()).toEqual(['Amy', 'Zed']);
+
+ unmount();
+ render();
+
+ expect(getFavoritesOrder()).toEqual(['Amy', 'Zed']);
+ });
});
diff --git a/frontend/src/utils/conversationState.ts b/frontend/src/utils/conversationState.ts
index 6a42156..df6d8f0 100644
--- a/frontend/src/utils/conversationState.ts
+++ b/frontend/src/utils/conversationState.ts
@@ -15,7 +15,7 @@ const SIDEBAR_SECTION_SORT_ORDERS_KEY = 'remoteterm-sidebar-section-sort-orders'
export type ConversationTimes = Record;
export type SortOrder = 'recent' | 'alpha';
-export type SidebarSortableSection = 'channels' | 'contacts' | 'repeaters';
+export type SidebarSortableSection = 'favorites' | 'channels' | 'contacts' | 'repeaters';
export type SidebarSectionSortOrders = Record;
// In-memory cache of last message times (loaded from server on init)
@@ -113,6 +113,7 @@ export function buildSidebarSectionSortOrders(
defaultOrder: SortOrder = 'recent'
): SidebarSectionSortOrders {
return {
+ favorites: defaultOrder,
channels: defaultOrder,
contacts: defaultOrder,
repeaters: defaultOrder,
@@ -129,6 +130,7 @@ export function loadLocalStorageSidebarSectionSortOrders(): SidebarSectionSortOr
const parsed = JSON.parse(stored) as Partial;
return {
+ favorites: parsed.favorites === 'alpha' ? 'alpha' : 'recent',
channels: parsed.channels === 'alpha' ? 'alpha' : 'recent',
contacts: parsed.contacts === 'alpha' ? 'alpha' : 'recent',
repeaters: parsed.repeaters === 'alpha' ? 'alpha' : 'recent',