mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-07-26 14:58:12 +00:00
feat(ui): channel Add gets a Type picker (Public/Hashtag/Private)
"+ Add channel" jumped straight into a generic Name+Secret form, so the
"hashtag channel" convention documented in docs/companion_protocol.md
(secret = first 16 bytes of sha256("#topic")) was only reachable by
already knowing to type a literal "#topic" into the passphrase field --
nothing in the UI surfaced it. The phone app instead shows an explicit
channel-type picker; this adds the same on-device.
"+ Add channel" now asks Public / Hashtag / Private first:
- Public commits immediately with the well-known default channel's name
and secret (8b3387e9c5cdea6ac9e5edbaa115cd72, confirmed to match
MyMesh.cpp's PUBLIC_GROUP_PSK and the docs' published key) -- useful
to restore it if deleted.
- Hashtag shows a single Topic field; Save synthesizes name="#topic" and
derives the secret via the existing SHA-256 passphrase path -- same
underlying mechanism Private already had, just discoverable without
knowing the "#" convention.
- Private is today's manual Name+Secret form, unchanged.
Editing an existing channel skips the picker (no ambiguity to resolve
there). Extracted hexToSecret() out of deriveSecret()'s hex-mode branch
so Public's fixed key parses through the same code instead of a second
hand-rolled loop. No other file needed changes -- openAdd()/openEdit()/
active()/render()/handleInput() keep their existing signatures.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,22 @@
|
||||
// Owned by MessagesScreen, which delegates render()/handleInput() to it while
|
||||
// active() — the same relationship WaypointsView has with TrailScreen.
|
||||
//
|
||||
// The Secret field toggles between two entry modes with LEFT/RIGHT:
|
||||
// "+ Add channel" starts with a Type picker (Public/Hashtag/Private), matching
|
||||
// the phone app's channel-type trichotomy (docs/companion_protocol.md's
|
||||
// "Channel Types") instead of exposing the raw Name+Secret form directly:
|
||||
// - Public: one tap re-adds the well-known default channel (same secret
|
||||
// MyMesh.cpp pre-configures at first boot) — no fields to fill in.
|
||||
// - Hashtag: type just a topic name; the channel name and secret are both
|
||||
// derived from it ("#topic", secret = first 16 bytes of sha256("#topic")).
|
||||
// A topic-based public group chat, discoverable without knowing the hash
|
||||
// convention exists.
|
||||
// - Private: today's manual Name + Secret form (see below) — a channel only
|
||||
// those you share the secret with can join.
|
||||
// Editing an existing channel (openEdit()) skips the Type picker and goes
|
||||
// straight to the Name+Secret form — there's no ambiguity to resolve there.
|
||||
//
|
||||
// The Secret field (Private, and internally for Public/Hashtag) toggles
|
||||
// between two entry modes with LEFT/RIGHT:
|
||||
// - Passphrase (default): any text, SHA-256'd down to 16 bytes — the same
|
||||
// primitive BaseChatMesh::addChannel()/setChannel() already use to derive
|
||||
// a channel's routing hash, just applied here to a typed phrase instead of
|
||||
@@ -19,17 +34,20 @@
|
||||
class ChannelsView {
|
||||
UITask* _task;
|
||||
|
||||
enum Mode : uint8_t { OFF, ADD, EDIT };
|
||||
enum Mode : uint8_t { OFF, TYPE_PICK, ADD, ADD_HASHTAG, EDIT };
|
||||
uint8_t _mode = OFF;
|
||||
int _idx = -1; // channel slot being added/edited
|
||||
|
||||
int _type_sel = 0; // TYPE_PICK cursor: 0=Public 1=Hashtag 2=Private
|
||||
|
||||
char _name[32] = "";
|
||||
bool _hex_mode = false; // false = passphrase, true = 32-hex-char raw key
|
||||
char _secret_text[33] = ""; // passphrase or hex string typed so far
|
||||
char _topic[31] = ""; // ADD_HASHTAG: topic without the leading '#'
|
||||
|
||||
int _sel = 0; // 0=Name 1=Secret 2=[Save]
|
||||
int _sel = 0; // 0=Name 1=Secret 2=[Save] (ADD_HASHTAG: 0=Topic 1=[Save])
|
||||
bool _kb_active = false;
|
||||
int _kb_field = -1; // 0=Name, 1=Secret
|
||||
int _kb_field = -1; // 0=Name, 1=Secret, 2=Topic
|
||||
|
||||
KeyboardWidget& kb() { return _task->keyboard(); }
|
||||
|
||||
@@ -39,31 +57,36 @@ class ChannelsView {
|
||||
_kb_active = true;
|
||||
}
|
||||
|
||||
// Parses exactly 32 hex chars into a 16-byte secret (zero-padded to 32 for
|
||||
// ChannelDetails.secret's full field width). Rejects malformed hex and the
|
||||
// all-zero sentinel setChannelLocal() reads as "slot deleted" (see
|
||||
// isAllZero() in MyMesh.cpp) -- saving one would pass, then immediately
|
||||
// trigger onChannelRemoved() on this very slot, silently discarding the
|
||||
// channel and its bot/notif/favourite state. Shared by deriveSecret()'s
|
||||
// hex-mode branch and the Public quick-add (a fixed, known-good constant,
|
||||
// but parsed through the same path rather than duplicated).
|
||||
static bool hexToSecret(const char* hex32, uint8_t out[32]) {
|
||||
if (strlen(hex32) != 32) return false;
|
||||
uint8_t tmp[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
char byte_str[3] = { hex32[i * 2], hex32[i * 2 + 1], 0 };
|
||||
char* end = nullptr;
|
||||
long v = strtol(byte_str, &end, 16);
|
||||
if (*end != 0) return false;
|
||||
tmp[i] = (uint8_t)v;
|
||||
}
|
||||
bool all_zero = true;
|
||||
for (int i = 0; i < 16 && all_zero; i++) if (tmp[i]) all_zero = false;
|
||||
if (all_zero) return false;
|
||||
memset(out, 0, 32);
|
||||
memcpy(out, tmp, 16);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Turn the typed Secret field into a 16-byte channel secret. Returns false
|
||||
// (and leaves out[] untouched) if the field can't be parsed as configured.
|
||||
bool deriveSecret(uint8_t out[32]) const {
|
||||
if (_hex_mode) {
|
||||
if (strlen(_secret_text) != 32) return false;
|
||||
uint8_t tmp[16];
|
||||
for (int i = 0; i < 16; i++) {
|
||||
char byte_str[3] = { _secret_text[i * 2], _secret_text[i * 2 + 1], 0 };
|
||||
char* end = nullptr;
|
||||
long v = strtol(byte_str, &end, 16);
|
||||
if (*end != 0) return false;
|
||||
tmp[i] = (uint8_t)v;
|
||||
}
|
||||
// An all-zero 16-byte secret is the sentinel setChannelLocal() reads as
|
||||
// "slot deleted" (see isAllZero() in MyMesh.cpp) -- saving one here would
|
||||
// pass, then immediately trigger onChannelRemoved() on this very slot,
|
||||
// silently discarding the channel and its bot/notif/favourite state.
|
||||
// Reject it up front rather than let that self-delete happen invisibly.
|
||||
bool all_zero = true;
|
||||
for (int i = 0; i < 16 && all_zero; i++) if (tmp[i]) all_zero = false;
|
||||
if (all_zero) return false;
|
||||
memset(out, 0, 32);
|
||||
memcpy(out, tmp, 16);
|
||||
return true;
|
||||
}
|
||||
if (_hex_mode) return hexToSecret(_secret_text, out);
|
||||
if (_secret_text[0] == '\0') return false; // blank passphrase not allowed
|
||||
uint8_t digest[32];
|
||||
mesh::Utils::sha256(digest, sizeof(digest), (const uint8_t*)_secret_text, (int)strlen(_secret_text));
|
||||
@@ -72,19 +95,48 @@ class ChannelsView {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool saveChannel(const char* name, const uint8_t secret[32]) {
|
||||
ChannelDetails ch;
|
||||
memset(&ch, 0, sizeof(ch));
|
||||
strncpy(ch.name, name, sizeof(ch.name) - 1);
|
||||
memcpy(ch.channel.secret, secret, sizeof(ch.channel.secret));
|
||||
return the_mesh.setChannelLocal((uint8_t)_idx, ch);
|
||||
}
|
||||
|
||||
// Type picker's Public row: no fields to fill in, so this commits directly
|
||||
// instead of dropping into the Name+Secret form -- the well-known channel's
|
||||
// name and secret are both fixed. Hex confirmed via
|
||||
// `python3 -c "import base64; print(base64.b64decode('izOH6cXN6mrJ5e26oRXNcg==').hex())"`
|
||||
// to match MyMesh.cpp's PUBLIC_GROUP_PSK (base64) and
|
||||
// docs/companion_protocol.md's documented public channel key (hex).
|
||||
void commitPublic() {
|
||||
static const char PUBLIC_SECRET_HEX[] = "8b3387e9c5cdea6ac9e5edbaa115cd72";
|
||||
uint8_t secret[32];
|
||||
hexToSecret(PUBLIC_SECRET_HEX, secret); // fixed, known-good constant -- can't fail
|
||||
if (saveChannel("Public", secret)) _task->showAlert("Channel added", 1000);
|
||||
else _task->showAlert("Save failed", 1200);
|
||||
_mode = OFF;
|
||||
}
|
||||
|
||||
void commit() {
|
||||
if (_mode == ADD_HASHTAG) {
|
||||
if (_topic[0] == '\0') { _task->showAlert("Topic required", 1200); return; }
|
||||
// "#topic" as both the channel's display name and the passphrase fed to
|
||||
// sha256 -- the exact "Hashtag Channels" convention in
|
||||
// docs/companion_protocol.md, just synthesized instead of hand-typed.
|
||||
snprintf(_name, sizeof(_name), "#%s", _topic);
|
||||
strncpy(_secret_text, _name, sizeof(_secret_text) - 1);
|
||||
_secret_text[sizeof(_secret_text) - 1] = '\0';
|
||||
_hex_mode = false;
|
||||
}
|
||||
if (_name[0] == '\0') { _task->showAlert("Name required", 1200); return; }
|
||||
uint8_t secret[32];
|
||||
if (!deriveSecret(secret)) {
|
||||
_task->showAlert(_hex_mode ? "Invalid secret" : "Secret required", 1400);
|
||||
return;
|
||||
}
|
||||
ChannelDetails ch;
|
||||
memset(&ch, 0, sizeof(ch));
|
||||
strncpy(ch.name, _name, sizeof(ch.name) - 1);
|
||||
memcpy(ch.channel.secret, secret, sizeof(ch.channel.secret));
|
||||
if (the_mesh.setChannelLocal((uint8_t)_idx, ch)) {
|
||||
_task->showAlert(_mode == ADD ? "Channel added" : "Channel updated", 1000);
|
||||
if (saveChannel(_name, secret)) {
|
||||
_task->showAlert((_mode == ADD || _mode == ADD_HASHTAG) ? "Channel added" : "Channel updated", 1000);
|
||||
_mode = OFF;
|
||||
} else {
|
||||
_task->showAlert("Save failed", 1200);
|
||||
@@ -95,14 +147,17 @@ public:
|
||||
explicit ChannelsView(UITask* task) : _task(task) {}
|
||||
|
||||
bool active() const { return _mode != OFF || _kb_active; }
|
||||
void reset() { _mode = OFF; _kb_active = false; _kb_field = -1; }
|
||||
void reset() { _mode = OFF; _kb_active = false; _kb_field = -1; _type_sel = 0; _topic[0] = '\0'; }
|
||||
|
||||
// idx = the slot to write on Save (first free slot for Add, existing slot
|
||||
// for Edit). Edit pre-fills Name; the Secret can't be redisplayed (only the
|
||||
// derived key is stored), so editing it means typing a new passphrase/hex —
|
||||
// same as re-logging into a room with a new password.
|
||||
// for Edit). Starts at the Type picker (Public/Hashtag/Private) -- see
|
||||
// handleInput()'s TYPE_PICK branch for what each option does next.
|
||||
void openAdd(int idx) {
|
||||
_mode = ADD; _idx = idx;
|
||||
_mode = TYPE_PICK; _idx = idx; _type_sel = 0;
|
||||
}
|
||||
// Private option's form: today's manual Name + Secret entry, unchanged.
|
||||
void openPrivate() {
|
||||
_mode = ADD;
|
||||
_name[0] = '\0'; _secret_text[0] = '\0'; _hex_mode = false; _sel = 0;
|
||||
}
|
||||
void openEdit(int idx, const char* existing_name) {
|
||||
@@ -116,9 +171,37 @@ public:
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
if (_kb_active) return kb().render(display);
|
||||
|
||||
display.drawCenteredHeader(_mode == ADD ? "ADD CHANNEL" : "EDIT CHANNEL");
|
||||
const int top = display.listStart();
|
||||
const int step = display.lineStep();
|
||||
|
||||
if (_mode == TYPE_PICK) {
|
||||
display.drawCenteredHeader("ADD CHANNEL");
|
||||
static const char* TYPE_LABELS[3] = { "Public", "Hashtag", "Private" };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int y = top + i * step;
|
||||
display.drawSelectionRow(0, y - 1, display.width(), step - 1, i == _type_sel);
|
||||
display.drawTextEllipsized(2, y, display.width() - 4, TYPE_LABELS[i]);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
return 1000;
|
||||
}
|
||||
|
||||
if (_mode == ADD_HASHTAG) {
|
||||
display.drawCenteredHeader("ADD CHANNEL");
|
||||
for (int i = 0; i < 2; i++) {
|
||||
int y = top + i * step;
|
||||
bool sel = (i == _sel);
|
||||
display.drawSelectionRow(0, y - 1, display.width(), step - 1, sel);
|
||||
char row[40];
|
||||
if (i == 0) snprintf(row, sizeof(row), "Topic: %s", _topic[0] ? _topic : "(none)");
|
||||
else snprintf(row, sizeof(row), "[Save]");
|
||||
display.drawTextEllipsized(2, y, display.width() - 4, row);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
}
|
||||
return 1000;
|
||||
}
|
||||
|
||||
display.drawCenteredHeader(_mode == ADD ? "ADD CHANNEL" : "EDIT CHANNEL");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int y = top + i * step;
|
||||
bool sel = (i == _sel);
|
||||
@@ -140,14 +223,39 @@ public:
|
||||
if (_kb_active) {
|
||||
auto r = kb().handleInput(c);
|
||||
if (r == KeyboardWidget::DONE) {
|
||||
if (_kb_field == 0) { strncpy(_name, kb().buf, sizeof(_name) - 1); _name[sizeof(_name) - 1] = '\0'; }
|
||||
else { strncpy(_secret_text, kb().buf, sizeof(_secret_text) - 1); _secret_text[sizeof(_secret_text) - 1] = '\0'; }
|
||||
if (_kb_field == 0) { strncpy(_name, kb().buf, sizeof(_name) - 1); _name[sizeof(_name) - 1] = '\0'; }
|
||||
else if (_kb_field == 1) { strncpy(_secret_text, kb().buf, sizeof(_secret_text) - 1); _secret_text[sizeof(_secret_text) - 1] = '\0'; }
|
||||
else { strncpy(_topic, kb().buf, sizeof(_topic) - 1); _topic[sizeof(_topic) - 1] = '\0'; }
|
||||
_kb_active = false; _kb_field = -1;
|
||||
} else if (r == KeyboardWidget::CANCELLED) {
|
||||
_kb_active = false; _kb_field = -1;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_mode == TYPE_PICK) {
|
||||
if (c == KEY_CANCEL) { _mode = OFF; return true; }
|
||||
if (c == KEY_UP) { _type_sel = (_type_sel > 0) ? _type_sel - 1 : 2; return true; }
|
||||
if (c == KEY_DOWN) { _type_sel = (_type_sel < 2) ? _type_sel + 1 : 0; return true; }
|
||||
if (c == KEY_ENTER) {
|
||||
if (_type_sel == 0) commitPublic();
|
||||
else if (_type_sel == 1) { _mode = ADD_HASHTAG; _sel = 0; _topic[0] = '\0'; }
|
||||
else openPrivate();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_mode == ADD_HASHTAG) {
|
||||
if (c == KEY_CANCEL) { _mode = OFF; return true; }
|
||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 1; return true; }
|
||||
if (c == KEY_DOWN) { _sel = (_sel < 1) ? _sel + 1 : 0; return true; }
|
||||
if (c == KEY_ENTER) {
|
||||
if (_sel == 0) { _kb_field = 2; openKb(_topic, sizeof(_topic) - 1); }
|
||||
else commit();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (c == KEY_CANCEL) { _mode = OFF; return true; }
|
||||
if (c == KEY_UP) { _sel = (_sel > 0) ? _sel - 1 : 2; return true; }
|
||||
if (c == KEY_DOWN) { _sel = (_sel < 2) ? _sel + 1 : 0; return true; }
|
||||
|
||||
Reference in New Issue
Block a user