fix(pager): preserve voice hold and add chat selection

This commit is contained in:
liu weikai
2026-08-11 22:56:53 +08:00
parent 398200d90c
commit c7d4e9abcc
5 changed files with 163 additions and 7 deletions
@@ -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);
@@ -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
@@ -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<unsigned long>(tenths / 10U),
static_cast<unsigned long>(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<unsigned long>(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<int>(messages_.size()))
{
return false;
}
MessageItem& item = messages_[static_cast<size_t>(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<int>(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<int>(messages_.size()))
{
MessageItem& previous = messages_[static_cast<size_t>(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<int>(messages_.size()))
{
selected_message_index_ = -1;
return;
}
selected_message_index_ = index;
MessageItem& selected = messages_[static_cast<size_t>(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_,
@@ -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;
@@ -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)