From 433233d7667932478885f34c6ad00f4605c0d0d6 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Fri, 30 Jan 2026 17:26:21 +0300 Subject: [PATCH] commit --- src/app/chat_state.rs | 162 +++++++ src/app/mod.rs | 430 ++++++++++-------- src/input/main_input.rs | 131 ++++-- src/ui/messages.rs | 68 ++- src/ui/profile.rs | 2 +- tests/helpers/app_builder.rs | 87 ++-- tests/input_field.rs | 2 +- tests/modals.rs | 30 +- .../input_field__input_editing_mode.snap | 2 +- .../modals__emoji_picker_default.snap | 2 +- .../modals__emoji_picker_with_selection.snap | 2 +- 11 files changed, 603 insertions(+), 315 deletions(-) create mode 100644 src/app/chat_state.rs diff --git a/src/app/chat_state.rs b/src/app/chat_state.rs new file mode 100644 index 0000000..56001e4 --- /dev/null +++ b/src/app/chat_state.rs @@ -0,0 +1,162 @@ +// Chat state management - type-safe state machine for chat modes + +use crate::tdlib::client::MessageInfo; +use crate::tdlib::ProfileInfo; + +/// Состояния чата - взаимоисключающие режимы работы с чатом +#[derive(Debug, Clone)] +pub enum ChatState { + /// Обычный режим - просмотр сообщений, набор текста + Normal, + + /// Выбор сообщения для действия (edit/delete/reply/forward/reaction) + MessageSelection { + /// Индекс выбранного сообщения (снизу вверх, 0 = последнее) + selected_index: usize, + }, + + /// Редактирование сообщения + Editing { + /// ID редактируемого сообщения + message_id: i64, + /// Индекс сообщения в списке + selected_index: usize, + }, + + /// Ответ на сообщение (reply) + Reply { + /// ID сообщения, на которое отвечаем + message_id: i64, + }, + + /// Пересылка сообщения (forward) + Forward { + /// ID сообщения для пересылки + message_id: i64, + /// Находимся в режиме выбора чата для пересылки + selecting_chat: bool, + }, + + /// Подтверждение удаления сообщения + DeleteConfirmation { + /// ID сообщения для удаления + message_id: i64, + }, + + /// Выбор реакции на сообщение + ReactionPicker { + /// ID сообщения для реакции + message_id: i64, + /// Список доступных реакций + available_reactions: Vec, + /// Индекс выбранной реакции в picker + selected_index: usize, + }, + + /// Просмотр профиля пользователя/чата + Profile { + /// Информация профиля + info: ProfileInfo, + /// Индекс выбранного действия + selected_action: usize, + /// Шаг подтверждения выхода из группы (0 = не показано, 1-2 = подтверждения) + leave_group_confirmation_step: u8, + }, + + /// Поиск по сообщениям в текущем чате + SearchInChat { + /// Поисковый запрос + query: String, + /// Результаты поиска + results: Vec, + /// Индекс выбранного результата + selected_index: usize, + }, + + /// Просмотр закреплённых сообщений + PinnedMessages { + /// Список закреплённых сообщений + messages: Vec, + /// Индекс выбранного pinned сообщения + selected_index: usize, + }, +} + +impl Default for ChatState { + fn default() -> Self { + ChatState::Normal + } +} + +impl ChatState { + /// Проверка: находимся в режиме выбора сообщения + pub fn is_message_selection(&self) -> bool { + matches!(self, ChatState::MessageSelection { .. }) + } + + /// Проверка: находимся в режиме редактирования + pub fn is_editing(&self) -> bool { + matches!(self, ChatState::Editing { .. }) + } + + /// Проверка: находимся в режиме ответа + pub fn is_reply(&self) -> bool { + matches!(self, ChatState::Reply { .. }) + } + + /// Проверка: находимся в режиме пересылки + pub fn is_forward(&self) -> bool { + matches!(self, ChatState::Forward { .. }) + } + + /// Проверка: показываем подтверждение удаления + pub fn is_delete_confirmation(&self) -> bool { + matches!(self, ChatState::DeleteConfirmation { .. }) + } + + /// Проверка: показываем reaction picker + pub fn is_reaction_picker(&self) -> bool { + matches!(self, ChatState::ReactionPicker { .. }) + } + + /// Проверка: показываем профиль + pub fn is_profile(&self) -> bool { + matches!(self, ChatState::Profile { .. }) + } + + /// Проверка: находимся в режиме поиска по сообщениям + pub fn is_search_in_chat(&self) -> bool { + matches!(self, ChatState::SearchInChat { .. }) + } + + /// Проверка: показываем pinned сообщения + pub fn is_pinned_mode(&self) -> bool { + matches!(self, ChatState::PinnedMessages { .. }) + } + + /// Проверка: находимся в обычном режиме + pub fn is_normal(&self) -> bool { + matches!(self, ChatState::Normal) + } + + /// Возвращает ID выбранного сообщения (если есть) + pub fn selected_message_id(&self) -> Option { + match self { + ChatState::Editing { message_id, .. } => Some(*message_id), + ChatState::Reply { message_id } => Some(*message_id), + ChatState::Forward { message_id, .. } => Some(*message_id), + ChatState::DeleteConfirmation { message_id } => Some(*message_id), + ChatState::ReactionPicker { message_id, .. } => Some(*message_id), + _ => None, + } + } + + /// Возвращает индекс выбранного сообщения (если есть) + pub fn selected_message_index(&self) -> Option { + match self { + ChatState::MessageSelection { selected_index } => Some(*selected_index), + ChatState::Editing { selected_index, .. } => Some(*selected_index), + _ => None, + } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 9ac1993..f6feffa 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,5 +1,7 @@ +mod chat_state; mod state; +pub use chat_state::ChatState; pub use state::AppScreen; use crate::tdlib::client::ChatInfo; @@ -10,6 +12,8 @@ pub struct App { pub config: crate::config::Config, pub screen: AppScreen, pub td_client: TdClient, + /// Состояние чата - type-safe state machine (новое!) + pub chat_state: ChatState, // Auth state pub phone_input: String, pub code_input: String, @@ -32,59 +36,9 @@ pub struct App { pub search_query: String, /// Флаг для оптимизации рендеринга - перерисовывать только при изменениях pub needs_redraw: bool, - // Edit message state - /// ID сообщения, которое редактируется (None = режим отправки нового) - pub editing_message_id: Option, - /// Индекс выбранного сообщения для навигации (снизу вверх, 0 = последнее) - pub selected_message_index: Option, - // Delete confirmation - /// ID сообщения для подтверждения удаления (показывает модалку) - pub confirm_delete_message_id: Option, - // Reply state - /// ID сообщения, на которое отвечаем (None = обычная отправка) - pub replying_to_message_id: Option, - // Forward state - /// ID сообщения для пересылки - pub forwarding_message_id: Option, - /// Режим выбора чата для пересылки - pub is_selecting_forward_chat: bool, // Typing indicator /// Время последней отправки typing status (для throttling) pub last_typing_sent: Option, - // Pinned messages mode - /// Режим просмотра закреплённых сообщений - pub is_pinned_mode: bool, - /// Список закреплённых сообщений - pub pinned_messages: Vec, - /// Индекс выбранного pinned сообщения - pub selected_pinned_index: usize, - // Message search mode - /// Режим поиска по сообщениям - pub is_message_search_mode: bool, - /// Поисковый запрос - pub message_search_query: String, - /// Результаты поиска - pub message_search_results: Vec, - /// Индекс выбранного результата - pub selected_search_result_index: usize, - // Profile mode - /// Режим просмотра профиля - pub is_profile_mode: bool, - /// Индекс выбранного действия в профиле - pub selected_profile_action: usize, - /// Шаг подтверждения выхода из группы (0 = не показано, 1 = первое, 2 = второе) - pub leave_group_confirmation_step: u8, - /// Информация профиля для отображения - pub profile_info: Option, - // Reaction picker mode - /// Режим выбора реакции - pub is_reaction_picker_mode: bool, - /// ID сообщения для добавления реакции - pub selected_message_for_reaction: Option, - /// Список доступных реакций - pub available_reactions: Vec, - /// Индекс выбранной реакции в picker - pub selected_reaction_index: usize, } impl App { @@ -96,6 +50,7 @@ impl App { config, screen: AppScreen::Loading, td_client: TdClient::new(), + chat_state: ChatState::Normal, phone_input: String::new(), code_input: String::new(), password_input: String::new(), @@ -112,28 +67,7 @@ impl App { is_searching: false, search_query: String::new(), needs_redraw: true, - editing_message_id: None, - selected_message_index: None, - confirm_delete_message_id: None, - replying_to_message_id: None, - forwarding_message_id: None, - is_selecting_forward_chat: false, last_typing_sent: None, - is_pinned_mode: false, - pinned_messages: Vec::new(), - selected_pinned_index: 0, - is_message_search_mode: false, - message_search_query: String::new(), - message_search_results: Vec::new(), - selected_search_result_index: 0, - is_profile_mode: false, - selected_profile_action: 0, - leave_group_confirmation_step: 0, - profile_info: None, - is_reaction_picker_mode: false, - selected_message_for_reaction: None, - available_reactions: Vec::new(), - selected_reaction_index: 0, } } @@ -187,24 +121,14 @@ impl App { self.message_input.clear(); self.cursor_position = 0; self.message_scroll_offset = 0; - self.editing_message_id = None; - self.selected_message_index = None; - self.replying_to_message_id = None; self.last_typing_sent = None; - // Сбрасываем pinned режим - self.is_pinned_mode = false; - self.pinned_messages.clear(); - self.selected_pinned_index = 0; + // Сбрасываем состояние чата в нормальный режим + self.chat_state = ChatState::Normal; // Очищаем данные в TdClient self.td_client.current_chat_id = None; self.td_client.current_chat_messages.clear(); self.td_client.typing_status = None; self.td_client.current_pinned_message = None; - // Сбрасываем режим поиска - self.is_message_search_mode = false; - self.message_search_query.clear(); - self.message_search_results.clear(); - self.selected_search_result_index = 0; } /// Начать выбор сообщения для редактирования (при стрелке вверх в пустом инпуте) @@ -213,7 +137,7 @@ impl App { return; } // Начинаем с последнего сообщения (индекс 0 = самое новое снизу) - self.selected_message_index = Some(0); + self.chat_state = ChatState::MessageSelection { selected_index: 0 }; } /// Выбрать предыдущее сообщение (вверх по списку = увеличить индекс) @@ -222,24 +146,25 @@ impl App { if total == 0 { return; } - self.selected_message_index = Some( - self.selected_message_index - .map(|i| (i + 1).min(total - 1)) - .unwrap_or(0), - ); + if let ChatState::MessageSelection { selected_index } = &mut self.chat_state { + *selected_index = (*selected_index + 1).min(total - 1); + } } /// Выбрать следующее сообщение (вниз по списку = уменьшить индекс) pub fn select_next_message(&mut self) { - self.selected_message_index = self - .selected_message_index - .map(|i| if i > 0 { Some(i - 1) } else { None }) - .flatten(); + if let ChatState::MessageSelection { selected_index } = &mut self.chat_state { + if *selected_index > 0 { + *selected_index -= 1; + } else { + self.chat_state = ChatState::Normal; + } + } } /// Получить выбранное сообщение pub fn get_selected_message(&self) -> Option<&crate::tdlib::client::MessageInfo> { - self.selected_message_index.and_then(|idx| { + self.chat_state.selected_message_index().and_then(|idx| { let total = self.td_client.current_chat_messages.len(); if total == 0 || idx >= total { return None; @@ -251,21 +176,33 @@ impl App { /// Начать редактирование выбранного сообщения pub fn start_editing_selected(&mut self) -> bool { + // Получаем selected_index из текущего состояния + let selected_idx = match &self.chat_state { + ChatState::MessageSelection { selected_index } => Some(*selected_index), + _ => None, + }; + + if selected_idx.is_none() { + return false; + } + // Сначала извлекаем данные из сообщения let msg_data = self.get_selected_message().and_then(|msg| { if msg.can_be_edited && msg.is_outgoing { - Some((msg.id, msg.content.clone())) + Some((msg.id, msg.content.clone(), selected_idx.unwrap())) } else { None } }); // Затем присваиваем - if let Some((id, content)) = msg_data { - self.editing_message_id = Some(id); + if let Some((id, content, idx)) = msg_data { self.cursor_position = content.chars().count(); self.message_input = content; - self.selected_message_index = None; + self.chat_state = ChatState::Editing { + message_id: id, + selected_index: idx, + }; return true; } false @@ -273,20 +210,19 @@ impl App { /// Отменить редактирование pub fn cancel_editing(&mut self) { - self.editing_message_id = None; - self.selected_message_index = None; + self.chat_state = ChatState::Normal; self.message_input.clear(); self.cursor_position = 0; } /// Проверить, находимся ли в режиме редактирования pub fn is_editing(&self) -> bool { - self.editing_message_id.is_some() + self.chat_state.is_editing() } /// Проверить, находимся ли в режиме выбора сообщения pub fn is_selecting_message(&self) -> bool { - self.selected_message_index.is_some() + self.chat_state.is_message_selection() } pub fn get_selected_chat_id(&self) -> Option { @@ -385,14 +321,15 @@ impl App { /// Проверить, показывается ли модалка подтверждения удаления pub fn is_confirm_delete_shown(&self) -> bool { - self.confirm_delete_message_id.is_some() + self.chat_state.is_delete_confirmation() } /// Начать режим ответа на выбранное сообщение pub fn start_reply_to_selected(&mut self) -> bool { if let Some(msg) = self.get_selected_message() { - self.replying_to_message_id = Some(msg.id); - self.selected_message_index = None; + self.chat_state = ChatState::Reply { + message_id: msg.id, + }; return true; } false @@ -400,17 +337,17 @@ impl App { /// Отменить режим ответа pub fn cancel_reply(&mut self) { - self.replying_to_message_id = None; + self.chat_state = ChatState::Normal; } /// Проверить, находимся ли в режиме ответа pub fn is_replying(&self) -> bool { - self.replying_to_message_id.is_some() + self.chat_state.is_reply() } /// Получить сообщение, на которое отвечаем pub fn get_replying_to_message(&self) -> Option<&crate::tdlib::client::MessageInfo> { - self.replying_to_message_id.and_then(|id| { + self.chat_state.selected_message_id().and_then(|id| { self.td_client .current_chat_messages .iter() @@ -421,9 +358,10 @@ impl App { /// Начать режим пересылки выбранного сообщения pub fn start_forward_selected(&mut self) -> bool { if let Some(msg) = self.get_selected_message() { - self.forwarding_message_id = Some(msg.id); - self.selected_message_index = None; - self.is_selecting_forward_chat = true; + self.chat_state = ChatState::Forward { + message_id: msg.id, + selecting_chat: true, + }; // Сбрасываем выбор чата на первый self.chat_list_state.select(Some(0)); return true; @@ -433,18 +371,20 @@ impl App { /// Отменить режим пересылки pub fn cancel_forward(&mut self) { - self.forwarding_message_id = None; - self.is_selecting_forward_chat = false; + self.chat_state = ChatState::Normal; } /// Проверить, находимся ли в режиме выбора чата для пересылки pub fn is_forwarding(&self) -> bool { - self.is_selecting_forward_chat && self.forwarding_message_id.is_some() + self.chat_state.is_forward() } /// Получить сообщение для пересылки pub fn get_forwarding_message(&self) -> Option<&crate::tdlib::client::MessageInfo> { - self.forwarding_message_id.and_then(|id| { + if !self.chat_state.is_forward() { + return None; + } + self.chat_state.selected_message_id().and_then(|id| { self.td_client .current_chat_messages .iter() @@ -456,44 +396,57 @@ impl App { /// Проверка режима pinned pub fn is_pinned_mode(&self) -> bool { - self.is_pinned_mode + self.chat_state.is_pinned_mode() } /// Войти в режим pinned (вызывается после загрузки pinned сообщений) pub fn enter_pinned_mode(&mut self, messages: Vec) { if !messages.is_empty() { - self.pinned_messages = messages; - self.selected_pinned_index = 0; - self.is_pinned_mode = true; + self.chat_state = ChatState::PinnedMessages { + messages, + selected_index: 0, + }; } } /// Выйти из режима pinned pub fn exit_pinned_mode(&mut self) { - self.is_pinned_mode = false; - self.pinned_messages.clear(); - self.selected_pinned_index = 0; + self.chat_state = ChatState::Normal; } /// Выбрать предыдущий pinned (вверх = более старый) pub fn select_previous_pinned(&mut self) { - if !self.pinned_messages.is_empty() - && self.selected_pinned_index < self.pinned_messages.len() - 1 + if let ChatState::PinnedMessages { + selected_index, + messages, + } = &mut self.chat_state { - self.selected_pinned_index += 1; + if *selected_index + 1 < messages.len() { + *selected_index += 1; + } } } /// Выбрать следующий pinned (вниз = более новый) pub fn select_next_pinned(&mut self) { - if self.selected_pinned_index > 0 { - self.selected_pinned_index -= 1; + if let ChatState::PinnedMessages { selected_index, .. } = &mut self.chat_state { + if *selected_index > 0 { + *selected_index -= 1; + } } } /// Получить текущее выбранное pinned сообщение pub fn get_selected_pinned(&self) -> Option<&crate::tdlib::client::MessageInfo> { - self.pinned_messages.get(self.selected_pinned_index) + if let ChatState::PinnedMessages { + messages, + selected_index, + } = &self.chat_state + { + messages.get(*selected_index) + } else { + None + } } /// Получить ID текущего pinned для перехода в историю @@ -505,51 +458,66 @@ impl App { /// Проверить, активен ли режим поиска по сообщениям pub fn is_message_search_mode(&self) -> bool { - self.is_message_search_mode + self.chat_state.is_search_in_chat() } /// Войти в режим поиска по сообщениям pub fn enter_message_search_mode(&mut self) { - self.is_message_search_mode = true; - self.message_search_query.clear(); - self.message_search_results.clear(); - self.selected_search_result_index = 0; + self.chat_state = ChatState::SearchInChat { + query: String::new(), + results: Vec::new(), + selected_index: 0, + }; } /// Выйти из режима поиска pub fn exit_message_search_mode(&mut self) { - self.is_message_search_mode = false; - self.message_search_query.clear(); - self.message_search_results.clear(); - self.selected_search_result_index = 0; + self.chat_state = ChatState::Normal; } /// Установить результаты поиска pub fn set_search_results(&mut self, results: Vec) { - self.message_search_results = results; - self.selected_search_result_index = 0; + if let ChatState::SearchInChat { results: r, selected_index, .. } = &mut self.chat_state { + *r = results; + *selected_index = 0; + } } /// Выбрать предыдущий результат (вверх) pub fn select_previous_search_result(&mut self) { - if self.selected_search_result_index > 0 { - self.selected_search_result_index -= 1; + if let ChatState::SearchInChat { selected_index, .. } = &mut self.chat_state { + if *selected_index > 0 { + *selected_index -= 1; + } } } /// Выбрать следующий результат (вниз) pub fn select_next_search_result(&mut self) { - if !self.message_search_results.is_empty() - && self.selected_search_result_index < self.message_search_results.len() - 1 + if let ChatState::SearchInChat { + selected_index, + results, + .. + } = &mut self.chat_state { - self.selected_search_result_index += 1; + if *selected_index + 1 < results.len() { + *selected_index += 1; + } } } /// Получить текущий выбранный результат pub fn get_selected_search_result(&self) -> Option<&crate::tdlib::client::MessageInfo> { - self.message_search_results - .get(self.selected_search_result_index) + if let ChatState::SearchInChat { + results, + selected_index, + .. + } = &self.chat_state + { + results.get(*selected_index) + } else { + None + } } /// Получить ID выбранного результата для перехода @@ -557,6 +525,40 @@ impl App { self.get_selected_search_result().map(|m| m.id) } + /// Получить поисковый запрос из режима поиска + pub fn get_search_query(&self) -> Option<&str> { + if let ChatState::SearchInChat { query, .. } = &self.chat_state { + Some(query.as_str()) + } else { + None + } + } + + /// Обновить поисковый запрос + pub fn update_search_query(&mut self, new_query: String) { + if let ChatState::SearchInChat { query, .. } = &mut self.chat_state { + *query = new_query; + } + } + + /// Получить индекс выбранного результата поиска + pub fn get_search_selected_index(&self) -> Option { + if let ChatState::SearchInChat { selected_index, .. } = &self.chat_state { + Some(*selected_index) + } else { + None + } + } + + /// Получить результаты поиска + pub fn get_search_results(&self) -> Option<&[crate::tdlib::client::MessageInfo]> { + if let ChatState::SearchInChat { results, .. } = &self.chat_state { + Some(results.as_slice()) + } else { + None + } + } + // === Draft Management === /// Получить черновик для текущего чата @@ -581,62 +583,118 @@ impl App { /// Проверить, активен ли режим профиля pub fn is_profile_mode(&self) -> bool { - self.is_profile_mode + self.chat_state.is_profile() } /// Войти в режим профиля - pub fn enter_profile_mode(&mut self) { - self.is_profile_mode = true; - self.selected_profile_action = 0; - self.leave_group_confirmation_step = 0; + pub fn enter_profile_mode(&mut self, info: crate::tdlib::ProfileInfo) { + self.chat_state = ChatState::Profile { + info, + selected_action: 0, + leave_group_confirmation_step: 0, + }; } /// Выйти из режима профиля pub fn exit_profile_mode(&mut self) { - self.is_profile_mode = false; - self.selected_profile_action = 0; - self.leave_group_confirmation_step = 0; - self.profile_info = None; + self.chat_state = ChatState::Normal; } /// Выбрать предыдущее действие pub fn select_previous_profile_action(&mut self) { - if self.selected_profile_action > 0 { - self.selected_profile_action -= 1; + if let ChatState::Profile { + selected_action, .. + } = &mut self.chat_state + { + if *selected_action > 0 { + *selected_action -= 1; + } } } /// Выбрать следующее действие pub fn select_next_profile_action(&mut self, max_actions: usize) { - if self.selected_profile_action < max_actions.saturating_sub(1) { - self.selected_profile_action += 1; + if let ChatState::Profile { + selected_action, .. + } = &mut self.chat_state + { + if *selected_action < max_actions.saturating_sub(1) { + *selected_action += 1; + } } } /// Показать первое подтверждение выхода из группы pub fn show_leave_group_confirmation(&mut self) { - self.leave_group_confirmation_step = 1; + if let ChatState::Profile { + leave_group_confirmation_step, + .. + } = &mut self.chat_state + { + *leave_group_confirmation_step = 1; + } } /// Показать второе подтверждение выхода из группы pub fn show_leave_group_final_confirmation(&mut self) { - self.leave_group_confirmation_step = 2; + if let ChatState::Profile { + leave_group_confirmation_step, + .. + } = &mut self.chat_state + { + *leave_group_confirmation_step = 2; + } } /// Отменить подтверждение выхода из группы pub fn cancel_leave_group(&mut self) { - self.leave_group_confirmation_step = 0; + if let ChatState::Profile { + leave_group_confirmation_step, + .. + } = &mut self.chat_state + { + *leave_group_confirmation_step = 0; + } } /// Получить текущий шаг подтверждения pub fn get_leave_group_confirmation_step(&self) -> u8 { - self.leave_group_confirmation_step + if let ChatState::Profile { + leave_group_confirmation_step, + .. + } = &self.chat_state + { + *leave_group_confirmation_step + } else { + 0 + } + } + + /// Получить информацию профиля + pub fn get_profile_info(&self) -> Option<&crate::tdlib::ProfileInfo> { + if let ChatState::Profile { info, .. } = &self.chat_state { + Some(info) + } else { + None + } + } + + /// Получить индекс выбранного действия в профиле + pub fn get_selected_profile_action(&self) -> Option { + if let ChatState::Profile { + selected_action, .. + } = &self.chat_state + { + Some(*selected_action) + } else { + None + } } // ========== Reaction Picker ========== pub fn is_reaction_picker_mode(&self) -> bool { - self.is_reaction_picker_mode + self.chat_state.is_reaction_picker() } pub fn enter_reaction_picker_mode( @@ -644,36 +702,52 @@ impl App { message_id: i64, available_reactions: Vec, ) { - self.is_reaction_picker_mode = true; - self.selected_message_for_reaction = Some(message_id); - self.available_reactions = available_reactions; - self.selected_reaction_index = 0; + self.chat_state = ChatState::ReactionPicker { + message_id, + available_reactions, + selected_index: 0, + }; } pub fn exit_reaction_picker_mode(&mut self) { - self.is_reaction_picker_mode = false; - self.selected_message_for_reaction = None; - self.available_reactions.clear(); - self.selected_reaction_index = 0; + self.chat_state = ChatState::Normal; } pub fn select_previous_reaction(&mut self) { - if !self.available_reactions.is_empty() && self.selected_reaction_index > 0 { - self.selected_reaction_index -= 1; + if let ChatState::ReactionPicker { selected_index, .. } = &mut self.chat_state { + if *selected_index > 0 { + *selected_index -= 1; + } } } pub fn select_next_reaction(&mut self) { - if self.selected_reaction_index + 1 < self.available_reactions.len() { - self.selected_reaction_index += 1; + if let ChatState::ReactionPicker { + selected_index, + available_reactions, + .. + } = &mut self.chat_state + { + if *selected_index + 1 < available_reactions.len() { + *selected_index += 1; + } } } pub fn get_selected_reaction(&self) -> Option<&String> { - self.available_reactions.get(self.selected_reaction_index) + if let ChatState::ReactionPicker { + available_reactions, + selected_index, + .. + } = &self.chat_state + { + available_reactions.get(*selected_index) + } else { + None + } } pub fn get_selected_message_for_reaction(&self) -> Option { - self.selected_message_for_reaction + self.chat_state.selected_message_id() } } diff --git a/src/input/main_input.rs b/src/input/main_input.rs index f54bf9c..a808d92 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -114,16 +114,16 @@ pub async fn handle(app: &mut App, key: KeyEvent) { app.select_previous_profile_action(); } KeyCode::Down => { - if let Some(profile) = &app.profile_info { + if let Some(profile) = app.get_profile_info() { let max_actions = get_available_actions_count(profile); app.select_next_profile_action(max_actions); } } KeyCode::Enter => { // Выполнить выбранное действие - if let Some(profile) = &app.profile_info { + if let Some(profile) = app.get_profile_info() { let actions = get_available_actions_count(profile); - let action_index = app.selected_profile_action; + let action_index = app.get_selected_profile_action().unwrap_or(0); if action_index < actions { // Определяем какое действие выбрано @@ -201,36 +201,42 @@ pub async fn handle(app: &mut App, key: KeyEvent) { } } KeyCode::Backspace => { - app.message_search_query.pop(); - // Выполняем поиск при изменении запроса - if let Some(chat_id) = app.get_selected_chat_id() { - if !app.message_search_query.is_empty() { + // Удаляем символ из запроса + if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) { + query.pop(); + app.update_search_query(query.clone()); + // Выполняем поиск при изменении запроса + if let Some(chat_id) = app.get_selected_chat_id() { + if !query.is_empty() { + if let Ok(Ok(results)) = timeout( + Duration::from_secs(3), + app.td_client.search_messages(chat_id, &query), + ) + .await + { + app.set_search_results(results); + } + } else { + app.set_search_results(Vec::new()); + } + } + } + } + KeyCode::Char(c) => { + // Добавляем символ к запросу + if let Some(mut query) = app.get_search_query().map(|s| s.to_string()) { + query.push(c); + app.update_search_query(query.clone()); + // Выполняем поиск при изменении запроса + if let Some(chat_id) = app.get_selected_chat_id() { if let Ok(Ok(results)) = timeout( Duration::from_secs(3), - app.td_client - .search_messages(chat_id, &app.message_search_query), + app.td_client.search_messages(chat_id, &query), ) .await { app.set_search_results(results); } - } else { - app.set_search_results(Vec::new()); - } - } - } - KeyCode::Char(c) => { - app.message_search_query.push(c); - // Выполняем поиск при изменении запроса - if let Some(chat_id) = app.get_selected_chat_id() { - if let Ok(Ok(results)) = timeout( - Duration::from_secs(3), - app.td_client - .search_messages(chat_id, &app.message_search_query), - ) - .await - { - app.set_search_results(results); } } } @@ -287,17 +293,30 @@ pub async fn handle(app: &mut App, key: KeyEvent) { } KeyCode::Up => { // Переход на ряд выше (8 эмодзи в ряду) - if app.selected_reaction_index >= 8 { - app.selected_reaction_index = app.selected_reaction_index.saturating_sub(8); - app.needs_redraw = true; + if let crate::app::ChatState::ReactionPicker { + selected_index, + .. + } = &mut app.chat_state + { + if *selected_index >= 8 { + *selected_index = selected_index.saturating_sub(8); + app.needs_redraw = true; + } } } KeyCode::Down => { // Переход на ряд ниже (8 эмодзи в ряду) - let new_index = app.selected_reaction_index + 8; - if new_index < app.available_reactions.len() { - app.selected_reaction_index = new_index; - app.needs_redraw = true; + if let crate::app::ChatState::ReactionPicker { + selected_index, + available_reactions, + .. + } = &mut app.chat_state + { + let new_index = *selected_index + 8; + if new_index < available_reactions.len() { + *selected_index = new_index; + app.needs_redraw = true; + } } } KeyCode::Enter => { @@ -351,7 +370,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { match key.code { KeyCode::Char('y') | KeyCode::Char('н') | KeyCode::Enter => { // Подтверждение удаления - if let Some(msg_id) = app.confirm_delete_message_id { + if let Some(msg_id) = app.chat_state.selected_message_id() { if let Some(chat_id) = app.get_selected_chat_id() { // Находим сообщение для проверки can_be_deleted_for_all_users let can_delete_for_all = app @@ -377,7 +396,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) { app.td_client .current_chat_messages .retain(|m| m.id != msg_id); - app.selected_message_index = None; + // Сбрасываем состояние + app.chat_state = crate::app::ChatState::Normal; } Ok(Err(e)) => { app.error_message = Some(e); @@ -388,11 +408,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) { } } } - app.confirm_delete_message_id = None; + // Закрываем модалку + app.chat_state = crate::app::ChatState::Normal; } KeyCode::Char('n') | KeyCode::Char('т') | KeyCode::Esc => { // Отмена удаления - app.confirm_delete_message_id = None; + app.chat_state = crate::app::ChatState::Normal; } _ => {} } @@ -411,7 +432,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(i) = app.chat_list_state.selected() { if let Some(chat) = filtered.get(i) { let to_chat_id = chat.id; - if let Some(msg_id) = app.forwarding_message_id { + if let Some(msg_id) = app.chat_state.selected_message_id() { if let Some(from_chat_id) = app.get_selected_chat_id() { match timeout( Duration::from_secs(5), @@ -528,7 +549,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Редактирование начато } else { // Нельзя редактировать это сообщение - app.selected_message_index = None; + app.chat_state = crate::app::ChatState::Normal; } return; } @@ -538,11 +559,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(chat_id) = app.get_selected_chat_id() { let text = app.message_input.clone(); - if let Some(msg_id) = app.editing_message_id { - // Режим редактирования - app.message_input.clear(); - app.cursor_position = 0; - app.editing_message_id = None; + if let Some(msg_id) = app.chat_state.selected_message_id() { + if app.is_editing() { + // Режим редактирования + app.message_input.clear(); + app.cursor_position = 0; + app.chat_state = crate::app::ChatState::Normal; match timeout( Duration::from_secs(5), @@ -570,9 +592,14 @@ pub async fn handle(app: &mut App, key: KeyEvent) { app.error_message = Some("Таймаут редактирования".to_string()); } } + } } else { // Обычная отправка (или reply) - let reply_to_id = app.replying_to_message_id; + let reply_to_id = if app.is_replying() { + app.chat_state.selected_message_id() + } else { + None + }; // Создаём ReplyInfo ДО отправки, пока сообщение точно доступно let reply_info = app.get_replying_to_message().map(|m| { crate::tdlib::client::ReplyInfo { @@ -583,7 +610,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) { }); app.message_input.clear(); app.cursor_position = 0; - app.replying_to_message_id = None; + // Сбрасываем режим reply если он был активен + if app.is_replying() { + app.chat_state = crate::app::ChatState::Normal; + } app.last_typing_sent = None; // Отменяем typing status @@ -665,7 +695,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if key.code == KeyCode::Esc { if app.is_selecting_message() { // Отменить выбор сообщения - app.selected_message_index = None; + app.chat_state = crate::app::ChatState::Normal; } else if app.is_editing() { // Отменить редактирование app.cancel_editing(); @@ -709,7 +739,9 @@ pub async fn handle(app: &mut App, key: KeyEvent) { let can_delete = msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users; if can_delete { - app.confirm_delete_message_id = Some(msg.id); + app.chat_state = crate::app::ChatState::DeleteConfirmation { + message_id: msg.id, + }; } } } @@ -789,8 +821,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await { Ok(Ok(profile)) => { - app.profile_info = Some(profile); - app.enter_profile_mode(); + app.enter_profile_mode(profile); app.status_message = None; } Ok(Err(e)) => { diff --git a/src/ui/messages.rs b/src/ui/messages.rs index 7317727..ed35e71 100644 --- a/src/ui/messages.rs +++ b/src/ui/messages.rs @@ -322,7 +322,7 @@ fn adjust_entities_for_substring( pub fn render(f: &mut Frame, area: Rect, app: &App) { // Режим профиля if app.is_profile_mode() { - if let Some(profile) = &app.profile_info { + if let Some(profile) = app.get_profile_info() { crate::ui::profile::render(f, area, app, profile); } return; @@ -964,18 +964,31 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } // Модалка выбора реакции - if app.is_reaction_picker_mode() { - render_reaction_picker_modal( - f, - area, - &app.available_reactions, - app.selected_reaction_index, - ); + if let crate::app::ChatState::ReactionPicker { + available_reactions, + selected_index, + .. + } = &app.chat_state + { + render_reaction_picker_modal(f, area, available_reactions, *selected_index); } } /// Рендерит режим поиска по сообщениям fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { + // Извлекаем данные из ChatState + let (query, results, selected_index) = + if let crate::app::ChatState::SearchInChat { + query, + results, + selected_index, + } = &app.chat_state + { + (query.as_str(), results.as_slice(), *selected_index) + } else { + return; // Некорректное состояние, не рендерим + }; + let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -986,14 +999,14 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { .split(area); // Search input - let total = app.message_search_results.len(); + let total = results.len(); let current = if total > 0 { - app.selected_search_result_index + 1 + selected_index + 1 } else { 0 }; - let input_line = if app.message_search_query.is_empty() { + let input_line = if query.is_empty() { Line::from(vec![ Span::styled("🔍 ", Style::default().fg(Color::Yellow)), Span::styled("█", Style::default().fg(Color::Yellow)), @@ -1002,7 +1015,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { } else { Line::from(vec![ Span::styled("🔍 ", Style::default().fg(Color::Yellow)), - Span::styled(&app.message_search_query, Style::default().fg(Color::White)), + Span::styled(query, Style::default().fg(Color::White)), Span::styled("█", Style::default().fg(Color::Yellow)), Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)), ]) @@ -1025,16 +1038,16 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { let content_width = chunks[1].width.saturating_sub(2) as usize; let mut lines: Vec = Vec::new(); - if app.message_search_results.is_empty() { - if !app.message_search_query.is_empty() { + if results.is_empty() { + if !query.is_empty() { lines.push(Line::from(Span::styled( "Ничего не найдено", Style::default().fg(Color::Gray), ))); } } else { - for (idx, msg) in app.message_search_results.iter().enumerate() { - let is_selected = idx == app.selected_search_result_index; + for (idx, msg) in results.iter().enumerate() { + let is_selected = idx == selected_index; // Пустая строка между результатами if idx > 0 { @@ -1101,7 +1114,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { // Скролл к выбранному результату let visible_height = chunks[1].height.saturating_sub(2) as usize; let lines_per_result = 4; - let selected_line = app.selected_search_result_index * lines_per_result; + let selected_line = selected_index * lines_per_result; let scroll_offset = if selected_line > visible_height / 2 { (selected_line - visible_height / 2) as u16 } else { @@ -1158,6 +1171,17 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { /// Рендерит режим просмотра закреплённых сообщений fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { + // Извлекаем данные из ChatState + let (messages, selected_index) = if let crate::app::ChatState::PinnedMessages { + messages, + selected_index, + } = &app.chat_state + { + (messages.as_slice(), *selected_index) + } else { + return; // Некорректное состояние + }; + let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ @@ -1168,8 +1192,8 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { .split(area); // Header - let total = app.pinned_messages.len(); - let current = app.selected_pinned_index + 1; + let total = messages.len(); + let current = selected_index + 1; let header_text = format!("📌 ЗАКРЕПЛЁННЫЕ ({}/{})", current, total); let header = Paragraph::new(header_text) .block( @@ -1188,8 +1212,8 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { let content_width = chunks[1].width.saturating_sub(2) as usize; let mut lines: Vec = Vec::new(); - for (idx, msg) in app.pinned_messages.iter().enumerate() { - let is_selected = idx == app.selected_pinned_index; + for (idx, msg) in messages.iter().enumerate() { + let is_selected = idx == selected_index; // Пустая строка между сообщениями if idx > 0 { @@ -1263,7 +1287,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { // Скролл к выбранному сообщению let visible_height = chunks[1].height.saturating_sub(2) as usize; let lines_per_msg = 5; // Примерно строк на сообщение - let selected_line = app.selected_pinned_index * lines_per_msg; + let selected_line = selected_index * lines_per_msg; let scroll_offset = if selected_line > visible_height / 2 { (selected_line - visible_height / 2) as u16 } else { diff --git a/src/ui/profile.rs b/src/ui/profile.rs index 225f0ac..99488f6 100644 --- a/src/ui/profile.rs +++ b/src/ui/profile.rs @@ -144,7 +144,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { let actions = get_available_actions(profile); for (idx, action) in actions.iter().enumerate() { - let is_selected = idx == app.selected_profile_action; + let is_selected = idx == app.get_selected_profile_action().unwrap_or(0); let marker = if is_selected { "▶ " } else { " " }; let style = if is_selected { Style::default() diff --git a/tests/helpers/app_builder.rs b/tests/helpers/app_builder.rs index 828c6dd..8870fb4 100644 --- a/tests/helpers/app_builder.rs +++ b/tests/helpers/app_builder.rs @@ -2,7 +2,7 @@ use ratatui::widgets::ListState; use std::collections::HashMap; -use tele_tui::app::{App, AppScreen}; +use tele_tui::app::{App, AppScreen, ChatState}; use tele_tui::config::Config; use tele_tui::tdlib::client::AuthState; use tele_tui::tdlib::{ChatInfo, MessageInfo}; @@ -21,17 +21,8 @@ pub struct TestAppBuilder { message_input: String, is_searching: bool, search_query: String, - editing_message_id: Option, - replying_to_message_id: Option, - is_reaction_picker_mode: bool, - is_profile_mode: bool, - confirm_delete_message_id: Option, + chat_state: Option, messages: HashMap>, - selected_message_index: Option, - message_search_mode: bool, - message_search_query: String, - forwarding_message_id: Option, - is_selecting_forward_chat: bool, status_message: Option, auth_state: Option, phone_input: Option, @@ -55,17 +46,8 @@ impl TestAppBuilder { message_input: String::new(), is_searching: false, search_query: String::new(), - editing_message_id: None, - replying_to_message_id: None, - is_reaction_picker_mode: false, - is_profile_mode: false, - confirm_delete_message_id: None, + chat_state: None, messages: HashMap::new(), - selected_message_index: None, - message_search_mode: false, - message_search_query: String::new(), - forwarding_message_id: None, - is_selecting_forward_chat: false, status_message: None, auth_state: None, phone_input: None, @@ -118,32 +100,43 @@ impl TestAppBuilder { } /// Режим редактирования сообщения - pub fn editing_message(mut self, message_id: i64) -> Self { - self.editing_message_id = Some(message_id); + pub fn editing_message(mut self, message_id: i64, selected_index: usize) -> Self { + self.chat_state = Some(ChatState::Editing { + message_id, + selected_index, + }); self } /// Режим ответа на сообщение pub fn replying_to(mut self, message_id: i64) -> Self { - self.replying_to_message_id = Some(message_id); + self.chat_state = Some(ChatState::Reply { message_id }); self } /// Режим выбора реакции - pub fn reaction_picker(mut self) -> Self { - self.is_reaction_picker_mode = true; + pub fn reaction_picker(mut self, message_id: i64, available_reactions: Vec) -> Self { + self.chat_state = Some(ChatState::ReactionPicker { + message_id, + available_reactions, + selected_index: 0, + }); self } /// Режим профиля - pub fn profile_mode(mut self) -> Self { - self.is_profile_mode = true; + pub fn profile_mode(mut self, info: tele_tui::tdlib::ProfileInfo) -> Self { + self.chat_state = Some(ChatState::Profile { + info, + selected_action: 0, + leave_group_confirmation_step: 0, + }); self } /// Подтверждение удаления pub fn delete_confirmation(mut self, message_id: i64) -> Self { - self.confirm_delete_message_id = Some(message_id); + self.chat_state = Some(ChatState::DeleteConfirmation { message_id }); self } @@ -166,22 +159,27 @@ impl TestAppBuilder { } /// Установить выбранное сообщение (режим selection) - pub fn selecting_message(mut self, message_index: usize) -> Self { - self.selected_message_index = Some(message_index); + pub fn selecting_message(mut self, selected_index: usize) -> Self { + self.chat_state = Some(ChatState::MessageSelection { selected_index }); self } /// Режим поиска по сообщениям в чате pub fn message_search(mut self, query: &str) -> Self { - self.message_search_mode = true; - self.message_search_query = query.to_string(); + self.chat_state = Some(ChatState::SearchInChat { + query: query.to_string(), + results: Vec::new(), + selected_index: 0, + }); self } /// Режим пересылки сообщения pub fn forward_mode(mut self, message_id: i64) -> Self { - self.forwarding_message_id = Some(message_id); - self.is_selecting_forward_chat = true; + self.chat_state = Some(ChatState::Forward { + message_id, + selecting_chat: true, + }); self } @@ -229,16 +227,10 @@ impl TestAppBuilder { app.message_input = self.message_input; app.is_searching = self.is_searching; app.search_query = self.search_query; - app.editing_message_id = self.editing_message_id; - app.replying_to_message_id = self.replying_to_message_id; - app.is_reaction_picker_mode = self.is_reaction_picker_mode; - app.is_profile_mode = self.is_profile_mode; - app.confirm_delete_message_id = self.confirm_delete_message_id; - app.selected_message_index = self.selected_message_index; - app.is_message_search_mode = self.message_search_mode; - app.message_search_query = self.message_search_query; - app.forwarding_message_id = self.forwarding_message_id; - app.is_selecting_forward_chat = self.is_selecting_forward_chat; + // Применяем chat_state если он установлен + if let Some(chat_state) = self.chat_state { + app.chat_state = chat_state; + } // Применяем status_message if let Some(status) = self.status_message { @@ -325,11 +317,12 @@ mod tests { #[test] fn test_builder_editing_mode() { let app = TestAppBuilder::new() - .editing_message(999) + .editing_message(999, 0) .message_input("Edited text") .build(); - assert_eq!(app.editing_message_id, Some(999)); + assert!(app.is_editing()); + assert_eq!(app.chat_state.selected_message_id(), Some(999)); assert_eq!(app.message_input, "Edited text"); } diff --git a/tests/input_field.rs b/tests/input_field.rs index fce8fe1..5570945 100644 --- a/tests/input_field.rs +++ b/tests/input_field.rs @@ -95,7 +95,7 @@ fn snapshot_input_editing_mode() { .with_chat(chat) .with_message(123, message) .selected_chat(123) - .editing_message(1) + .editing_message(1, 0) .message_input("Edited text here") .build(); diff --git a/tests/modals.rs b/tests/modals.rs index e19b533..f82303d 100644 --- a/tests/modals.rs +++ b/tests/modals.rs @@ -34,11 +34,13 @@ fn snapshot_emoji_picker_default() { let chat = create_test_chat("Mom", 123); let message = TestMessageBuilder::new("React to this", 1).build(); + let reactions = vec!["👍".to_string(), "👎".to_string(), "❤️".to_string(), "🔥".to_string(), "😊".to_string(), "😢".to_string(), "😮".to_string(), "🎉".to_string()]; + let app = TestAppBuilder::new() .with_chat(chat) .with_message(123, message) .selected_chat(123) - .reaction_picker() + .reaction_picker(1, reactions) .build(); let buffer = render_to_buffer(80, 24, |f| { @@ -54,15 +56,19 @@ fn snapshot_emoji_picker_with_selection() { let chat = create_test_chat("Mom", 123); let message = TestMessageBuilder::new("React to this", 1).build(); + let reactions = vec!["👍".to_string(), "👎".to_string(), "❤️".to_string(), "🔥".to_string(), "😊".to_string(), "😢".to_string(), "😮".to_string(), "🎉".to_string()]; + let mut app = TestAppBuilder::new() .with_chat(chat) .with_message(123, message) .selected_chat(123) - .reaction_picker() + .reaction_picker(1, reactions) .build(); // Выбираем 5-ю реакцию (индекс 4) - app.selected_reaction_index = 4; + if let tele_tui::app::ChatState::ReactionPicker { selected_index, .. } = &mut app.chat_state { + *selected_index = 4; + } let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::messages::render(f, f.area(), &app); @@ -77,14 +83,12 @@ fn snapshot_profile_personal_chat() { let chat = create_test_chat("Alice", 123); let profile = create_test_profile("Alice", 123); - let mut app = TestAppBuilder::new() + let app = TestAppBuilder::new() .with_chat(chat) .selected_chat(123) - .profile_mode() + .profile_mode(profile) .build(); - app.profile_info = Some(profile); - let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::messages::render(f, f.area(), &app); }); @@ -103,14 +107,12 @@ fn snapshot_profile_group_chat() { profile.member_count = Some(25); profile.description = Some("Work discussion group".to_string()); - let mut app = TestAppBuilder::new() + let app = TestAppBuilder::new() .with_chat(chat) .selected_chat(456) - .profile_mode() + .profile_mode(profile) .build(); - app.profile_info = Some(profile); - let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::messages::render(f, f.area(), &app); }); @@ -157,8 +159,10 @@ fn snapshot_search_in_chat() { .build(); // Устанавливаем результаты поиска - app.message_search_results = vec![msg1, msg2]; - app.selected_search_result_index = 0; + if let tele_tui::app::ChatState::SearchInChat { results, selected_index, .. } = &mut app.chat_state { + *results = vec![msg1, msg2]; + *selected_index = 0; + } let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::messages::render(f, f.area(), &app); diff --git a/tests/snapshots/input_field__input_editing_mode.snap b/tests/snapshots/input_field__input_editing_mode.snap index c8832aa..481c3ec 100644 --- a/tests/snapshots/input_field__input_editing_mode.snap +++ b/tests/snapshots/input_field__input_editing_mode.snap @@ -9,7 +9,7 @@ expression: output │ ──────── 02.01.2022 ──────── │ │ │ │ Вы ──────────────── │ -│ Original message text (14:33 ✓✓) │ +│ ▶ Original message text (14:33 ✓✓) │ │ │ │ │ │ │ diff --git a/tests/snapshots/modals__emoji_picker_default.snap b/tests/snapshots/modals__emoji_picker_default.snap index b3f621a..9819757 100644 --- a/tests/snapshots/modals__emoji_picker_default.snap +++ b/tests/snapshots/modals__emoji_picker_default.snap @@ -11,9 +11,9 @@ expression: output │User ──────────────── │ │ (14:33) React to this │ │ │ -│ │ │ ┌ Выбери реакцию ────────────────────────────────┐ │ │ │ │ │ +│ │ 👍 👎 ❤️ 🔥 😊 😢 😮 🎉 │ │ │ │ │ │ │ └────────────────────────────────────────────────┘ │ │ │ diff --git a/tests/snapshots/modals__emoji_picker_with_selection.snap b/tests/snapshots/modals__emoji_picker_with_selection.snap index b3f621a..9819757 100644 --- a/tests/snapshots/modals__emoji_picker_with_selection.snap +++ b/tests/snapshots/modals__emoji_picker_with_selection.snap @@ -11,9 +11,9 @@ expression: output │User ──────────────── │ │ (14:33) React to this │ │ │ -│ │ │ ┌ Выбери реакцию ────────────────────────────────┐ │ │ │ │ │ +│ │ 👍 👎 ❤️ 🔥 😊 😢 😮 🎉 │ │ │ │ │ │ │ └────────────────────────────────────────────────┘ │ │ │