mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-10-15 18:12:09 +00:00
feat(ui-core,ui-lvgl): room sessions and contact helpers in the Core, L2 rooms, conversation options, message actions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
// Conversation-level helpers shared by the messages screens (ui-new's
|
||||
// Messages, ui-lvgl's Chats / thread): per-contact notification and melody
|
||||
// overrides, the favourite flag, the reply prefix, splitting a room post into
|
||||
// author and text, and naming the hops of a message's path.
|
||||
|
||||
namespace contactctl {
|
||||
|
||||
// Per-contact overrides live in small {prefix[4], value} tables in NodePrefs:
|
||||
// value 0 = no override = free slot. Full table: slot 0 is overwritten.
|
||||
enum : uint8_t { NOTIF_DEFAULT = 0, NOTIF_MUTED = 1, NOTIF_ALWAYS = 2 };
|
||||
|
||||
template <class Entry>
|
||||
inline uint8_t tableGet(const Entry* tbl, int n, const uint8_t* pub_key, uint8_t Entry::* val) {
|
||||
for (int i = 0; i < n; i++)
|
||||
if (tbl[i].*val && memcmp(tbl[i].prefix, pub_key, 4) == 0) return tbl[i].*val;
|
||||
return 0;
|
||||
}
|
||||
template <class Entry>
|
||||
inline void tableSet(Entry* tbl, int n, const uint8_t* pub_key, uint8_t Entry::* val, uint8_t v) {
|
||||
for (int i = 0; i < n; i++)
|
||||
if (tbl[i].*val && memcmp(tbl[i].prefix, pub_key, 4) == 0) {
|
||||
if (v == 0) memset(&tbl[i], 0, sizeof(tbl[i])); else tbl[i].*val = v;
|
||||
return;
|
||||
}
|
||||
if (v == 0) return;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (tbl[i].*val == 0) { memcpy(tbl[i].prefix, pub_key, 4); tbl[i].*val = v; return; }
|
||||
memcpy(tbl[0].prefix, pub_key, 4); tbl[0].*val = v;
|
||||
}
|
||||
|
||||
inline uint8_t notif(const NodePrefs* p, const uint8_t* pub_key) {
|
||||
return p ? tableGet(p->dm_notif, NodePrefs::DM_NOTIF_TABLE_MAX, pub_key, &NodePrefs::DmNotifEntry::state) : 0;
|
||||
}
|
||||
inline void setNotif(NodePrefs* p, const uint8_t* pub_key, uint8_t state) {
|
||||
if (p) tableSet(p->dm_notif, NodePrefs::DM_NOTIF_TABLE_MAX, pub_key, &NodePrefs::DmNotifEntry::state, state);
|
||||
}
|
||||
// Melody slot: 0 = default, 1 / 2 = the user melodies.
|
||||
inline uint8_t melody(const NodePrefs* p, const uint8_t* pub_key) {
|
||||
return p ? tableGet(p->dm_melody, NodePrefs::DM_MELODY_TABLE_MAX, pub_key, &NodePrefs::DmMelodyEntry::slot) : 0;
|
||||
}
|
||||
inline void setMelody(NodePrefs* p, const uint8_t* pub_key, uint8_t slot) {
|
||||
if (p) tableSet(p->dm_melody, NodePrefs::DM_MELODY_TABLE_MAX, pub_key, &NodePrefs::DmMelodyEntry::slot, slot);
|
||||
}
|
||||
|
||||
// ContactInfo::flags bit 0, the same star the companion app sets.
|
||||
inline bool favourite(const ContactInfo& ci) { return (ci.flags & 0x01) != 0; }
|
||||
inline bool setFavourite(const uint8_t* pub_key, bool on) { return the_mesh.setContactFavourite(pub_key, on); }
|
||||
|
||||
// "@[nick] " -- how a reply names who it answers (raw UTF-8, over the air as is).
|
||||
inline void replyPrefix(const char* nick, char* out, size_t n) { snprintf(out, n, "@[%.31s] ", nick); }
|
||||
|
||||
// A room post is filed "Author: text" (UiCore::onMessageRecvEx). Author into
|
||||
// `author` ("" when there is none), returns the text after it.
|
||||
inline const char* splitRoomPost(const char* text, char* author, size_t n) {
|
||||
const char* sep = strstr(text, ": ");
|
||||
if (!sep || (size_t)(sep - text) >= n) { if (n) author[0] = '\0'; return text; }
|
||||
memcpy(author, text, sep - text);
|
||||
author[sep - text] = '\0';
|
||||
return sep + 2;
|
||||
}
|
||||
|
||||
// Hops recorded with a history entry: path_len packs hash size (top 2 bits,
|
||||
// minus 1) and hop count (low 6 bits).
|
||||
inline uint8_t hopCount(uint8_t path_len_packed) {
|
||||
uint8_t hash_size = (path_len_packed >> 6) + 1;
|
||||
uint8_t n = path_len_packed & 63;
|
||||
uint8_t max_hops = MAX_HIST_PATH_BYTES / hash_size; // what capturePath() stores at most
|
||||
return n > max_hops ? max_hops : n;
|
||||
}
|
||||
|
||||
// Hop i's name: the contact whose key starts with the hash, else "?AABB".
|
||||
inline void hopName(uint8_t path_len_packed, const uint8_t* path, int i, char* out, size_t n) {
|
||||
uint8_t hash_size = (path_len_packed >> 6) + 1;
|
||||
const uint8_t* hash = &path[i * hash_size];
|
||||
for (int idx = 0; ; idx++) {
|
||||
ContactInfo c;
|
||||
if (!the_mesh.getContactByIdx(idx, c)) break;
|
||||
if (c.id.isHashMatch(hash, hash_size)) { snprintf(out, n, "%s", c.name); return; }
|
||||
}
|
||||
char hex[9] = {0};
|
||||
uint8_t b = hash_size > 4 ? 4 : hash_size;
|
||||
for (uint8_t k = 0; k < b; k++) snprintf(hex + k * 2, 3, "%02X", hash[k]);
|
||||
snprintf(out, n, "?%s", hex);
|
||||
}
|
||||
|
||||
} // namespace contactctl
|
||||
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
// Room server sessions for the on-device messages screens (ui-new's
|
||||
// Messages > Room Servers, ui-lvgl's Messages > Rooms). Posting to a room needs
|
||||
// a login handshake first, even with a blank password; this keeps which rooms
|
||||
// were logged in to this power-on, the one login in flight, and the password
|
||||
// memory (MyMesh::saveRoomPassword): a room logged in to on an earlier boot
|
||||
// logs in again silently with the saved password, a failed login forgets it.
|
||||
//
|
||||
// RAM-only memo: the server's ACL is the real permission store and survives
|
||||
// reboot on its own, but the device can't query it, so this only saves asking
|
||||
// again for a password entered this session.
|
||||
|
||||
class RoomSessions {
|
||||
public:
|
||||
enum Open : uint8_t { OPEN_NOW, LOGGING_IN, NEED_PASSWORD, SEND_FAILED };
|
||||
enum Outcome : uint8_t { NONE, LOGGED_IN, LOGIN_FAILED, LOGIN_TIMEOUT };
|
||||
|
||||
bool isLoggedIn(const uint8_t* pub_key) const {
|
||||
for (int i = 0; i < _count; i++)
|
||||
if (memcmp(_prefix[(_head + i) % TABLE_SIZE], pub_key, 4) == 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Opening a room: straight in when logged in this session, a background
|
||||
// login with the saved password, else the frontend asks for one.
|
||||
Open open(const ContactInfo& ci) {
|
||||
if (isLoggedIn(ci.id.pub_key)) return OPEN_NOW;
|
||||
char saved[sizeof(_pw)];
|
||||
if (the_mesh.getRoomPassword(ci.id.pub_key, saved, sizeof(saved)))
|
||||
return login(ci, saved) ? LOGGING_IN : SEND_FAILED;
|
||||
return NEED_PASSWORD;
|
||||
}
|
||||
|
||||
bool login(const ContactInfo& ci, const char* password) {
|
||||
snprintf(_pw, sizeof(_pw), "%s", password);
|
||||
uint32_t est = 0;
|
||||
if (!the_mesh.sendRoomLogin(ci, password, est)) { _pending = false; return false; }
|
||||
memcpy(_pending_key, ci.id.pub_key, 4);
|
||||
_pending = true;
|
||||
_deadline = millis() + est + 4000; // same margin as a DM's ACK wait
|
||||
return true;
|
||||
}
|
||||
|
||||
bool loggingIn() const { return _pending; }
|
||||
bool loggingIn(const uint8_t* pub_key) const { return _pending && memcmp(_pending_key, pub_key, 4) == 0; }
|
||||
const uint8_t* pendingKey() const { return _pending_key; }
|
||||
|
||||
// Local only: the room ACL has no session to tear down (MyMesh::logoutRoom()).
|
||||
void logout(const uint8_t* pub_key) {
|
||||
the_mesh.logoutRoom(pub_key);
|
||||
forget(pub_key);
|
||||
if (loggingIn(pub_key)) cancel();
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
if (_pending) the_mesh.cancelUiPendingLogin(_pending_key);
|
||||
_pending = false;
|
||||
}
|
||||
|
||||
// From UiCore::onRoomLoginResult (after the admin session passed on it).
|
||||
// True when it answers this session's login.
|
||||
bool onLoginResult(const uint8_t* pub_key, bool success) {
|
||||
if (!loggingIn(pub_key)) return false;
|
||||
_pending = false;
|
||||
if (success) {
|
||||
markLoggedIn(pub_key);
|
||||
the_mesh.saveRoomPassword(pub_key, _pw);
|
||||
_outcome = LOGGED_IN;
|
||||
} else {
|
||||
// The saved password no longer works: forget it, so the next open asks
|
||||
// instead of retrying the same bad one forever.
|
||||
the_mesh.forgetRoomPassword(pub_key);
|
||||
_outcome = LOGIN_FAILED;
|
||||
}
|
||||
memcpy(_outcome_key, pub_key, 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
void loop() {
|
||||
if (_pending && (int32_t)(millis() - _deadline) > 0) {
|
||||
memcpy(_outcome_key, _pending_key, 4);
|
||||
cancel();
|
||||
_outcome = LOGIN_TIMEOUT;
|
||||
}
|
||||
}
|
||||
|
||||
// The last login's outcome, once (NONE when nothing new); key = its room.
|
||||
Outcome take(uint8_t* key_out = nullptr) {
|
||||
Outcome o = _outcome;
|
||||
_outcome = NONE;
|
||||
if (key_out) memcpy(key_out, _outcome_key, 4);
|
||||
return o;
|
||||
}
|
||||
|
||||
private:
|
||||
static const int TABLE_SIZE = 8; // oldest dropped when full
|
||||
|
||||
void markLoggedIn(const uint8_t* pub_key) {
|
||||
if (isLoggedIn(pub_key)) return;
|
||||
int pos;
|
||||
if (_count < TABLE_SIZE) pos = (_head + _count++) % TABLE_SIZE;
|
||||
else { pos = _head; _head = (_head + 1) % TABLE_SIZE; }
|
||||
memcpy(_prefix[pos], pub_key, 4);
|
||||
}
|
||||
|
||||
void forget(const uint8_t* pub_key) {
|
||||
for (int i = 0; i < _count; i++) {
|
||||
if (memcmp(_prefix[(_head + i) % TABLE_SIZE], pub_key, 4) != 0) continue;
|
||||
for (int j = i; j < _count - 1; j++)
|
||||
memcpy(_prefix[(_head + j) % TABLE_SIZE], _prefix[(_head + j + 1) % TABLE_SIZE], 4);
|
||||
_count--;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t _prefix[TABLE_SIZE][4];
|
||||
int _head = 0, _count = 0;
|
||||
char _pw[16] = ""; // of the login in flight, saved on success
|
||||
uint8_t _pending_key[4] = {0};
|
||||
bool _pending = false;
|
||||
uint32_t _deadline = 0;
|
||||
Outcome _outcome = NONE;
|
||||
uint8_t _outcome_key[4] = {0};
|
||||
};
|
||||
@@ -25,6 +25,8 @@
|
||||
#include "WaypointModel.h"
|
||||
#include "ChannelControl.h"
|
||||
#include "AdminSession.h"
|
||||
#include "RoomSessions.h"
|
||||
#include "ContactControl.h"
|
||||
|
||||
class UiCore : public MyMesh::Listener {
|
||||
public:
|
||||
@@ -53,6 +55,7 @@ public:
|
||||
locator.loop();
|
||||
trail.loop();
|
||||
admin.loop();
|
||||
rooms.loop();
|
||||
}
|
||||
|
||||
UiEventQueue events; // Core → frontend; drained by the frontend's loop()
|
||||
@@ -65,6 +68,7 @@ public:
|
||||
LocatorEngine locator; // active target, geofence crossings, proximity beeper
|
||||
TrailEngine trail; // GPS trail store, sampling, auto-pause, shutdown save
|
||||
AdminSession admin; // remote repeater / room admin: login + CLI round trips
|
||||
RoomSessions rooms; // room server logins for posting (messages screens)
|
||||
|
||||
// ── Models ────────────────────────────────────────────────────────────────
|
||||
MessageHistory history; // channel + DM rings, delivery state, channel unread
|
||||
@@ -240,9 +244,12 @@ public:
|
||||
}
|
||||
|
||||
// Login answers go to the admin session when it's the one waiting, else to
|
||||
// the frontend (room server logins from its messages screen).
|
||||
// the room sessions (logins from the messages screen), then the frontend.
|
||||
void onRoomLoginResult(const uint8_t* pub_key, bool success, uint8_t permissions) override {
|
||||
if (!admin.onLoginResult(pub_key, success, permissions)) _host->onRoomLoginResult(pub_key, success, permissions);
|
||||
if (!admin.onLoginResult(pub_key, success, permissions)) {
|
||||
rooms.onLoginResult(pub_key, success);
|
||||
_host->onRoomLoginResult(pub_key, success, permissions);
|
||||
}
|
||||
_host->onAdminStateChanged();
|
||||
}
|
||||
void onAdminReply(const uint8_t* pub_key, const char* text) override {
|
||||
|
||||
Reference in New Issue
Block a user