From 7198d92c746e4139fe2dc5a1212f2b44e4501674 Mon Sep 17 00:00:00 2001 From: jkingsman Date: Mon, 15 Jun 2026 22:06:55 -0700 Subject: [PATCH] Use the correct last-heard time for repeater recency sorting --- frontend/src/components/Sidebar.tsx | 8 +++++- frontend/src/test/sidebar.test.tsx | 43 +++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index d74bfd1..a5f6d9d 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -197,7 +197,13 @@ export function Sidebar({ ); const getContactHeardTime = useCallback((contact: Contact): number => { - return Math.max(contact.last_seen ?? 0, contact.last_advert ?? 0); + // Prefer last_seen (server receive wall clock — the value the UI shows as + // "Last heard") so the recency sort matches the displayed date. Fall back to + // last_advert only for repeaters known purely from radio sync, which have no + // independent last_seen. Using Math.max here let a radio-reported (sender + // clock, skew-prone) last_advert pin a repeater to the top even when its + // displayed last_seen was older. See ContactStatusInfo "Last heard". + return contact.last_seen || contact.last_advert || 0; }, []); const getContactRecentTime = useCallback( diff --git a/frontend/src/test/sidebar.test.tsx b/frontend/src/test/sidebar.test.tsx index a484d6d..4b65795 100644 --- a/frontend/src/test/sidebar.test.tsx +++ b/frontend/src/test/sidebar.test.tsx @@ -597,6 +597,49 @@ describe('Sidebar section summaries', () => { expect(repeaterRows).toEqual(['Fresh Advert Relay', 'Stale Message Relay']); }); + it('sorts a favorite repeater by its displayed last_seen, not an inflated last_advert', () => { + const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public'); + // Radio contact sync overwrites last_advert with the radio's sender-clock + // value, which can land far ahead of (or in the future relative to) the + // server's last_seen. The sidebar shows last_seen as "Last heard", so the + // recency sort must follow last_seen rather than the skewed last_advert. + const skewedRelay = makeContact('44'.repeat(32), 'Skewed Relay', CONTACT_TYPE_REPEATER, { + last_seen: 100, + last_advert: 9_999_999, + favorite: true, + }); + const recentRelay = makeContact('55'.repeat(32), 'Recent Relay', CONTACT_TYPE_REPEATER, { + last_seen: 500, + favorite: true, + }); + + render( + + ); + + const repeaterRows = screen + .getAllByText(/Relay$/) + .map((node) => node.textContent) + .filter((text): text is string => Boolean(text)); + + // Recent Relay was actually heard more recently (last_seen 500 > 100), so it + // sorts above the relay with the inflated last_advert. + expect(repeaterRows).toEqual(['Recent Relay', 'Skewed Relay']); + }); + it('pins only the canonical Public channel to the top of channel sorting', () => { const publicChannel = makeChannel(PUBLIC_CHANNEL_KEY, 'Public'); const fakePublic = makeChannel('DD'.repeat(16), 'Public');