mirror of
https://github.com/MarekZegare4/MeshCore-Solo.git
synced 2026-11-03 03:11:57 +00:00
feat: a message's bytes left, counted as it will be sent, on every screen
The limit is what the send leaves of MAX_TEXT_LEN: a channel post carries "<node name>: ", a direct message resent more than three times keeps two bytes spare, and placeholders count as they expand (msgtext::limit/sentLen). Bytes, not characters. The path and packet header sit outside the payload; static_asserts keep a full message within the radio frame on the longest path. From 40 bytes left the count shows: on the L2 as a tag on the field's top edge, on OLED/e-ink at the right of the preview's last line with the text wrapped short of it. Input stops at the limit; a send still over (a placeholder grew) is held. Sim hooks to open a compose with text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -112,6 +112,7 @@ static const char* const KB_T9_GROUPS_GREEK[9] = {
|
||||
// (10*CIPHER_BLOCK_SIZE = 160) so a full-length message can be composed; each
|
||||
// field passes its own smaller max to begin() where its store is smaller.
|
||||
static const int KB_MAX_LEN = 160;
|
||||
static const int KB_COUNT_FROM = 40; // a message's bytes left show from here on (the L2's too)
|
||||
|
||||
// Longest preview line we render per row, in CHARACTERS. Caps the per-line
|
||||
// stack buffers so a very wide display (small font → many chars per line)
|
||||
@@ -275,6 +276,13 @@ struct KeyboardWidget {
|
||||
// the right of the preview; set it after begin(), which clears it. Kept past
|
||||
// the PIN pad's ABC switch.
|
||||
const char* prompt = nullptr;
|
||||
// What the message may take once sent (a channel's "Name: " comes out of it,
|
||||
// a {loc} grows): sent_limit 0 = max_len as typed. sent_len measures `buf`
|
||||
// as it will go out; both are set by the owner after begin(). Input stops
|
||||
// there, and the bytes left show near it (see render()).
|
||||
int sent_limit = 0;
|
||||
int (*sent_len)(const char* buf, void* ctx) = nullptr;
|
||||
void* sent_ctx = nullptr;
|
||||
static const int PIN_MAX_LEN = 16;
|
||||
int row, col;
|
||||
int page; // see totalPages()/scriptAt()/pageIsSymbols() below
|
||||
@@ -420,6 +428,9 @@ struct KeyboardWidget {
|
||||
pin_kb_mask_enabled = false;
|
||||
pin_mode = false;
|
||||
prompt = nullptr;
|
||||
sent_limit = 0;
|
||||
sent_len = nullptr;
|
||||
sent_ctx = nullptr;
|
||||
t9_cell = -1;
|
||||
t9_cycle = 0;
|
||||
_ph_menu.active = false;
|
||||
@@ -599,8 +610,19 @@ struct KeyboardWidget {
|
||||
// works in codepoints via the kbUtf8*() helpers; this was the last
|
||||
// byte-based holdout.
|
||||
int cpl = display.width() / cw; // chars per preview line
|
||||
if (cpl < 1) cpl = 1;
|
||||
if (cpl > KB_PREVIEW_CAP) cpl = KB_PREVIEW_CAP; // never overrun linebuf below
|
||||
// A message near its end: the bytes left at the right of the preview's last
|
||||
// line, the text wrapped short of it so the count is always there.
|
||||
char count[8];
|
||||
count[0] = '\0';
|
||||
if (!isPin() && sent_limit > 0) {
|
||||
const int n = sent_limit - sentNow();
|
||||
if (n <= KB_COUNT_FROM) {
|
||||
snprintf(count, sizeof(count), "%d", n);
|
||||
cpl -= (int)strlen(count) + 2; // its digits, the cursor, a space
|
||||
}
|
||||
}
|
||||
if (cpl < 1) cpl = 1;
|
||||
// Which preview line the cursor sits on = how many whole codepoints precede it.
|
||||
int cursor_chars = 0;
|
||||
for (int p = 0; p < cursor_pos; ) { p += kbUtf8CharBytesAt(buf, p, len); cursor_chars++; }
|
||||
@@ -658,12 +680,21 @@ struct KeyboardWidget {
|
||||
display.print(linebuf_t);
|
||||
ps = pe;
|
||||
}
|
||||
if (!isPin()) {
|
||||
if (count[0]) {
|
||||
const int tw = display.getTextWidth(count), y = (prev_lines - 1) * lh;
|
||||
if (count[0] == '-') { // over (a placeholder grew): inverted
|
||||
display.fillRect(display.width() - tw - 2, y, tw + 2, lh);
|
||||
display.setColor(DisplayDriver::DARK);
|
||||
}
|
||||
display.setCursor(display.width() - tw - (count[0] == '-' ? 1 : 0), y);
|
||||
display.print(count);
|
||||
display.setColor(DisplayDriver::LIGHT);
|
||||
} else if (!isPin()) {
|
||||
// Bottom-right of the preview: what the field is, or near the limit how
|
||||
// much room is left. Only when it clears the text on that line.
|
||||
char left[12];
|
||||
const char* tag = prompt;
|
||||
if (max_len > 0 && len * 4 >= max_len * 3) { snprintf(left, sizeof(left), "%d left", max_len - len); tag = left; }
|
||||
if (sent_limit <= 0 && max_len > 0 && len * 4 >= max_len * 3) { snprintf(left, sizeof(left), "%d left", max_len - len); tag = left; }
|
||||
if (tag) {
|
||||
int tw = display.getTextWidth(tag);
|
||||
// Codepoints on the last preview line, plus the cursor if it's there.
|
||||
@@ -901,7 +932,29 @@ struct KeyboardWidget {
|
||||
return true;
|
||||
}
|
||||
|
||||
// What buf takes once sent (sent_len set), or as typed.
|
||||
int sentNow() { return sent_len ? sent_len(buf, sent_ctx) : len; }
|
||||
|
||||
// A message never grows past what it may send (sent_limit, its placeholders
|
||||
// as they expand): a key that would take it there is undone. Shortening is
|
||||
// always let through, even while over (a {loc} that grew since).
|
||||
Result handleInput(char c) {
|
||||
if (sent_limit <= 0 || isPin()) return handleKey(c);
|
||||
char was[KB_MAX_LEN + 1];
|
||||
memcpy(was, buf, len + 1);
|
||||
const int was_len = len, was_cur = cursor_pos, before = sentNow();
|
||||
Result r = handleKey(c);
|
||||
const int after = sentNow();
|
||||
if (after > sent_limit && after > before) {
|
||||
memcpy(buf, was, was_len + 1);
|
||||
len = was_len;
|
||||
cursor_pos = was_cur;
|
||||
t9_cell = -1; // a T9 cycle would otherwise go on replacing the char before it
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
Result handleKey(char c) {
|
||||
// placeholder overlay consumes all input
|
||||
if (_ph_menu.active) {
|
||||
auto res = _ph_menu.handleInput(c);
|
||||
|
||||
@@ -324,6 +324,11 @@ class MessagesScreen : public UIScreen {
|
||||
if (_sending_to_channel && the_mesh.getChannel(_sel_channel_idx, ch)) to = ch.name;
|
||||
snprintf(_kb_prompt, sizeof(_kb_prompt), "%.12s", to);
|
||||
_kb->prompt = _kb_prompt;
|
||||
// The room it really has: this node's name rides a channel post, and the
|
||||
// placeholders grow when sent.
|
||||
_kb->sent_limit = msgtext::limit(_sending_to_channel, _task->getNodePrefs());
|
||||
_kb->sent_len = [](const char* b, void* t) { return msgtext::sentLen(b, ((UITask*)t)->getNodePrefs()); };
|
||||
_kb->sent_ctx = _task;
|
||||
}
|
||||
|
||||
// Recipient chosen while sharing — open the keyboard with the prepared text.
|
||||
@@ -948,6 +953,15 @@ class MessagesScreen : public UIScreen {
|
||||
bool _ch_view_pending_rebuild = false; // tracks redraw after channel create/delete
|
||||
|
||||
public:
|
||||
#ifdef SIM_PLATFORM
|
||||
// Sim tests: channel 0's keyboard holding `text`.
|
||||
void simCompose(const char* text) {
|
||||
_sel_channel_idx = 0;
|
||||
snprintf(_share_text, sizeof(_share_text), "%s", text);
|
||||
beginShareCompose(true);
|
||||
}
|
||||
#endif
|
||||
|
||||
MessagesScreen(UITask* task, KeyboardWidget* kb)
|
||||
: _task(task), _kb(kb), _phase(MODE_SELECT), _mode_sel(0),
|
||||
_contact_sel(0), _contact_scroll(0), _num_contacts(0), _room_mode(false), _login_mode(false),
|
||||
@@ -2448,13 +2462,16 @@ public:
|
||||
if (_kb->len > prefix_len) {
|
||||
// Expand only the body — prefix "@[nick] " is preserved verbatim, so a nick
|
||||
// that happens to contain a placeholder token isn't substituted.
|
||||
char expanded[KB_MAX_LEN + 1];
|
||||
char expanded[2 * KB_MAX_LEN + 1];
|
||||
if (prefix_len > 0) {
|
||||
memcpy(expanded, _kb->buf, prefix_len);
|
||||
expandMsg(_kb->buf + prefix_len, expanded + prefix_len, sizeof(expanded) - prefix_len);
|
||||
} else {
|
||||
expandMsg(_kb->buf, expanded, sizeof(expanded));
|
||||
}
|
||||
// Too long for what it adds (the keyboard's "N over" says by how much): it
|
||||
// would be cut short, or refused, so it stays on the keyboard.
|
||||
if ((int)strlen(expanded) > msgtext::limit(_sending_to_channel, _task->getNodePrefs())) return true;
|
||||
bool ok = sendText(expanded);
|
||||
afterSend(ok);
|
||||
}
|
||||
|
||||
@@ -2190,6 +2190,13 @@ void UITask::gotoMessagesScreen() {
|
||||
setCurrScreen(messages_screen);
|
||||
}
|
||||
|
||||
#ifdef SIM_PLATFORM
|
||||
void UITask::simCompose(const char* text) {
|
||||
gotoMessagesScreen();
|
||||
((MessagesScreen*)messages_screen)->simCompose(text);
|
||||
}
|
||||
#endif
|
||||
|
||||
void UITask::openContactDM(const ContactInfo& ci) {
|
||||
((MessagesScreen*)messages_screen)->reset();
|
||||
((MessagesScreen*)messages_screen)->enterDM(ci);
|
||||
@@ -2661,6 +2668,11 @@ extern "C" EMSCRIPTEN_KEEPALIVE void sim_enqueue_key(char c) {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->injectSimKey(c);
|
||||
}
|
||||
|
||||
// Sim tests: a channel post's keyboard, holding `text`.
|
||||
extern "C" EMSCRIPTEN_KEEPALIVE void sim_compose(const char* text) {
|
||||
if (g_sim_ui_task_for_js) g_sim_ui_task_for_js->simCompose(text);
|
||||
}
|
||||
|
||||
// Long-press counterpart -- the host page's own press-and-hold timer (see
|
||||
// web/index.html/mesh.html) calls this instead of sim_enqueue_key() once a
|
||||
// button/key has been held past the same ~1000ms threshold every real
|
||||
|
||||
@@ -274,6 +274,9 @@ public:
|
||||
void gotoHomeScreen() { setCurrScreen(home); }
|
||||
void gotoSettingsScreen();
|
||||
void gotoMessagesScreen();
|
||||
#ifdef SIM_PLATFORM
|
||||
void simCompose(const char* text); // sim tests: a channel post's keyboard holding `text`
|
||||
#endif
|
||||
void openContactDM(const ContactInfo& ci);
|
||||
void openChannelHistory(uint8_t channel_idx); // Favourites dial: open a pinned channel
|
||||
void openRoomServer(const ContactInfo& ci); // Favourites dial: open a pinned room (logs in first)
|
||||
|
||||
Reference in New Issue
Block a user