fix(chat): cap rendered bubble text so large messages don't crawl on scroll

A message with multi-KB content (e.g. a large bz2-delivered payload) rendered
untruncated, so LVGL laid it out as a 50+ line wrapped bubble and re-drew the
whole thing while scrolling past it -- crawling the UI. Cap the *displayed* text
to MAX_DISPLAY_CHARS; the full content stays stored. (Decompression is unrelated:
it happens once in Resource::assemble() at receive, content is saved already
decompressed, and load_message_metadata never re-decompresses.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5
This commit is contained in:
torlando-agent[bot]
2026-06-20 01:24:14 -04:00
co-authored by Claude Opus 4.8
parent 13b3d7d73a
commit 28c40de3a1
2 changed files with 15 additions and 3 deletions
+10 -3
View File
@@ -410,6 +410,13 @@ void ChatScreen::create_message_bubble(const MessageItem& item) {
build_status_text(status_text, sizeof(status_text), item.timestamp_str,
item.outgoing, item.delivered, item.failed);
// Cap very long messages for display — laying out and re-drawing a multi-KB
// wrapped bubble is slow and memory-heavy. The full content stays stored.
String display_text = item.content;
if (display_text.length() > MAX_DISPLAY_CHARS) {
display_text = display_text.substring(0, MAX_DISPLAY_CHARS) + "...";
}
// Calculate text widths to decide layout
// Bubble is 80% of 320 = 256px, minus 16px padding = 240px usable
const lv_coord_t bubble_inner_width = 240;
@@ -417,7 +424,7 @@ void ChatScreen::create_message_bubble(const MessageItem& item) {
const lv_coord_t gap = 12; // Space between message and timestamp
lv_coord_t msg_width = lv_txt_get_width(
item.content.c_str(), item.content.length(), font, 0, LV_TEXT_FLAG_NONE);
display_text.c_str(), display_text.length(), font, 0, LV_TEXT_FLAG_NONE);
lv_coord_t status_width = lv_txt_get_width(
status_text, strlen(status_text), font, 0, LV_TEXT_FLAG_NONE);
@@ -431,7 +438,7 @@ void ChatScreen::create_message_bubble(const MessageItem& item) {
// Message content
lv_obj_t* label_content = lv_label_create(bubble);
lv_label_set_text(label_content, item.content.c_str());
lv_label_set_text(label_content, display_text.c_str());
lv_obj_set_style_text_color(label_content, lv_color_white(), 0);
// Timestamp on same row
@@ -446,7 +453,7 @@ void ChatScreen::create_message_bubble(const MessageItem& item) {
// Message content with wrapping
lv_obj_t* label_content = lv_label_create(bubble);
lv_label_set_text(label_content, item.content.c_str());
lv_label_set_text(label_content, display_text.c_str());
lv_label_set_long_mode(label_content, LV_LABEL_LONG_WRAP);
lv_obj_set_width(label_content, LV_PCT(100));
lv_obj_set_style_text_color(label_content, lv_color_white(), 0);
+5
View File
@@ -192,6 +192,11 @@ private:
static constexpr size_t MESSAGES_PER_PAGE = 10; // full first page (filled in background)
static constexpr size_t BG_FILL_BATCH = 2; // older messages streamed per tick
static constexpr size_t MAX_DISPLAYED_MESSAGES = 50; // Cap to prevent memory exhaustion
// Cap the text rendered per bubble. LVGL lays out (and re-draws on scroll) a
// wrapped label in O(length); a multi-KB message (e.g. a large bz2-delivered
// payload) becomes a 50+ line bubble that crawls when scrolled past. The full
// content stays stored; only the rendered text is truncated.
static constexpr size_t MAX_DISPLAY_CHARS = 600;
bool _loading_more; // Prevent concurrent loads
// Streaming state. _bg_fill_active is atomic because on_scroll() (LVGL task)
// and refresh() may set it while tick_background_fill() (main loop) reads it;