#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), choiceSelected((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); lv_obj_t* g = group(panel, nullptr); choiceRow(g, "Alerts", nullptr, "Default\nMuted\nAlways", chanctl::notif(_prefs, idx), onChanDropdown, (void*)(uintptr_t)C_NOTIF); choiceRow(g, "Sound", nullptr, "Default\nMelody 1\nMelody 2", chanctl::melody(_prefs, idx), onChanDropdown, (void*)(uintptr_t)C_MELODY); const ScopeList& sl = the_mesh.scopeList(); if (sl.count > 0) { // only once regions are set up (Settings in the app) 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)); choiceRow(g, "Scope", "Region it's sent in", s_scope_opts, chanctl::scope(_prefs, idx), onChanDropdown, (void*)(uintptr_t)C_SCOPE); } 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 = 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(); }