feat(ui): message history newest-at-bottom; keyboard one-shot Shift + hold-clear

Message history (DM + channel) now stacks bubbles upward from the compose
row instead of the header, so the newest message sits at the bottom like
a typical messenger, with older messages progressively above. UP/DOWN and
the scrollbar direction are swapped to match; the newest bubble now keeps
the same breathing-room gap against the compose row as it does against
another bubble, instead of touching it.

Keyboard: Shift is one-shot by default (capitalises just the next
letter, then reverts), with Hold-Enter on Shift toggling a persistent
caps-lock for the old sticky behavior. Hold-Enter on Backspace clears
the whole field in one action.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-07-17 10:02:00 +02:00
parent 9d44921d34
commit af0cd2116e
2 changed files with 91 additions and 29 deletions

View File

@@ -319,6 +319,10 @@ struct KeyboardWidget {
int row, col;
int page; // see totalPages()/pageIsAltAlphabet()/pageIsSymbols() below
bool caps;
// Shift is one-shot by default (like a phone keyboard: capitalises just the
// next letter, then reverts) — Hold-Enter on Shift toggles caps_lock, which
// keeps it on for a whole run of capitals instead.
bool caps_lock = false;
char _ph_buf[KB_PH_MAX][KB_PH_LEN];
int _ph_count;
PopupMenu _ph_menu;
@@ -433,6 +437,7 @@ struct KeyboardWidget {
row = col = 0;
page = 0;
caps = false;
caps_lock = false;
t9_cell = -1;
t9_cycle = 0;
_ph_menu.active = false;
@@ -617,11 +622,30 @@ struct KeyboardWidget {
return NONE;
}
if (c == KEY_CANCEL || c == KEY_CONTEXT_MENU) return CANCELLED;
if (c == KEY_CANCEL) return CANCELLED;
const int rows = gridRows();
const int cols = gridCols();
// Hold-Enter is normally "cancel", but over the two keys where a long-press
// has its own well-known meaning on a phone keyboard, it does that instead:
// Shift -> toggle a persistent caps-lock (a plain tap is one-shot -- see the
// commit sites below); Backspace -> clear the whole field in one action
// instead of holding it down. Every other cell keeps hold-to-cancel.
if (c == KEY_CONTEXT_MENU) {
if (row == rows && col == 0) { // Shift
caps_lock = !caps_lock;
caps = caps_lock;
return NONE;
}
if (row == rows && col == 2) { // Backspace
len = 0; buf[0] = '\0';
t9_cell = -1;
return NONE;
}
return CANCELLED;
}
if (c == KEY_UP) {
if (row > 0) {
row--;
@@ -692,6 +716,7 @@ struct KeyboardWidget {
buf[len] = '\0';
t9_cell = cell;
t9_cycle = 0;
if (caps && !caps_lock) caps = false; // one-shot: only this first tap gets capitalised
}
}
t9_last_ms = millis();
@@ -703,11 +728,14 @@ struct KeyboardWidget {
memcpy(buf + len, shown, n);
len += n;
buf[len] = '\0';
if (caps && !caps_lock) caps = false; // one-shot: revert after the letter it capitalised
}
} else {
t9_cell = -1; // any special-row action finalizes a pending multi-tap cycle
switch (col) {
case 0: caps = !caps; break;
// Tap toggles one-shot caps on/off; while caps_lock is held (Hold-Enter
// on this key, see handleInput's top), a tap cancels the lock instead.
case 0: if (caps_lock) { caps = false; caps_lock = false; } else { caps = !caps; } break;
case 1:
if (len < max_len) { buf[len++] = ' '; buf[len] = '\0'; }
break;