mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-07-07 02:11:22 +02:00
Allow favorites to be sorted. Closes #91.
This commit is contained in:
@@ -314,6 +314,36 @@ export function Sidebar({
|
|||||||
[getContactHeardTime]
|
[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
|
// Split non-repeater contacts and repeater contacts into separate sorted lists
|
||||||
const sortedNonRepeaterContacts = useMemo(
|
const sortedNonRepeaterContacts = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -461,27 +491,10 @@ export function Sidebar({
|
|||||||
const items: FavoriteItem[] = [
|
const items: FavoriteItem[] = [
|
||||||
...favChannels.map((channel) => ({ type: 'channel' as const, channel })),
|
...favChannels.map((channel) => ({ type: 'channel' as const, channel })),
|
||||||
...favContacts.map((contact) => ({ type: 'contact' as const, contact })),
|
...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 {
|
return {
|
||||||
favoriteItems: items,
|
favoriteItems: sortFavoriteItemsByOrder(items, sectionSortOrders.favorites),
|
||||||
nonFavoriteChannels: nonFavChannels,
|
nonFavoriteChannels: nonFavChannels,
|
||||||
nonFavoriteContacts: nonFavContacts,
|
nonFavoriteContacts: nonFavContacts,
|
||||||
nonFavoriteRepeaters: nonFavRepeaters,
|
nonFavoriteRepeaters: nonFavRepeaters,
|
||||||
@@ -493,6 +506,8 @@ export function Sidebar({
|
|||||||
favorites,
|
favorites,
|
||||||
getContactRecentTime,
|
getContactRecentTime,
|
||||||
getLastMessageTime,
|
getLastMessageTime,
|
||||||
|
sectionSortOrders.favorites,
|
||||||
|
sortFavoriteItemsByOrder,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const buildChannelRow = (channel: Channel, keyPrefix: string): ConversationRow => ({
|
const buildChannelRow = (channel: Channel, keyPrefix: string): ConversationRow => ({
|
||||||
@@ -841,7 +856,7 @@ export function Sidebar({
|
|||||||
'Favorites',
|
'Favorites',
|
||||||
favoritesCollapsed,
|
favoritesCollapsed,
|
||||||
() => setFavoritesCollapsed((prev) => !prev),
|
() => setFavoritesCollapsed((prev) => !prev),
|
||||||
null,
|
'favorites',
|
||||||
favoritesUnreadCount,
|
favoritesUnreadCount,
|
||||||
favoritesHasMention
|
favoritesHasMention
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -336,16 +336,17 @@ describe('Sidebar section summaries', () => {
|
|||||||
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: 'Sort Channels alphabetically' }));
|
fireEvent.click(screen.getByRole('button', { name: 'Sort Channels alphabetically' }));
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Sort Contacts alphabetically' }));
|
||||||
|
|
||||||
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
||||||
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
expect(getContactsOrder()).toEqual(['Amy', 'Zed']);
|
||||||
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
||||||
|
|
||||||
unmount();
|
unmount();
|
||||||
render(<Sidebar {...props} />);
|
render(<Sidebar {...props} />);
|
||||||
|
|
||||||
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
||||||
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
expect(getContactsOrder()).toEqual(['Amy', 'Zed']);
|
||||||
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -466,4 +467,51 @@ describe('Sidebar section summaries', () => {
|
|||||||
name: 'Public',
|
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(<Sidebar {...props} />);
|
||||||
|
|
||||||
|
expect(getFavoritesOrder()).toEqual(['Zed', 'Amy']);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: 'Sort Favorites alphabetically' }));
|
||||||
|
|
||||||
|
expect(getFavoritesOrder()).toEqual(['Amy', 'Zed']);
|
||||||
|
|
||||||
|
unmount();
|
||||||
|
render(<Sidebar {...props} />);
|
||||||
|
|
||||||
|
expect(getFavoritesOrder()).toEqual(['Amy', 'Zed']);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const SIDEBAR_SECTION_SORT_ORDERS_KEY = 'remoteterm-sidebar-section-sort-orders'
|
|||||||
|
|
||||||
export type ConversationTimes = Record<string, number>;
|
export type ConversationTimes = Record<string, number>;
|
||||||
export type SortOrder = 'recent' | 'alpha';
|
export type SortOrder = 'recent' | 'alpha';
|
||||||
export type SidebarSortableSection = 'channels' | 'contacts' | 'repeaters';
|
export type SidebarSortableSection = 'favorites' | 'channels' | 'contacts' | 'repeaters';
|
||||||
export type SidebarSectionSortOrders = Record<SidebarSortableSection, SortOrder>;
|
export type SidebarSectionSortOrders = Record<SidebarSortableSection, SortOrder>;
|
||||||
|
|
||||||
// In-memory cache of last message times (loaded from server on init)
|
// In-memory cache of last message times (loaded from server on init)
|
||||||
@@ -113,6 +113,7 @@ export function buildSidebarSectionSortOrders(
|
|||||||
defaultOrder: SortOrder = 'recent'
|
defaultOrder: SortOrder = 'recent'
|
||||||
): SidebarSectionSortOrders {
|
): SidebarSectionSortOrders {
|
||||||
return {
|
return {
|
||||||
|
favorites: defaultOrder,
|
||||||
channels: defaultOrder,
|
channels: defaultOrder,
|
||||||
contacts: defaultOrder,
|
contacts: defaultOrder,
|
||||||
repeaters: defaultOrder,
|
repeaters: defaultOrder,
|
||||||
@@ -129,6 +130,7 @@ export function loadLocalStorageSidebarSectionSortOrders(): SidebarSectionSortOr
|
|||||||
|
|
||||||
const parsed = JSON.parse(stored) as Partial<SidebarSectionSortOrders>;
|
const parsed = JSON.parse(stored) as Partial<SidebarSectionSortOrders>;
|
||||||
return {
|
return {
|
||||||
|
favorites: parsed.favorites === 'alpha' ? 'alpha' : 'recent',
|
||||||
channels: parsed.channels === 'alpha' ? 'alpha' : 'recent',
|
channels: parsed.channels === 'alpha' ? 'alpha' : 'recent',
|
||||||
contacts: parsed.contacts === 'alpha' ? 'alpha' : 'recent',
|
contacts: parsed.contacts === 'alpha' ? 'alpha' : 'recent',
|
||||||
repeaters: parsed.repeaters === 'alpha' ? 'alpha' : 'recent',
|
repeaters: parsed.repeaters === 'alpha' ? 'alpha' : 'recent',
|
||||||
|
|||||||
Reference in New Issue
Block a user