feat(contacts): channel favourites — filter in Settings + context menu

Add per-channel favourite marking and a Settings filter (all/fav) for
the channel list, symmetric with the existing DM and Room filters.

NodePrefs:
- ch_fav_bitmask (uint64_t): bit i = channel i is marked favourite
- ch_fav_only (uint8_t): 0=show all channels, 1=favourites only
- SCHEMA_SENTINEL bumped to 0xC0DE0005

DataStore: save/load both new fields before the sentinel.

SettingsScreen: new CH_FILTER item ('Channels: all/fav') in the
Contacts section, positioned between DM_FILTER and ROOM_FILTER.

QuickMsgScreen:
- buildChannelList() skips non-favourite channels when ch_fav_only=1
- Channel context menu gains a 4th option 'Fav'/'Unfav' that toggles
  ch_fav_bitmask for the selected channel and rebuilds the list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-05-27 17:33:52 +02:00
parent dd9c75d4f4
commit 5b40e91df3
3 changed files with 35 additions and 7 deletions

View File

@@ -105,11 +105,14 @@ struct NodePrefs { // persisted to file
uint8_t trail_min_delta_idx; // indexes TrailStore::minDeltaMeters (5/10/25/100 m) uint8_t trail_min_delta_idx; // indexes TrailStore::minDeltaMeters (5/10/25/100 m)
uint8_t trail_units_idx; // indexes TrailStore::unitLabel (km/h, mph, min/km, min/mi) uint8_t trail_units_idx; // indexes TrailStore::unitLabel (km/h, mph, min/km, min/mi)
uint64_t ch_fav_bitmask; // bit i = channel i is marked as favourite
uint8_t ch_fav_only; // 0=show all channels (default), 1=show favourites only
// Tail sentinel written at the end of /new_prefs. Bump the low byte when // Tail sentinel written at the end of /new_prefs. Bump the low byte when
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so // adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
// older saves are detected on load and skipped (zero-init defaults kept). // older saves are detected on load and skipped (zero-init defaults kept).
// High 24 bits identify the file format; low byte is the schema revision. // High 24 bits identify the file format; low byte is the schema revision.
static const uint32_t SCHEMA_SENTINEL = 0xC0DE0004; static const uint32_t SCHEMA_SENTINEL = 0xC0DE0005;
// Bit-index for each home page. Used by page_order (entries store bit+1) and // Bit-index for each home page. Used by page_order (entries store bit+1) and
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage // by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage

View File

@@ -47,6 +47,7 @@ class QuickMsgScreen : public UIScreen {
char _ctx_notif_item[22]; char _ctx_notif_item[22];
char _ctx_melody_item[20]; char _ctx_melody_item[20];
char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)" char _ctx_pin_item[28]; // "Pin to dial" or "Unpin (slot N)"
char _ctx_ch_fav_item[12]; // "Fav" or "Unfav"
char _pin_slot_labels[NodePrefs::FAVOURITES_COUNT][22]; // per-slot picker labels char _pin_slot_labels[NodePrefs::FAVOURITES_COUNT][22]; // per-slot picker labels
bool _pin_picker_active; // true while the slot-picker submenu is open bool _pin_picker_active; // true while the slot-picker submenu is open
bool _dm_direct_entry; // entered DM_HIST via Favourites shortcut; CANCEL returns home bool _dm_direct_entry; // entered DM_HIST via Favourites shortcut; CANCEL returns home
@@ -271,12 +272,14 @@ class QuickMsgScreen : public UIScreen {
} }
void buildChannelList() { void buildChannelList() {
NodePrefs* p = _task->getNodePrefs();
bool fav_only = (p && p->ch_fav_only);
_num_channels = 0; _num_channels = 0;
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) { for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails ch; ChannelDetails ch;
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') { if (!the_mesh.getChannel(i, ch) || ch.name[0] == '\0') continue;
_channel_indices[_num_channels++] = (uint8_t)i; if (fav_only && !(p->ch_fav_bitmask & (1ULL << i))) continue;
} _channel_indices[_num_channels++] = (uint8_t)i;
} }
} }
@@ -1083,10 +1086,19 @@ public:
uint8_t nstate = chNotifState(ch_idx); uint8_t nstate = chNotifState(ch_idx);
setChNotifState(ch_idx, (nstate + 1) % 3); setChNotifState(ch_idx, (nstate + 1) % 3);
_ctx_dirty = true; _ctx_dirty = true;
} else { } else if (_ctx_menu.selectedIndex() == 2) {
uint8_t slot = chNotifMelody(ch_idx); uint8_t slot = chNotifMelody(ch_idx);
setChNotifMelody(ch_idx, (slot + 1) % 3); setChNotifMelody(ch_idx, (slot + 1) % 3);
_ctx_dirty = true; _ctx_dirty = true;
} else {
// Toggle favourite
NodePrefs* p2 = _task->getNodePrefs();
if (p2) {
p2->ch_fav_bitmask ^= (1ULL << ch_idx);
_ctx_dirty = true;
buildChannelList(); // refilter if ch_fav_only is active
if (_channel_sel >= _num_channels) _channel_sel = _num_channels > 0 ? _num_channels - 1 : 0;
}
} }
} }
if (res != PopupMenu::NONE && _ctx_dirty) { if (res != PopupMenu::NONE && _ctx_dirty) {
@@ -1124,10 +1136,14 @@ public:
{ static const char* ML[] = { "global", "M1", "M2" }; { static const char* ML[] = { "global", "M1", "M2" };
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s", snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s",
ML[chNotifMelody(ch_idx)]); } ML[chNotifMelody(ch_idx)]); }
_ctx_menu.begin("Channel options", 3); { NodePrefs* p2 = _task->getNodePrefs();
bool is_fav = p2 && (p2->ch_fav_bitmask & (1ULL << ch_idx));
snprintf(_ctx_ch_fav_item, sizeof(_ctx_ch_fav_item), is_fav ? "Unfav" : "Fav"); }
_ctx_menu.begin("Channel options", 4);
_ctx_menu.addItem("Mark all read"); _ctx_menu.addItem("Mark all read");
_ctx_menu.addItem(_ctx_notif_item); _ctx_menu.addItem(_ctx_notif_item);
_ctx_menu.addItem(_ctx_melody_item); _ctx_menu.addItem(_ctx_melody_item);
_ctx_menu.addItem(_ctx_ch_fav_item);
_ctx_dirty = false; _ctx_dirty = false;
return true; return true;
} }

View File

@@ -57,7 +57,7 @@ class SettingsScreen : public UIScreen {
TIMEZONE, TIMEZONE,
LOW_BAT, LOW_BAT,
// Contacts section // Contacts section
SECTION_CONTACTS, DM_FILTER, ROOM_FILTER, SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER,
// Messages section // Messages section
SECTION_MESSAGES, SECTION_MESSAGES,
MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4, MSG_SLOT_0, MSG_SLOT_1, MSG_SLOT_2, MSG_SLOT_3, MSG_SLOT_4,
@@ -533,6 +533,10 @@ class SettingsScreen : public UIScreen {
display.print("DM"); display.print("DM");
display.setCursor(display.valCol(), y); display.setCursor(display.valCol(), y);
display.print((p && p->dm_show_all) ? "all" : "fav"); display.print((p && p->dm_show_all) ? "all" : "fav");
} else if (item == CH_FILTER) {
display.print("Channels");
display.setCursor(display.valCol(), y);
display.print((p && p->ch_fav_only) ? "fav" : "all");
} else if (item == ROOM_FILTER) { } else if (item == ROOM_FILTER) {
display.print("Rooms"); display.print("Rooms");
display.setCursor(display.valCol(), y); display.setCursor(display.valCol(), y);
@@ -783,6 +787,11 @@ public:
_dirty = true; _dirty = true;
return true; return true;
} }
if (_selected == CH_FILTER && p && (left || right || enter)) {
p->ch_fav_only = p->ch_fav_only ? 0 : 1;
_dirty = true;
return true;
}
if (_selected == ROOM_FILTER && p && (left || right || enter)) { if (_selected == ROOM_FILTER && p && (left || right || enter)) {
p->room_fav_only = p->room_fav_only ? 0 : 1; p->room_fav_only = p->room_fav_only ? 0 : 1;
_dirty = true; _dirty = true;