From 28c40de3a1ff7ca5f821548b9e8fedc3e97a5ffd Mon Sep 17 00:00:00 2001 From: "torlando-agent[bot]" <281092095+torlando-agent[bot]@users.noreply.github.com> Date: Sat, 20 Jun 2026 01:24:14 -0400 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01UWZuYkHBRqNb6BZHV8sTG5 --- lib/tdeck_ui/UI/LXMF/ChatScreen.cpp | 13 ++++++++++--- lib/tdeck_ui/UI/LXMF/ChatScreen.h | 5 +++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lib/tdeck_ui/UI/LXMF/ChatScreen.cpp b/lib/tdeck_ui/UI/LXMF/ChatScreen.cpp index 72423305..d337d807 100644 --- a/lib/tdeck_ui/UI/LXMF/ChatScreen.cpp +++ b/lib/tdeck_ui/UI/LXMF/ChatScreen.cpp @@ -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); diff --git a/lib/tdeck_ui/UI/LXMF/ChatScreen.h b/lib/tdeck_ui/UI/LXMF/ChatScreen.h index 6638e045..e95d0899 100644 --- a/lib/tdeck_ui/UI/LXMF/ChatScreen.h +++ b/lib/tdeck_ui/UI/LXMF/ChatScreen.h @@ -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;