From 3dde94c2c45c8ce01a13d55dcbce1376ddb71fef Mon Sep 17 00:00:00 2001 From: "Michael A. Cojocari" Date: Tue, 20 Oct 2026 14:17:29 -0400 Subject: [PATCH] touch: "Fewest hops" sort for Contacts and Discovered (#718) Both lists already show the hop count on each row; they can now be sorted by it so the nearest repeaters come first. Combined with the Repeaters filter this shows which repeaters are reachable directly. Rows with no known path sink to the end in either direction, like the "?" rows in the time-based sorts. Hops use pathHops() so 2/3-byte path hashes don't read as 65+ hops. Language rows added for all 14 languages. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- deploy/apps/lang/bg.lang | 1 + deploy/apps/lang/cs.lang | 1 + deploy/apps/lang/de.lang | 1 + deploy/apps/lang/el.lang | 1 + deploy/apps/lang/es.lang | 1 + deploy/apps/lang/fr.lang | 1 + deploy/apps/lang/hu.lang | 1 + deploy/apps/lang/it.lang | 1 + deploy/apps/lang/nl.lang | 1 + deploy/apps/lang/pt-br.lang | 1 + deploy/apps/lang/ro.lang | 1 + deploy/apps/lang/ru.lang | 1 + deploy/apps/lang/sr.lang | 1 + deploy/apps/lang/uk.lang | 1 + src/ui-touch/UITask.cpp | 31 ++++++++++++++++++++++++++----- 15 files changed, 40 insertions(+), 5 deletions(-) diff --git a/deploy/apps/lang/bg.lang b/deploy/apps/lang/bg.lang index 3b2ee96b..9ddaca64 100644 --- a/deploy/apps/lang/bg.lang +++ b/deploy/apps/lang/bg.lang @@ -2160,3 +2160,4 @@ you вие Flood in / out Flood вход / изход Direct in / out Директни вход / изход Airtime TX / RX Ефирно време TX / RX +Fewest hops Най-малко скокове diff --git a/deploy/apps/lang/cs.lang b/deploy/apps/lang/cs.lang index f152cd29..0ef1e9e5 100644 --- a/deploy/apps/lang/cs.lang +++ b/deploy/apps/lang/cs.lang @@ -2176,3 +2176,4 @@ you vy Flood in / out Flood příchozí / odchozí Direct in / out Přímé příchozí / odchozí Airtime TX / RX Vysílací čas TX / RX +Fewest hops Nejméně skoků diff --git a/deploy/apps/lang/de.lang b/deploy/apps/lang/de.lang index 48ee3dee..626012f8 100644 --- a/deploy/apps/lang/de.lang +++ b/deploy/apps/lang/de.lang @@ -2158,3 +2158,4 @@ you du Flood in / out Flood ein / aus Direct in / out Direkt ein / aus Airtime TX / RX Sendezeit TX / RX +Fewest hops Wenigste Hops diff --git a/deploy/apps/lang/el.lang b/deploy/apps/lang/el.lang index 5d8e0394..d83825c6 100644 --- a/deploy/apps/lang/el.lang +++ b/deploy/apps/lang/el.lang @@ -2160,3 +2160,4 @@ you εσείς Flood in / out Flood εισ. / εξ. Direct in / out Άμεσα εισ. / εξ. Airtime TX / RX Χρόνος εκπομπής TX / RX +Fewest hops Λιγότερα άλματα diff --git a/deploy/apps/lang/es.lang b/deploy/apps/lang/es.lang index c4b53806..34396beb 100644 --- a/deploy/apps/lang/es.lang +++ b/deploy/apps/lang/es.lang @@ -2160,3 +2160,4 @@ you tú Flood in / out Flood entrada / salida Direct in / out Directo entrada / salida Airtime TX / RX Tiempo en aire TX / RX +Fewest hops Menos saltos diff --git a/deploy/apps/lang/fr.lang b/deploy/apps/lang/fr.lang index d394a482..c5232bcf 100644 --- a/deploy/apps/lang/fr.lang +++ b/deploy/apps/lang/fr.lang @@ -2159,3 +2159,4 @@ you vous Flood in / out Flood reçus / émis Direct in / out Direct reçus / émis Airtime TX / RX Temps d'antenne TX / RX +Fewest hops Moins de sauts diff --git a/deploy/apps/lang/hu.lang b/deploy/apps/lang/hu.lang index e361b9dc..e5ebf9d7 100644 --- a/deploy/apps/lang/hu.lang +++ b/deploy/apps/lang/hu.lang @@ -2170,3 +2170,4 @@ you te Flood in / out Flood be / ki Direct in / out Direkt be / ki Airtime TX / RX Adásidő TX / RX +Fewest hops Legkevesebb ugrás diff --git a/deploy/apps/lang/it.lang b/deploy/apps/lang/it.lang index 9b434be7..9ce57ba6 100644 --- a/deploy/apps/lang/it.lang +++ b/deploy/apps/lang/it.lang @@ -2159,3 +2159,4 @@ you tu Flood in / out Flood in / out Direct in / out Diretti in / out Airtime TX / RX Tempo in onda TX / RX +Fewest hops Meno salti diff --git a/deploy/apps/lang/nl.lang b/deploy/apps/lang/nl.lang index 02146a65..1fd942eb 100644 --- a/deploy/apps/lang/nl.lang +++ b/deploy/apps/lang/nl.lang @@ -2158,3 +2158,4 @@ you jij Flood in / out Flood in / uit Direct in / out Direct in / uit Airtime TX / RX Zendtijd TX / RX +Fewest hops Minste hops diff --git a/deploy/apps/lang/pt-br.lang b/deploy/apps/lang/pt-br.lang index 5a36b247..55e6e829 100644 --- a/deploy/apps/lang/pt-br.lang +++ b/deploy/apps/lang/pt-br.lang @@ -2160,3 +2160,4 @@ you você Flood in / out Flood entrada / saída Direct in / out Direto entrada / saída Airtime TX / RX Tempo no ar TX / RX +Fewest hops Menos saltos diff --git a/deploy/apps/lang/ro.lang b/deploy/apps/lang/ro.lang index 06edf2b1..71f439d6 100644 --- a/deploy/apps/lang/ro.lang +++ b/deploy/apps/lang/ro.lang @@ -2161,3 +2161,4 @@ you tu Flood in / out Flood intrare / ieșire Direct in / out Direct intrare / ieșire Airtime TX / RX Timp în aer TX / RX +Fewest hops Cele mai puține salturi diff --git a/deploy/apps/lang/ru.lang b/deploy/apps/lang/ru.lang index a9e06973..a6ff9f81 100644 --- a/deploy/apps/lang/ru.lang +++ b/deploy/apps/lang/ru.lang @@ -2160,3 +2160,4 @@ you вы Flood in / out Flood вход / выход Direct in / out Прямые вход / выход Airtime TX / RX Эфирное время TX / RX +Fewest hops Меньше всего хопов diff --git a/deploy/apps/lang/sr.lang b/deploy/apps/lang/sr.lang index e27a4daf..38b1b54e 100644 --- a/deploy/apps/lang/sr.lang +++ b/deploy/apps/lang/sr.lang @@ -2160,3 +2160,4 @@ you ви Flood in / out Flood улаз / излаз Direct in / out Директни улаз / излаз Airtime TX / RX Време емитовања TX / RX +Fewest hops Најмање скокова diff --git a/deploy/apps/lang/uk.lang b/deploy/apps/lang/uk.lang index 2bf5194b..1bc9df04 100644 --- a/deploy/apps/lang/uk.lang +++ b/deploy/apps/lang/uk.lang @@ -2160,3 +2160,4 @@ you ви Flood in / out Flood вхід / вихід Direct in / out Прямі вхід / вихід Airtime TX / RX Ефірний час TX / RX +Fewest hops Найменше хопів diff --git a/src/ui-touch/UITask.cpp b/src/ui-touch/UITask.cpp index f2aa606e..5a4cf85c 100644 --- a/src/ui-touch/UITask.cpp +++ b/src/ui-touch/UITask.cpp @@ -2962,14 +2962,15 @@ enum ContactsSortMode : uint8_t { CONTACTS_SORT_LAST_HEARD = 1, // most recent advert first (default) CONTACTS_SORT_LAST_MSG = 2, // most recent message first (falls back to last-heard) CONTACTS_SORT_DISTANCE = 3, // nearest first; rows with no shared location are excluded - CONTACTS_SORT_COUNT = 4, + CONTACTS_SORT_HOPS = 4, // fewest hops first; path-unknown rows sink to the end (#718) + CONTACTS_SORT_COUNT = 5, }; // Default to most-recently-heard at the top — operators care who's on the // air now far more than alphabetical order. Favorites still pin above all // (see the qsort comparator in refreshContactsList). static uint8_t g_contacts_sort = CONTACTS_SORT_LAST_HEARD; // Reverse the active sort's natural order (the asc/desc toggle). Natural order is -// A→Z / newest-heard / newest-message / nearest; `desc` flips just the primary key +// A→Z / newest-heard / newest-message / nearest / fewest hops; `desc` flips just the primary key // (favourites stay pinned on top, "?"-heard rows stay sunk). Reset when the mode // changes; re-tapping the active mode in the Sort sheet flips it. static bool g_contacts_sort_desc = false; @@ -2980,6 +2981,7 @@ static const char* contactsSortLabel(uint8_t m) { case CONTACTS_SORT_LAST_HEARD: return "RECENT"; case CONTACTS_SORT_LAST_MSG: return "MSG"; case CONTACTS_SORT_DISTANCE: return "DIST"; + case CONTACTS_SORT_HOPS: return "HOPS"; } return "?"; } @@ -2990,6 +2992,7 @@ static const char* contactsSortOptName(uint8_t m) { case CONTACTS_SORT_LAST_HEARD: return "Recently heard"; case CONTACTS_SORT_LAST_MSG: return "Recent message"; case CONTACTS_SORT_DISTANCE: return "Nearest first"; + case CONTACTS_SORT_HOPS: return "Fewest hops"; } return "?"; } @@ -14154,14 +14157,15 @@ static DiscoveredAddCtx s_disc_add_ctx[DISCOVERED_MAX]; static void openDiscoveredModalCb(lv_event_t* e); // ---- Discovered-list sort (mirrors the Contacts sort) ------------------------- -// Recently heard / Name / Distance, each with an ascending/descending toggle. +// Recently heard / Name / Distance / Hops, each with an ascending/descending toggle. // "Recent message" is contacts-only (discovered nodes have no message history) so // it's omitted. Distance excludes entries that have shared no location. enum DiscSortMode : uint8_t { DISC_SORT_RECENT = 0, // newest advert first (default) DISC_SORT_AZ = 1, // name A-Z DISC_SORT_DISTANCE = 2, // nearest first; no-location entries excluded - DISC_SORT_COUNT = 3, + DISC_SORT_HOPS = 3, // fewest hops first; path-unknown entries sink to the end (#718) + DISC_SORT_COUNT = 4, }; static uint8_t g_disc_sort = DISC_SORT_RECENT; static bool g_disc_sort_desc = false; @@ -14170,6 +14174,7 @@ static const char* discSortOptName(uint8_t m) { case DISC_SORT_RECENT: return "Recently heard"; case DISC_SORT_AZ: return "Name (A-Z)"; case DISC_SORT_DISTANCE: return "Nearest first"; + case DISC_SORT_HOPS: return "Fewest hops"; } return "?"; } @@ -14186,6 +14191,14 @@ static bool discSortBefore(int ia, int ib, double self_lat, double self_lon) { if (da < db) prim = -1; else if (da > db) prim = 1; break; } + case DISC_SORT_HOPS: { + // Unknown path sinks to the end whichever way the list is flipped. + const bool ua = (A.path_len == OUT_PATH_UNKNOWN), ub = (B.path_len == OUT_PATH_UNKNOWN); + if (ua != ub) return ub; + const uint8_t ha = pathHops(A.path_len), hb = pathHops(B.path_len); + if (ha != hb) prim = (ha < hb) ? -1 : 1; + break; + } case DISC_SORT_AZ: prim = strcasecmp(A.ci.name, B.ci.name); break; @@ -32663,7 +32676,7 @@ static lv_obj_t* s_ct_sort_sheet = nullptr; // Sort & filter overlay static lv_obj_t* s_ct_normal_bar = nullptr; // header toolbar shown in normal mode static lv_obj_t* s_ct_select_bar = nullptr; // header toolbar shown in select mode static lv_obj_t* s_ct_del_btn = nullptr; // "Delete (N)" button in the select bar -static lv_obj_t* s_ct_sort_btns[CONTACTS_SORT_COUNT] = { nullptr, nullptr, nullptr, nullptr }; +static lv_obj_t* s_ct_sort_btns[CONTACTS_SORT_COUNT] = {}; static lv_obj_t* s_ct_filter_btns[6] = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; static lv_obj_t* s_ct_filter_btn_lbl = nullptr; // header Filter button label (shows the active filter) static lv_obj_t* s_ct_disc_badge = nullptr; // count badge on the header Discovered button @@ -33059,6 +33072,7 @@ static void openContactsSortSheet(){ s_ct_sort_btns[1] = ctSheetOption(card, contactsSortOptName(CONTACTS_SORT_LAST_HEARD), ctSortOptCb, CONTACTS_SORT_LAST_HEARD, false); s_ct_sort_btns[2] = ctSheetOption(card, contactsSortOptName(CONTACTS_SORT_LAST_MSG), ctSortOptCb, CONTACTS_SORT_LAST_MSG, false); s_ct_sort_btns[3] = ctSheetOption(card, contactsSortOptName(CONTACTS_SORT_DISTANCE), ctSortOptCb, CONTACTS_SORT_DISTANCE, false); + s_ct_sort_btns[4] = ctSheetOption(card, contactsSortOptName(CONTACTS_SORT_HOPS), ctSortOptCb, CONTACTS_SORT_HOPS, false); ctSortRowsRepaint(); // tap the active row again to flip ascending/descending lv_obj_t* sp = lv_obj_create(card); lv_obj_remove_style_all(sp); lv_obj_set_size(sp, 1, 4); ctSheetOption(card, LV_SYMBOL_TRASH " Select to delete", ctSelectFromSheetCb, 0, false); @@ -45795,6 +45809,13 @@ static void refreshContactsList() { const double db = contactDistanceKm(s_ct_sort_self_lat, s_ct_sort_self_lon, eb->gps_lat / 1.0e6, eb->gps_lon / 1.0e6); if (da < db) prim = -1; else if (da > db) prim = 1; + } else if (g_contacts_sort == CONTACTS_SORT_HOPS) { + // Fewest hops first; a row with no known path (shows no hop count) sinks to + // the end whichever way the list is flipped. + const bool ua = (ea->out_path_len == OUT_PATH_UNKNOWN), ub = (eb->out_path_len == OUT_PATH_UNKNOWN); + if (ua != ub) return ua ? 1 : -1; + const uint8_t ha = pathHops(ea->out_path_len), hb = pathHops(eb->out_path_len); + if (ha != hb) prim = (ha < hb) ? -1 : 1; } else if (g_contacts_sort == CONTACTS_SORT_LAST_HEARD || g_contacts_sort == CONTACTS_SORT_LAST_MSG) { // Contacts whose last-heard renders as "?" sink to the end of the time-based