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_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
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
// 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.
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
// 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_melody_item[20];
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
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
@@ -271,14 +272,16 @@ class QuickMsgScreen : public UIScreen {
}
void buildChannelList() {
NodePrefs* p = _task->getNodePrefs();
bool fav_only = (p && p->ch_fav_only);
_num_channels = 0;
for (int i = 0; i < MAX_GROUP_CHANNELS; i++) {
ChannelDetails ch;
if (the_mesh.getChannel(i, ch) && ch.name[0] != '\0') {
if (!the_mesh.getChannel(i, ch) || ch.name[0] == '\0') continue;
if (fav_only && !(p->ch_fav_bitmask & (1ULL << i))) continue;
_channel_indices[_num_channels++] = (uint8_t)i;
}
}
}
// Returns per-channel notification state: 0=follow global, 1=muted, 2=force-on
uint8_t chNotifState(uint8_t ch_idx) const {
@@ -1083,10 +1086,19 @@ public:
uint8_t nstate = chNotifState(ch_idx);
setChNotifState(ch_idx, (nstate + 1) % 3);
_ctx_dirty = true;
} else {
} else if (_ctx_menu.selectedIndex() == 2) {
uint8_t slot = chNotifMelody(ch_idx);
setChNotifMelody(ch_idx, (slot + 1) % 3);
_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) {
@@ -1124,10 +1136,14 @@ public:
{ static const char* ML[] = { "global", "M1", "M2" };
snprintf(_ctx_melody_item, sizeof(_ctx_melody_item), "Melody: %s",
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(_ctx_notif_item);
_ctx_menu.addItem(_ctx_melody_item);
_ctx_menu.addItem(_ctx_ch_fav_item);
_ctx_dirty = false;
return true;
}

View File

@@ -57,7 +57,7 @@ class SettingsScreen : public UIScreen {
TIMEZONE,
LOW_BAT,
// Contacts section
SECTION_CONTACTS, DM_FILTER, ROOM_FILTER,
SECTION_CONTACTS, DM_FILTER, CH_FILTER, ROOM_FILTER,
// Messages section
SECTION_MESSAGES,
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.setCursor(display.valCol(), y);
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) {
display.print("Rooms");
display.setCursor(display.valCol(), y);
@@ -783,6 +787,11 @@ public:
_dirty = 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)) {
p->room_fav_only = p->room_fav_only ? 0 : 1;
_dirty = true;