mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Fix sidebar ordering for contacts by advert. Closes #69.
This commit is contained in:
@@ -216,6 +216,20 @@ export function Sidebar({
|
||||
[lastMessageTimes]
|
||||
);
|
||||
|
||||
const getContactHeardTime = useCallback((contact: Contact): number => {
|
||||
return Math.max(contact.last_seen ?? 0, contact.last_advert ?? 0);
|
||||
}, []);
|
||||
|
||||
const getContactRecentTime = useCallback(
|
||||
(contact: Contact): number => {
|
||||
if (contact.type === CONTACT_TYPE_REPEATER) {
|
||||
return getContactHeardTime(contact);
|
||||
}
|
||||
return getLastMessageTime('contact', contact.public_key) || getContactHeardTime(contact);
|
||||
},
|
||||
[getContactHeardTime, getLastMessageTime]
|
||||
);
|
||||
|
||||
// Deduplicate channels by key only.
|
||||
// Channel names are not unique; distinct keys must remain visible.
|
||||
const uniqueChannels = useMemo(
|
||||
@@ -274,15 +288,30 @@ export function Sidebar({
|
||||
(items: Contact[], order: SortOrder) =>
|
||||
[...items].sort((a, b) => {
|
||||
if (order === 'recent') {
|
||||
const timeA = getLastMessageTime('contact', a.public_key);
|
||||
const timeB = getLastMessageTime('contact', b.public_key);
|
||||
const timeA = getContactRecentTime(a);
|
||||
const timeB = getContactRecentTime(b);
|
||||
if (timeA && timeB) return timeB - timeA;
|
||||
if (timeA && !timeB) return -1;
|
||||
if (!timeA && timeB) return 1;
|
||||
}
|
||||
return (a.name || a.public_key).localeCompare(b.name || b.public_key);
|
||||
}),
|
||||
[getLastMessageTime]
|
||||
[getContactRecentTime]
|
||||
);
|
||||
|
||||
const sortRepeatersByOrder = useCallback(
|
||||
(items: Contact[], order: SortOrder) =>
|
||||
[...items].sort((a, b) => {
|
||||
if (order === 'recent') {
|
||||
const timeA = getContactHeardTime(a);
|
||||
const timeB = getContactHeardTime(b);
|
||||
if (timeA && timeB) return timeB - timeA;
|
||||
if (timeA && !timeB) return -1;
|
||||
if (!timeA && timeB) return 1;
|
||||
}
|
||||
return (a.name || a.public_key).localeCompare(b.name || b.public_key);
|
||||
}),
|
||||
[getContactHeardTime]
|
||||
);
|
||||
|
||||
// Split non-repeater contacts and repeater contacts into separate sorted lists
|
||||
@@ -297,11 +326,11 @@ export function Sidebar({
|
||||
|
||||
const sortedRepeaters = useMemo(
|
||||
() =>
|
||||
sortContactsByOrder(
|
||||
sortRepeatersByOrder(
|
||||
uniqueContacts.filter((c) => c.type === CONTACT_TYPE_REPEATER),
|
||||
sectionSortOrders.repeaters
|
||||
),
|
||||
[uniqueContacts, sectionSortOrders.repeaters, sortContactsByOrder]
|
||||
[uniqueContacts, sectionSortOrders.repeaters, sortRepeatersByOrder]
|
||||
);
|
||||
|
||||
// Filter by search query
|
||||
@@ -436,11 +465,11 @@ export function Sidebar({
|
||||
const timeA =
|
||||
a.type === 'channel'
|
||||
? getLastMessageTime('channel', a.channel.key)
|
||||
: getLastMessageTime('contact', a.contact.public_key);
|
||||
: getContactRecentTime(a.contact);
|
||||
const timeB =
|
||||
b.type === 'channel'
|
||||
? getLastMessageTime('channel', b.channel.key)
|
||||
: getLastMessageTime('contact', b.contact.public_key);
|
||||
: getContactRecentTime(b.contact);
|
||||
if (timeA && timeB) return timeB - timeA;
|
||||
if (timeA && !timeB) return -1;
|
||||
if (!timeA && timeB) return 1;
|
||||
@@ -462,6 +491,7 @@ export function Sidebar({
|
||||
filteredNonRepeaterContacts,
|
||||
filteredRepeaters,
|
||||
favorites,
|
||||
getContactRecentTime,
|
||||
getLastMessageTime,
|
||||
]);
|
||||
|
||||
|
||||
@@ -16,7 +16,12 @@ function makeChannel(key: string, name: string): Channel {
|
||||
};
|
||||
}
|
||||
|
||||
function makeContact(public_key: string, name: string, type = 1): Contact {
|
||||
function makeContact(
|
||||
public_key: string,
|
||||
name: string,
|
||||
type = 1,
|
||||
overrides: Partial<Contact> = {}
|
||||
): Contact {
|
||||
return {
|
||||
public_key,
|
||||
name,
|
||||
@@ -33,6 +38,7 @@ function makeContact(public_key: string, name: string, type = 1): Contact {
|
||||
last_contacted: null,
|
||||
last_read_at: null,
|
||||
first_seen: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -257,10 +263,14 @@ describe('Sidebar section summaries', () => {
|
||||
const publicChannel = makeChannel('AA'.repeat(16), 'Public');
|
||||
const zebraChannel = makeChannel('BB'.repeat(16), '#zebra');
|
||||
const alphaChannel = makeChannel('CC'.repeat(16), '#alpha');
|
||||
const zed = makeContact('11'.repeat(32), 'Zed');
|
||||
const zed = makeContact('11'.repeat(32), 'Zed', 1, { last_advert: 150 });
|
||||
const amy = makeContact('22'.repeat(32), 'Amy');
|
||||
const relayZulu = makeContact('33'.repeat(32), 'Zulu Relay', CONTACT_TYPE_REPEATER);
|
||||
const relayAlpha = makeContact('44'.repeat(32), 'Alpha Relay', CONTACT_TYPE_REPEATER);
|
||||
const relayZulu = makeContact('33'.repeat(32), 'Zulu Relay', CONTACT_TYPE_REPEATER, {
|
||||
last_seen: 100,
|
||||
});
|
||||
const relayAlpha = makeContact('44'.repeat(32), 'Alpha Relay', CONTACT_TYPE_REPEATER, {
|
||||
last_seen: 300,
|
||||
});
|
||||
|
||||
const props = {
|
||||
contacts: [zed, amy, relayZulu, relayAlpha],
|
||||
@@ -272,9 +282,6 @@ describe('Sidebar section summaries', () => {
|
||||
[getStateKey('channel', zebraChannel.key)]: 300,
|
||||
[getStateKey('channel', alphaChannel.key)]: 100,
|
||||
[getStateKey('contact', zed.public_key)]: 200,
|
||||
[getStateKey('contact', amy.public_key)]: 100,
|
||||
[getStateKey('contact', relayZulu.public_key)]: 300,
|
||||
[getStateKey('contact', relayAlpha.public_key)]: 100,
|
||||
},
|
||||
unreadCounts: {},
|
||||
mentions: {},
|
||||
@@ -302,20 +309,104 @@ describe('Sidebar section summaries', () => {
|
||||
|
||||
expect(getChannelsOrder()).toEqual(['#zebra', '#alpha']);
|
||||
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
||||
expect(getRepeatersOrder()).toEqual(['Zulu Relay', 'Alpha Relay']);
|
||||
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Sort Channels alphabetically' }));
|
||||
|
||||
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
||||
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
||||
expect(getRepeatersOrder()).toEqual(['Zulu Relay', 'Alpha Relay']);
|
||||
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
||||
|
||||
unmount();
|
||||
render(<Sidebar {...props} />);
|
||||
|
||||
expect(getChannelsOrder()).toEqual(['#alpha', '#zebra']);
|
||||
expect(getContactsOrder()).toEqual(['Zed', 'Amy']);
|
||||
expect(getRepeatersOrder()).toEqual(['Zulu Relay', 'Alpha Relay']);
|
||||
expect(getRepeatersOrder()).toEqual(['Alpha Relay', 'Zulu Relay']);
|
||||
});
|
||||
|
||||
it('sorts contacts by DM recency first, then advert recency, then no-recency at the bottom', () => {
|
||||
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
||||
const dmRecent = makeContact('11'.repeat(32), 'DM Recent', 1, { last_advert: 100 });
|
||||
const advertOnly = makeContact('22'.repeat(32), 'Advert Only', 1, { last_seen: 300 });
|
||||
const noRecency = makeContact('33'.repeat(32), 'No Recency');
|
||||
|
||||
render(
|
||||
<Sidebar
|
||||
contacts={[noRecency, advertOnly, dmRecent]}
|
||||
channels={[publicChannel]}
|
||||
activeConversation={null}
|
||||
onSelectConversation={vi.fn()}
|
||||
onNewMessage={vi.fn()}
|
||||
lastMessageTimes={{
|
||||
[getStateKey('contact', dmRecent.public_key)]: 400,
|
||||
}}
|
||||
unreadCounts={{}}
|
||||
mentions={{}}
|
||||
showCracker={false}
|
||||
crackerRunning={false}
|
||||
onToggleCracker={vi.fn()}
|
||||
onMarkAllRead={vi.fn()}
|
||||
favorites={[]}
|
||||
legacySortOrder="recent"
|
||||
/>
|
||||
);
|
||||
|
||||
const contactRows = screen
|
||||
.getAllByText(/^(DM Recent|Advert Only|No Recency)$/)
|
||||
.map((node) => node.textContent)
|
||||
.filter((text): text is string => Boolean(text));
|
||||
|
||||
expect(contactRows).toEqual(['DM Recent', 'Advert Only', 'No Recency']);
|
||||
});
|
||||
|
||||
it('sorts repeaters by heard recency even when message times disagree', () => {
|
||||
const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public');
|
||||
const staleMessageRelay = makeContact(
|
||||
'44'.repeat(32),
|
||||
'Stale Message Relay',
|
||||
CONTACT_TYPE_REPEATER,
|
||||
{
|
||||
last_seen: 100,
|
||||
}
|
||||
);
|
||||
const freshAdvertRelay = makeContact(
|
||||
'55'.repeat(32),
|
||||
'Fresh Advert Relay',
|
||||
CONTACT_TYPE_REPEATER,
|
||||
{
|
||||
last_advert: 500,
|
||||
}
|
||||
);
|
||||
|
||||
render(
|
||||
<Sidebar
|
||||
contacts={[staleMessageRelay, freshAdvertRelay]}
|
||||
channels={[publicChannel]}
|
||||
activeConversation={null}
|
||||
onSelectConversation={vi.fn()}
|
||||
onNewMessage={vi.fn()}
|
||||
lastMessageTimes={{
|
||||
[getStateKey('contact', staleMessageRelay.public_key)]: 1000,
|
||||
[getStateKey('contact', freshAdvertRelay.public_key)]: 50,
|
||||
}}
|
||||
unreadCounts={{}}
|
||||
mentions={{}}
|
||||
showCracker={false}
|
||||
crackerRunning={false}
|
||||
onToggleCracker={vi.fn()}
|
||||
onMarkAllRead={vi.fn()}
|
||||
favorites={[]}
|
||||
legacySortOrder="recent"
|
||||
/>
|
||||
);
|
||||
|
||||
const repeaterRows = screen
|
||||
.getAllByText(/Relay$/)
|
||||
.map((node) => node.textContent)
|
||||
.filter((text): text is string => Boolean(text));
|
||||
|
||||
expect(repeaterRows).toEqual(['Fresh Advert Relay', 'Stale Message Relay']);
|
||||
});
|
||||
|
||||
it('pins only the canonical Public channel to the top of channel sorting', () => {
|
||||
|
||||
Reference in New Issue
Block a user