mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-09 11:16:39 +00:00
- Unlocking fades the lock screen out while the screen underneath drifts into place; the slider unlocks on letting go at the end and springs back when let go earlier - Channel names no longer get a "# " prefix (thread title, channel popup, live share targets) -- "#" marks a hashtag channel; favourite contacts in the live share list are starred instead Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
343 lines
14 KiB
C++
343 lines
14 KiB
C++
#pragma once
|
|
// Channel management -- ui-new's Messages > Channels options and Add / Edit
|
|
// form. Options popup (hold a channel row, or the ⚙ in an open channel):
|
|
// notifications, scope, favourite, edit, delete. Add channel: Public (the
|
|
// well-known channel), Hashtag ("#topic", secret derived from the name) or
|
|
// Private (name + passphrase or 32-hex key). Edit: new name, secret kept unless
|
|
// a new one is typed. The logic is ui-core/ChannelControl.h, shared with ui-new.
|
|
//
|
|
// Single-TU fragment: included by ui-lvgl/UITask.cpp after RadioScreen.h.
|
|
|
|
namespace chanview {
|
|
|
|
enum : uint8_t { A_FAV, A_EDIT, A_DELETE, A_READ, A_PIN };
|
|
enum : uint8_t { T_PUBLIC, T_HASHTAG, T_PRIVATE };
|
|
enum : uint8_t { C_NOTIF, C_SCOPE, C_MELODY };
|
|
|
|
static int s_idx = -1; // channel the popup / form is about (-1: adding)
|
|
static uint8_t s_type = T_HASHTAG;
|
|
static bool s_hex = false;
|
|
static uint32_t s_del_armed_ms = 0;
|
|
static lv_obj_t* s_del_lbl = nullptr;
|
|
static lv_obj_t* s_fav_btn = nullptr;
|
|
// Add / Edit form
|
|
static lv_obj_t* s_types = nullptr;
|
|
static lv_obj_t* s_name = nullptr;
|
|
static lv_obj_t* s_secret = nullptr;
|
|
static lv_obj_t* s_hex_sw = nullptr;
|
|
static lv_obj_t* s_hint = nullptr;
|
|
static lv_obj_t* s_status = nullptr;
|
|
static lv_obj_t* s_kb = nullptr;
|
|
static char s_scope_opts[256];
|
|
|
|
static void clearForm() {
|
|
s_types = s_name = s_secret = s_hex_sw = s_hint = s_status = s_kb = nullptr;
|
|
}
|
|
|
|
} // namespace chanview
|
|
|
|
static void onChanAction(lv_event_t* e) { s_ui->channelAction((uint8_t)(uintptr_t)lv_event_get_user_data(e)); }
|
|
static void onChanDropdown(lv_event_t* e) {
|
|
s_ui->channelSet((uint8_t)(uintptr_t)lv_event_get_user_data(e),
|
|
(int)lv_dropdown_get_selected((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onChanSegment(lv_event_t* e) { // user data: C_NOTIF / C_MELODY
|
|
s_ui->channelSet((uint8_t)(uintptr_t)lv_event_get_user_data(e),
|
|
(int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onChanRowHold(lv_event_t* e) {
|
|
lv_indev_wait_release(lv_indev_active()); // the hold isn't also a tap that opens the channel
|
|
s_ui->channelMenu((int)(uintptr_t)lv_event_get_user_data(e));
|
|
}
|
|
static void onChanThreadMenu(lv_event_t* e) { (void)e; s_ui->channelMenu(-1); }
|
|
static void onChanAdd(lv_event_t* e) { (void)e; s_ui->showChannelEdit(-1); }
|
|
static void onChanType(lv_event_t* e) {
|
|
s_ui->channelEditType((int)lv_buttonmatrix_get_selected_button((lv_obj_t*)lv_event_get_target(e)));
|
|
}
|
|
static void onChanHex(lv_event_t* e) {
|
|
s_ui->channelEditHex(lv_obj_has_state((lv_obj_t*)lv_event_get_target(e), LV_STATE_CHECKED));
|
|
}
|
|
static void onChanField(lv_event_t* e) { s_ui->channelEditField((lv_obj_t*)lv_event_get_target(e)); }
|
|
static void onChanSave(lv_event_t* e) { (void)e; s_ui->channelEditSave(); }
|
|
static void onChanKb(lv_event_t* e) {
|
|
if (lv_event_get_code(e) == LV_EVENT_READY) s_ui->channelEditSave();
|
|
else s_ui->channelEditKbHide();
|
|
}
|
|
|
|
// ── Options popup ─────────────────────────────────────────────────────────────
|
|
|
|
// idx -1: the channel open in the thread.
|
|
void UITask::channelMenu(int idx) {
|
|
using namespace chanview;
|
|
if (idx < 0) idx = _thread_channel;
|
|
ChannelDetails ch;
|
|
if (!chanctl::exists(idx) || !the_mesh.getChannel(idx, ch)) return;
|
|
s_idx = idx;
|
|
s_del_armed_ms = 0;
|
|
s_del_lbl = s_fav_btn = nullptr;
|
|
char title[40];
|
|
snprintf(title, sizeof(title), "%s", ch.name); // as named: "#" marks a hashtag channel
|
|
lv_obj_t* panel = navPopupPanel(title, false);
|
|
|
|
// Notifications: three segments in the row (a dropdown's list ran off the
|
|
// bottom of the screen from a popup this low).
|
|
static const char* NOTIF[] = { "Default", "Muted", "Always", "" };
|
|
static const char* MELODY[] = { "Default", "Melody 1", "Melody 2", "" };
|
|
lv_obj_t* row = radioview::settingRow(panel, "Alerts", NULL);
|
|
radioview::rowSegmented(row, NOTIF, chanctl::notif(_prefs, idx), 200, onChanSegment, C_NOTIF);
|
|
row = radioview::settingRow(panel, "Sound", NULL);
|
|
radioview::rowSegmented(row, MELODY, chanctl::melody(_prefs, idx), 200, onChanSegment, C_MELODY);
|
|
const ScopeList& sl = the_mesh.scopeList();
|
|
if (sl.count > 0) { // only once regions are set up (Settings in the app)
|
|
row = radioview::settingRow(panel, "Scope", "Region it's sent in");
|
|
int o = 0;
|
|
for (uint8_t i = 0; i <= sl.count && o < (int)sizeof(s_scope_opts) - 24; i++)
|
|
o += snprintf(s_scope_opts + o, sizeof(s_scope_opts) - o, "%s%s", i ? "\n" : "", sl.name(i));
|
|
lv_obj_t* dd = radioview::rowDropdown(row, s_scope_opts, chanctl::scope(_prefs, idx), 112, onChanDropdown, C_SCOPE);
|
|
lv_obj_set_style_max_height(lv_dropdown_get_list(dd), 100, 0); // stays on screen; scrolls if long
|
|
}
|
|
|
|
lv_obj_t* acts = lv_obj_create(panel);
|
|
styleSurface(acts, theme::BG);
|
|
lv_obj_remove_flag(acts, LV_OBJ_FLAG_SCROLLABLE);
|
|
lv_obj_set_size(acts, LV_PCT(100), LV_SIZE_CONTENT);
|
|
lv_obj_set_flex_flow(acts, LV_FLEX_FLOW_ROW);
|
|
lv_obj_set_style_pad_column(acts, theme::GAP, 0);
|
|
bool unread = _core->history.chUnread(idx) > 0;
|
|
struct { const char* text; uint8_t act; } btns[] = {
|
|
{ UI_SYMBOL_STAR " Fav", A_FAV },
|
|
{ LV_SYMBOL_OK " Read", A_READ },
|
|
{ UI_SYMBOL_PIN, A_PIN },
|
|
{ LV_SYMBOL_EDIT " Edit", A_EDIT },
|
|
{ LV_SYMBOL_TRASH, A_DELETE },
|
|
};
|
|
for (auto& b : btns) {
|
|
if (b.act == A_READ && !unread) continue;
|
|
lv_obj_t* bt = lv_button_create(acts);
|
|
lv_obj_set_height(bt, 40);
|
|
lv_obj_set_flex_grow(bt, 1);
|
|
lv_obj_set_style_pad_hor(bt, 4, 0);
|
|
lv_obj_set_style_radius(bt, theme::RADIUS, 0);
|
|
lv_obj_set_style_shadow_width(bt, 0, 0);
|
|
lv_obj_set_style_bg_color(bt, lv_color_hex(theme::SURFACE), 0);
|
|
lv_obj_add_event_cb(bt, onChanAction, LV_EVENT_CLICKED, (void*)(uintptr_t)b.act);
|
|
lv_obj_t* l = label(bt, b.text, THEME_FONT_SMALL, theme::TEXT);
|
|
lv_obj_center(l);
|
|
if (b.act == A_DELETE) s_del_lbl = l;
|
|
if (b.act == A_FAV) s_fav_btn = bt;
|
|
}
|
|
if (s_fav_btn && chanctl::favourite(_prefs, idx))
|
|
lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(theme::ACCENT_DIM), 0);
|
|
}
|
|
|
|
void UITask::channelSet(uint8_t which, int v) {
|
|
using namespace chanview;
|
|
if (!chanctl::exists(s_idx) || !_prefs) return;
|
|
if (which == C_NOTIF) chanctl::setNotif(_prefs, s_idx, (uint8_t)v);
|
|
else if (which == C_SCOPE) the_mesh.setChannelScope(s_idx, (uint8_t)v);
|
|
else if (which == C_MELODY) { chanctl::setMelody(_prefs, s_idx, (uint8_t)v); hearMelody(v); }
|
|
the_mesh.savePrefs();
|
|
}
|
|
|
|
void UITask::channelAction(uint8_t act) {
|
|
using namespace chanview;
|
|
int idx = s_idx;
|
|
if (!chanctl::exists(idx)) { navClosePopup(); return; }
|
|
switch (act) {
|
|
case A_FAV: {
|
|
bool on = !chanctl::favourite(_prefs, idx);
|
|
chanctl::setFavourite(_prefs, idx, on);
|
|
the_mesh.savePrefs();
|
|
if (s_fav_btn) lv_obj_set_style_bg_color(s_fav_btn, lv_color_hex(on ? theme::ACCENT_DIM : theme::SURFACE), 0);
|
|
showToast(on ? "Added to favourites" : "Removed from favourites", 1200);
|
|
if (_screen == SCR_CHATS) { buildChats(); channelMenu(idx); } // list order / star
|
|
break;
|
|
}
|
|
case A_READ:
|
|
_core->history.setChUnread(idx, 0);
|
|
navClosePopup();
|
|
if (_screen == SCR_CHATS) buildChats();
|
|
break;
|
|
case A_EDIT:
|
|
navClosePopup();
|
|
showChannelEdit(idx);
|
|
break;
|
|
case A_PIN:
|
|
pinPopup(true, (uint8_t)idx, nullptr); // DeviceScreen.h
|
|
break;
|
|
case A_DELETE:
|
|
if (!s_del_armed_ms || millis() - s_del_armed_ms > 3000) { // second tap within 3 s confirms
|
|
s_del_armed_ms = millis() | 1;
|
|
if (s_del_lbl) lv_label_set_text(s_del_lbl, "Delete?");
|
|
break;
|
|
}
|
|
chanctl::remove(idx);
|
|
navClosePopup();
|
|
showToast("Channel deleted");
|
|
showChats();
|
|
break;
|
|
}
|
|
}
|
|
|
|
// ── Add / Edit ────────────────────────────────────────────────────────────────
|
|
|
|
static lv_obj_t* chanField(lv_obj_t* parent, const char* placeholder) {
|
|
lv_obj_t* ta = wifiField(parent, placeholder, false);
|
|
lv_obj_remove_event_cb(ta, onWifiField);
|
|
lv_obj_add_event_cb(ta, onChanField, LV_EVENT_CLICKED, NULL);
|
|
return ta;
|
|
}
|
|
|
|
void UITask::showChannelEdit(int idx) {
|
|
_screen = SCR_CHANNEL_EDIT;
|
|
chanview::s_idx = chanctl::exists(idx) ? idx : -1;
|
|
chanview::s_hex = false;
|
|
buildChannelEdit();
|
|
}
|
|
|
|
void UITask::buildChannelEdit() {
|
|
using namespace chanview;
|
|
bool edit = s_idx >= 0;
|
|
lv_obj_t* body = newScreen(edit ? "Edit channel" : "Add channel", true);
|
|
lv_obj_set_style_pad_row(body, 4, 0);
|
|
clearForm();
|
|
|
|
if (!edit) {
|
|
static const char* TYPES[] = { "Public", "Hashtag", "Private", "" };
|
|
s_types = segmented(body, TYPES, s_type, LV_PCT(100), 38, theme::SURFACE);
|
|
lv_obj_set_style_text_font(s_types, THEME_FONT_BODY, LV_PART_ITEMS);
|
|
lv_obj_add_event_cb(s_types, onChanType, LV_EVENT_VALUE_CHANGED, NULL);
|
|
}
|
|
|
|
s_hint = label(body, "", THEME_FONT_SMALL, theme::TEXT_MUTED);
|
|
lv_label_set_long_mode(s_hint, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(s_hint, LV_PCT(100));
|
|
s_name = chanField(body, "Name");
|
|
lv_textarea_set_max_length(s_name, sizeof(((ChannelDetails*)0)->name) - 1);
|
|
s_secret = chanField(body, "Passphrase");
|
|
lv_textarea_set_max_length(s_secret, 32);
|
|
|
|
lv_obj_t* row = radioview::settingRow(body, "Hex key", "32 characters");
|
|
s_hex_sw = lv_switch_create(row);
|
|
lv_obj_set_size(s_hex_sw, 46, 24);
|
|
lv_obj_align(s_hex_sw, LV_ALIGN_RIGHT_MID, -theme::PAD, 0);
|
|
lv_obj_add_event_cb(s_hex_sw, onChanHex, LV_EVENT_VALUE_CHANGED, NULL);
|
|
|
|
lv_obj_t* save = lv_button_create(body);
|
|
lv_obj_set_size(save, LV_PCT(100), 38);
|
|
lv_obj_set_style_shadow_width(save, 0, 0);
|
|
lv_obj_set_style_radius(save, theme::RADIUS, 0);
|
|
lv_obj_set_style_bg_color(save, lv_color_hex(theme::ACCENT_DIM), 0);
|
|
lv_obj_add_event_cb(save, onChanSave, LV_EVENT_CLICKED, NULL);
|
|
lv_obj_center(label(save, LV_SYMBOL_OK " Save", THEME_FONT_BODY, theme::TEXT));
|
|
stylePrimary(save);
|
|
s_status = label(body, "", THEME_FONT_SMALL, theme::FAIL);
|
|
lv_label_set_long_mode(s_status, LV_LABEL_LONG_WRAP);
|
|
lv_obj_set_width(s_status, LV_PCT(100));
|
|
|
|
if (edit) {
|
|
ChannelDetails ch;
|
|
char hex[33] = "";
|
|
the_mesh.getChannel(s_idx, ch);
|
|
chanctl::secretHex(s_idx, hex);
|
|
lv_textarea_set_text(s_name, ch.name);
|
|
lv_textarea_set_placeholder_text(s_secret, "New passphrase (empty: keep)");
|
|
lv_label_set_text_fmt(s_hint, "Key: %s", hex);
|
|
}
|
|
|
|
s_kb = kb::create(screen(), _prefs);
|
|
lv_obj_set_size(s_kb, LV_PCT(100), 124);
|
|
lv_obj_align(s_kb, LV_ALIGN_BOTTOM_MID, 0, 0);
|
|
lv_obj_add_event_cb(s_kb, onChanKb, LV_EVENT_READY, NULL);
|
|
lv_obj_add_event_cb(s_kb, onChanKb, LV_EVENT_CANCEL, NULL);
|
|
lv_obj_add_flag(s_kb, LV_OBJ_FLAG_HIDDEN);
|
|
channelEditType(edit ? T_PRIVATE : s_type);
|
|
}
|
|
|
|
// Show the fields the channel type needs.
|
|
void UITask::channelEditType(int type) {
|
|
using namespace chanview;
|
|
if (!s_name || type < T_PUBLIC || type > T_PRIVATE) return;
|
|
bool edit = s_idx >= 0;
|
|
if (!edit) s_type = (uint8_t)type;
|
|
channelEditKbHide();
|
|
auto show = [](lv_obj_t* o, bool on) {
|
|
if (on) lv_obj_remove_flag(o, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(o, LV_OBJ_FLAG_HIDDEN);
|
|
};
|
|
show(s_name, type != T_PUBLIC);
|
|
show(s_secret, type == T_PRIVATE);
|
|
show(lv_obj_get_parent(s_hex_sw), type == T_PRIVATE);
|
|
lv_label_set_text(s_status, "");
|
|
if (edit) return; // the hint shows the key
|
|
switch (type) {
|
|
case T_PUBLIC:
|
|
lv_label_set_text(s_hint, "The open channel every MeshCore device starts with. Adds it back if you removed it.");
|
|
break;
|
|
case T_HASHTAG:
|
|
lv_textarea_set_placeholder_text(s_name, "Topic, e.g. hiking");
|
|
lv_label_set_text(s_hint, "A public group anyone can join by typing the same topic. The key comes from the name.");
|
|
break;
|
|
default:
|
|
lv_textarea_set_placeholder_text(s_name, "Name");
|
|
lv_label_set_text(s_hint, "Readable only by people you give the key to.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void UITask::channelEditHex(bool hex) {
|
|
using namespace chanview;
|
|
s_hex = hex;
|
|
if (!s_secret) return;
|
|
lv_textarea_set_text(s_secret, ""); // a passphrase isn't a key and vice versa
|
|
lv_textarea_set_placeholder_text(s_secret, hex ? "32 hex characters" : (s_idx >= 0 ? "New passphrase (empty: keep)" : "Passphrase"));
|
|
lv_textarea_set_accepted_chars(s_secret, hex ? "0123456789abcdefABCDEF" : NULL);
|
|
}
|
|
|
|
void UITask::channelEditField(lv_obj_t* ta) {
|
|
using namespace chanview;
|
|
if (!s_kb) return;
|
|
lv_obj_remove_state(s_name, LV_STATE_FOCUSED);
|
|
lv_obj_remove_state(s_secret, LV_STATE_FOCUSED);
|
|
lv_obj_add_state(ta, LV_STATE_FOCUSED); // draws the cursor
|
|
lv_keyboard_set_textarea(s_kb, ta);
|
|
if (lv_obj_has_flag(s_kb, LV_OBJ_FLAG_HIDDEN)) {
|
|
lv_obj_remove_flag(s_kb, LV_OBJ_FLAG_HIDDEN);
|
|
if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) - lv_obj_get_height(s_kb));
|
|
}
|
|
lv_obj_update_layout(screen());
|
|
lv_obj_scroll_to_view(ta, LV_ANIM_OFF);
|
|
}
|
|
|
|
void UITask::channelEditKbHide() {
|
|
using namespace chanview;
|
|
if (!s_kb || lv_obj_has_flag(s_kb, LV_OBJ_FLAG_HIDDEN)) return;
|
|
lv_obj_add_flag(s_kb, LV_OBJ_FLAG_HIDDEN);
|
|
lv_obj_remove_state(s_name, LV_STATE_FOCUSED);
|
|
lv_obj_remove_state(s_secret, LV_STATE_FOCUSED);
|
|
if (_body) lv_obj_set_height(_body, lv_obj_get_height(_body) + lv_obj_get_height(s_kb));
|
|
}
|
|
|
|
void UITask::channelEditSave() {
|
|
using namespace chanview;
|
|
if (!s_name) return;
|
|
const char* name = lv_textarea_get_text(s_name);
|
|
const char* secret = lv_textarea_get_text(s_secret);
|
|
chanctl::Result r;
|
|
if (s_idx >= 0)
|
|
r = secret[0] ? chanctl::savePrivate(s_idx, name, secret, s_hex) : chanctl::rename(s_idx, name);
|
|
else if (s_type == T_PUBLIC) r = chanctl::addPublic();
|
|
else if (s_type == T_HASHTAG) r = chanctl::addHashtag(name);
|
|
else r = chanctl::savePrivate(-1, name, secret, s_hex);
|
|
if (r != chanctl::OK) { // say so over the keyboard, and put the cursor where the fix goes
|
|
const char* msg = (r == chanctl::NO_NAME && s_type == T_HASHTAG && s_idx < 0) ? "Topic required" : chanctl::resultText(r);
|
|
lv_label_set_text(s_status, msg);
|
|
showToast(msg, 2000);
|
|
if (r == chanctl::NO_NAME) channelEditField(s_name);
|
|
else if (r == chanctl::NO_SECRET || r == chanctl::BAD_SECRET) channelEditField(s_secret);
|
|
return;
|
|
}
|
|
channelEditKbHide();
|
|
showToast(s_idx >= 0 ? "Channel updated" : "Channel added", 1500);
|
|
showChats();
|
|
}
|