From c7d4e9abccca1b305c7e76fe12afa90e58001cbf Mon Sep 17 00:00:00 2001 From: liu weikai Date: Tue, 11 Aug 2026 22:56:53 +0800 Subject: [PATCH] fix(pager): preserve voice hold and add chat selection --- .../chat/chat_conversation_components.h | 10 +- .../screens/chat/chat_compose_components.cpp | 13 +- .../chat/chat_conversation_components.cpp | 113 +++++++++++++++++- .../screens/chat/chat_conversation_input.cpp | 23 +++- .../screens/chat/chat_conversation_styles.cpp | 11 ++ 5 files changed, 163 insertions(+), 7 deletions(-) diff --git a/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h b/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h index 033fee23..5694e31a 100644 --- a/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h +++ b/modules/ui_shared/include/ui/screens/chat/chat_conversation_components.h @@ -52,9 +52,14 @@ class ChatConversationScreen ~ChatConversationScreen(); void addMessage(const ::ui::chat::MessageRow& row); - /** @brief Adds one local-only, click-to-play VMP voice bubble. */ + /** @brief Adds one local-only, selectable VMP voice bubble. */ void addVoiceMessage(const ::ui::chat_voice::MessageSummary& summary); void clearMessages(); + /** Moves the non-touch timeline selection without changing rotary scroll. */ + bool selectPreviousMessage(); + bool selectNextMessage(); + /** Plays the selected voice bubble; selected non-voice bubbles consume Enter. */ + bool activateSelectedMessage(); void scrollToTop(); void scrollToBottom(); bool updateMessageStatus( @@ -224,8 +229,11 @@ class ChatConversationScreen bool history_newer_boundary_notified_ = false; bool location_map_visible_ = false; bool location_map_created_ = false; + int selected_message_index_ = -1; void createMessageItem(const ::ui::chat::MessageRow& row); + bool selectMessageRelative(int direction); + void setSelectedMessageIndex(int index); void handleScroll(); void enableRetryAction(MessageItem& item); void disableRetryAction(MessageItem& item); diff --git a/modules/ui_shared/src/ui/screens/chat/chat_compose_components.cpp b/modules/ui_shared/src/ui/screens/chat/chat_compose_components.cpp index 791dd1f1..23a4c17c 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_compose_components.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_compose_components.cpp @@ -304,6 +304,8 @@ void ChatComposeScreen::setVoiceButton(const char* label, bool visible) { if (!impl_ || !impl_->w.position_btn) return; impl_->auxiliary_ctx.intent = ActionIntent::VoiceStart; + const bool visibility_changed = + lv_obj_has_flag(impl_->w.position_btn, LV_OBJ_FLAG_HIDDEN) == visible; if (label) { set_btn_label_text(impl_->w.position_btn, label); @@ -318,7 +320,16 @@ void ChatComposeScreen::setVoiceButton(const char* label, bool visible) lv_obj_add_flag(impl_->w.position_btn, LV_OBJ_FLAG_HIDDEN); } - syncFocusOrder(); + // Recording begins on LV_EVENT_PRESSED and ends on LV_EVENT_RELEASED. + // Rebuilding the LVGL group just to refresh "Voice" -> "Release" removes + // the currently pressed object from the group, which transfers encoder + // focus before the release reaches it. Only a visibility transition + // changes the group membership; a label/timer refresh must preserve the + // focused Voice button for the entire press-and-hold sequence. + if (visibility_changed) + { + syncFocusOrder(); + } } std::string ChatComposeScreen::getText() const diff --git a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp index 68a45064..55ef61d0 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_conversation_components.cpp @@ -537,7 +537,7 @@ void format_voice_text(char* out, std::snprintf(out, out_size, "%s", - playing ? "Playing voice..." : "Voice message - tap to play"); + playing ? "Playing voice..." : "Voice message - Enter plays"); return; } std::snprintf(out, @@ -545,7 +545,7 @@ void format_voice_text(char* out, "Voice %lu.%lus%s", static_cast(tenths / 10U), static_cast(tenths % 10U), - playing ? " - playing" : " - tap to play"); + playing ? " - playing" : " - Enter plays"); } } // namespace @@ -818,6 +818,14 @@ void ChatConversationScreen::addMessage(const ::ui::chat::MessageRow& row) } if (messages_.size() >= MAX_DISPLAY_MESSAGES) { + if (selected_message_index_ == 0) + { + setSelectedMessageIndex(-1); + } + else if (selected_message_index_ > 0) + { + --selected_message_index_; + } MessageItem& oldest = messages_[0]; if (oldest.container) { @@ -845,6 +853,14 @@ void ChatConversationScreen::addVoiceMessage( } if (messages_.size() >= MAX_DISPLAY_MESSAGES) { + if (selected_message_index_ == 0) + { + setSelectedMessageIndex(-1); + } + else if (selected_message_index_ > 0) + { + --selected_message_index_; + } MessageItem& oldest = messages_[0]; if (oldest.container) { @@ -951,6 +967,7 @@ void ChatConversationScreen::clearMessages() return; } clear_timers(TimerDomain::VoicePlayback); + setSelectedMessageIndex(-1); size_t index = 0; for (auto& item : messages_) { @@ -968,6 +985,92 @@ void ChatConversationScreen::clearMessages() static_cast(lv_tick_elaps(started_ms))); } +bool ChatConversationScreen::selectPreviousMessage() +{ + return selectMessageRelative(-1); +} + +bool ChatConversationScreen::selectNextMessage() +{ + return selectMessageRelative(1); +} + +bool ChatConversationScreen::activateSelectedMessage() +{ + if (selected_message_index_ < 0 || + selected_message_index_ >= static_cast(messages_.size())) + { + return false; + } + + MessageItem& item = messages_[static_cast(selected_message_index_)]; + if (item.voice_playback_ctx && item.bubble && lv_obj_is_valid(item.bubble)) + { + lv_obj_send_event(item.bubble, LV_EVENT_CLICKED, nullptr); + } + + // A selected text, image, or location bubble deliberately consumes Enter. + // This reserves the key for an eventual type-specific bubble action and + // prevents the message-list's edit mode from changing unexpectedly. + return true; +} + +bool ChatConversationScreen::selectMessageRelative(int direction) +{ + if (direction == 0 || messages_.empty()) + { + return false; + } + + const int newest = static_cast(messages_.size()) - 1; + int next = selected_message_index_; + if (next < 0 || next > newest) + { + next = newest; + } + else + { + next += direction; + if (next < 0 || next > newest) + { + return false; + } + } + + setSelectedMessageIndex(next); + return true; +} + +void ChatConversationScreen::setSelectedMessageIndex(int index) +{ + if (selected_message_index_ >= 0 && + selected_message_index_ < static_cast(messages_.size())) + { + MessageItem& previous = messages_[static_cast(selected_message_index_)]; + if (previous.bubble && lv_obj_is_valid(previous.bubble)) + { + lv_obj_clear_state(previous.bubble, LV_STATE_USER_1); + } + } + + if (index < 0 || index >= static_cast(messages_.size())) + { + selected_message_index_ = -1; + return; + } + + selected_message_index_ = index; + MessageItem& selected = messages_[static_cast(selected_message_index_)]; + if (selected.bubble && lv_obj_is_valid(selected.bubble)) + { + lv_obj_add_state(selected.bubble, LV_STATE_USER_1); + } + if (selected.container && lv_obj_is_valid(selected.container)) + { + lv_obj_scroll_to_view(selected.container, LV_ANIM_OFF); + } +} + void ChatConversationScreen::voice_message_event_cb(lv_event_t* e) { if (!e || lv_event_get_code(e) != LV_EVENT_CLICKED) @@ -1230,7 +1333,9 @@ void ChatConversationScreen::toggleShortcutHelp() } static constexpr ::ui::components::shortcut_help_modal::Row rows[] = { - {"S", nullptr, "Compose message"}, + {"R", nullptr, "Compose message"}, + {"W/S", nullptr, "Select message"}, + {"Enter", nullptr, "Play selected voice"}, {"Up/Down", nullptr, "Scroll messages"}, {"Prev/Next", nullptr, "Load older/newer"}, {"Home/End", nullptr, "Top/bottom"}, @@ -1252,7 +1357,7 @@ void ChatConversationScreen::toggleShortcutHelp() config.rows = rows; config.row_count = sizeof(rows) / sizeof(rows[0]); config.width = ::ui::page_profile::is_dense() ? 288 : 304; - config.height = ::ui::page_profile::is_dense() ? 170 : 186; + config.height = ::ui::page_profile::is_dense() ? 192 : 208; config.restore_group = lv_group_get_default(); (void)::ui::components::shortcut_help_modal::open( shortcut_help_modal_, diff --git a/modules/ui_shared/src/ui/screens/chat/chat_conversation_input.cpp b/modules/ui_shared/src/ui/screens/chat/chat_conversation_input.cpp index 3b45492a..ad3b3e0f 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_conversation_input.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_conversation_input.cpp @@ -143,10 +143,31 @@ static bool handle_conversation_shortcut(ChatConversationScreen* screen, consume(e); return true; } - if (key == 's' || key == 'S') + if (key == 'r' || key == 'R') { return send_reply(screen, e); } + if (key == 'w' || key == 'W') + { + if (screen->selectPreviousMessage()) + { + consume(e); + } + return true; + } + if (key == 's' || key == 'S') + { + if (screen->selectNextMessage()) + { + consume(e); + } + return true; + } + if (key == LV_KEY_ENTER && screen->activateSelectedMessage()) + { + consume(e); + return true; + } if (handle_map_key(screen, e, key)) { return true; diff --git a/modules/ui_shared/src/ui/screens/chat/chat_conversation_styles.cpp b/modules/ui_shared/src/ui/screens/chat/chat_conversation_styles.cpp index e19826a4..ba49a647 100644 --- a/modules/ui_shared/src/ui/screens/chat/chat_conversation_styles.cpp +++ b/modules/ui_shared/src/ui/screens/chat/chat_conversation_styles.cpp @@ -22,6 +22,7 @@ static lv_style_t s_bubble_base; static lv_style_t s_bubble_self; static lv_style_t s_bubble_other; static lv_style_t s_bubble_unverified; +static lv_style_t s_bubble_selected; static lv_style_t s_bubble_text; static lv_style_t s_bubble_time; @@ -128,6 +129,15 @@ void init_once() lv_style_set_bg_color(&s_bubble_unverified, lv_color_hex(0xF4E1DE)); lv_style_set_border_color(&s_bubble_unverified, lv_color_hex(0xC47D70)); + // This style is shared by every bubble. W/S selection only toggles the + // LV_STATE_USER_1 bit; it does not add bubbles to an LVGL group or create + // any per-message focus bookkeeping/allocation. + lv_style_init(&s_bubble_selected); + lv_style_set_outline_width(&s_bubble_selected, dense ? 1 : 2); + lv_style_set_outline_pad(&s_bubble_selected, dense ? 1 : 2); + lv_style_set_outline_color(&s_bubble_selected, lv_color_hex(0xE57B1F)); + lv_style_set_outline_opa(&s_bubble_selected, LV_OPA_COVER); + lv_style_init(&s_bubble_text); lv_style_set_text_color(&s_bubble_text, kTextColor); lv_style_set_text_align(&s_bubble_text, LV_TEXT_ALIGN_LEFT); @@ -184,6 +194,7 @@ void apply_bubble(lv_obj_t* bubble, bool is_self, bool source_unverified) is_self ? &s_bubble_self : (source_unverified ? &s_bubble_unverified : &s_bubble_other); lv_obj_add_style(bubble, message_style, LV_PART_MAIN); + lv_obj_add_style(bubble, &s_bubble_selected, LV_PART_MAIN | LV_STATE_USER_1); } void apply_bubble_text(lv_obj_t* label)