diff --git a/docs/REFACTOR_PLAN.md b/docs/REFACTOR_PLAN.md index a4e5795..51bc4ca 100644 --- a/docs/REFACTOR_PLAN.md +++ b/docs/REFACTOR_PLAN.md @@ -193,11 +193,11 @@ Target files: Steps: -- [ ] Review every TODO in `src/`. -- [ ] Convert active TODOs into tests or tracked plan items. -- [ ] Remove stale TODOs whose behavior is already implemented. -- [ ] For pinned-message compatibility in `messages/operations.rs`, decide whether the fallback is still needed and document the decision in code or tests. -- [ ] Run `cargo test --all-features`. +- [x] Review every TODO in `src/`. +- [x] Convert active TODOs into tests or tracked plan items. +- [x] Remove stale TODOs whose behavior is already implemented. +- [x] For pinned-message compatibility in `messages/operations.rs`, decide whether the fallback is still needed and document the decision in code or tests. +- [x] Run `cargo test --all-features`. Acceptance criteria: diff --git a/src/input/handlers/chat.rs b/src/input/handlers/chat.rs index f4a3d3d..c7c8194 100644 --- a/src/input/handlers/chat.rs +++ b/src/input/handlers/chat.rs @@ -456,24 +456,3 @@ pub async fn handle_open_chat_keyboard_input(app: &mut App, _ => {} } } - -// TODO (Этап 4): Эти функции будут переписаны для модального просмотрщика -/* -#[cfg(feature = "images")] -fn collapse_photo(app: &mut App, msg_id: crate::types::MessageId) { - // Закомментировано - будет реализовано в Этапе 4 -} - -#[cfg(feature = "images")] -fn expand_photo(app: &mut App, msg_id: crate::types::MessageId, path: &str) { - // Закомментировано - будет реализовано в Этапе 4 -} -*/ - -// TODO (Этап 4): Функция _download_and_expand будет переписана -/* -#[cfg(feature = "images")] -async fn _download_and_expand(app: &mut App, msg_id: crate::types::MessageId, file_id: i32) { - // Закомментировано - будет реализовано в Этапе 4 -} -*/ diff --git a/src/input/key_handler.rs b/src/input/key_handler.rs deleted file mode 100644 index a6d1395..0000000 --- a/src/input/key_handler.rs +++ /dev/null @@ -1,450 +0,0 @@ -/// Модуль для обработки клавиш с использованием trait-based подхода -/// -/// Позволяет каждому экрану/режиму определить свою логику обработки клавиш, -/// избегая огромных match блоков в одном месте. - -use crate::app::App; -use crate::config::Command; -use crate::tdlib::{TdClient, TdClientTrait}; -use crossterm::event::KeyEvent; - -/// Результат обработки клавиши -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum KeyResult { - /// Клавиша обработана, продолжить работу - Handled, - - /// Клавиша обработана, нужна перерисовка UI - HandledNeedsRedraw, - - /// Клавиша не обработана (fallback на глобальные команды) - NotHandled, - - /// Выход из приложения - Quit, -} - -impl KeyResult { - /// Проверяет нужна ли перерисовка - pub fn needs_redraw(&self) -> bool { - matches!(self, KeyResult::HandledNeedsRedraw) - } - - /// Проверяет был ли запрос выхода - pub fn should_quit(&self) -> bool { - matches!(self, KeyResult::Quit) - } -} - -/// Trait для обработки клавиш на конкретном экране/в режиме -/// -/// # Examples -/// -/// ```ignore -/// struct ChatListHandler; -/// -/// impl KeyHandler for ChatListHandler { -/// fn handle_key( -/// &self, -/// app: &mut App, -/// key: KeyEvent, -/// command: Option, -/// ) -> KeyResult { -/// match command { -/// Some(Command::MoveUp) => { -/// app.move_chat_selection_up(); -/// KeyResult::HandledNeedsRedraw -/// } -/// Some(Command::OpenChat) => { -/// // Open selected chat -/// KeyResult::HandledNeedsRedraw -/// } -/// _ => KeyResult::NotHandled, -/// } -/// } -/// } -/// ``` -pub trait KeyHandler { - /// Обрабатывает нажатие клавиши - /// - /// # Arguments - /// - /// * `app` - Mutable reference на состояние приложения - /// * `key` - Событие клавиши от crossterm - /// * `command` - Опциональная команда из keybindings (если привязана) - /// - /// # Returns - /// - /// `KeyResult` - результат обработки (обработана/не обработана/выход) - fn handle_key( - &self, - app: &mut App, - key: KeyEvent, - command: Option, - ) -> KeyResult; - - /// Приоритет обработчика (для цепочки обработчиков) - /// - /// Обработчики с более высоким приоритетом вызываются первыми. - /// По умолчанию 0. - fn priority(&self) -> i32 { - 0 - } -} - -/// Глобальный обработчик клавиш (работает на всех экранах) -pub struct GlobalKeyHandler; - -impl KeyHandler for GlobalKeyHandler { - fn handle_key( - &self, - app: &mut App, - _key: KeyEvent, - command: Option, - ) -> KeyResult { - match command { - Some(Command::Quit) => KeyResult::Quit, - - Some(Command::OpenSearch) if !app.is_searching() => { - // TODO: implement enter_search_mode or use existing method - KeyResult::HandledNeedsRedraw - } - - Some(Command::Cancel) => { - // Cancel различных режимов - if app.is_searching() { - // TODO: implement exit_search_mode or use existing method - KeyResult::HandledNeedsRedraw - } else { - KeyResult::NotHandled - } - } - - _ => KeyResult::NotHandled, - } - } - - fn priority(&self) -> i32 { - -100 // Низкий приоритет - fallback для всех экранов - } -} - -/// Обработчик для списка чатов -pub struct ChatListKeyHandler; - -impl KeyHandler for ChatListKeyHandler { - fn handle_key( - &self, - app: &mut App, - _key: KeyEvent, - command: Option, - ) -> KeyResult { - match command { - Some(Command::MoveUp) => { - // TODO: implement chat selection navigation - // app.chat_list_state is ListState, use .select() - KeyResult::HandledNeedsRedraw - } - - Some(Command::MoveDown) => { - // TODO: implement chat selection navigation - KeyResult::HandledNeedsRedraw - } - - Some(Command::OpenChat) => { - // Обработка открытия чата будет в async контексте - // Здесь только возвращаем что команда распознана - KeyResult::HandledNeedsRedraw - } - - // Папки 1-9 - Some(Command::SelectFolder1) => { - app.set_selected_folder_id(Some(1)); - KeyResult::HandledNeedsRedraw - } - Some(Command::SelectFolder2) => { - app.set_selected_folder_id(Some(2)); - KeyResult::HandledNeedsRedraw - } - Some(Command::SelectFolder3) => { - app.set_selected_folder_id(Some(3)); - KeyResult::HandledNeedsRedraw - } - Some(Command::SelectFolder4) => { - app.set_selected_folder_id(Some(4)); - KeyResult::HandledNeedsRedraw - } - Some(Command::SelectFolder5) => { - app.set_selected_folder_id(Some(5)); - KeyResult::HandledNeedsRedraw - } - Some(Command::SelectFolder6) => { - app.set_selected_folder_id(Some(6)); - KeyResult::HandledNeedsRedraw - } - Some(Command::SelectFolder7) => { - app.set_selected_folder_id(Some(7)); - KeyResult::HandledNeedsRedraw - } - Some(Command::SelectFolder8) => { - app.set_selected_folder_id(Some(8)); - KeyResult::HandledNeedsRedraw - } - Some(Command::SelectFolder9) => { - app.set_selected_folder_id(Some(9)); - KeyResult::HandledNeedsRedraw - } - - _ => KeyResult::NotHandled, - } - } - - fn priority(&self) -> i32 { - 10 // Средний приоритет - } -} - -/// Обработчик для просмотра сообщений -pub struct MessageViewKeyHandler; - -impl KeyHandler for MessageViewKeyHandler { - fn handle_key( - &self, - app: &mut App, - _key: KeyEvent, - command: Option, - ) -> KeyResult { - match command { - Some(Command::MoveUp) => { - if app.message_view_state().message_scroll_offset > 0 { - app.message_view_state().message_scroll_offset -= 1; - KeyResult::HandledNeedsRedraw - } else { - KeyResult::Handled - } - } - - Some(Command::MoveDown) => { - app.message_view_state().message_scroll_offset += 1; - KeyResult::HandledNeedsRedraw - } - - Some(Command::PageUp) => { - app.message_view_state().message_scroll_offset = app.message_view_state().message_scroll_offset.saturating_sub(10); - KeyResult::HandledNeedsRedraw - } - - Some(Command::PageDown) => { - app.message_view_state().message_scroll_offset += 10; - KeyResult::HandledNeedsRedraw - } - - Some(Command::OpenSearchInChat) => { - // Открыть поиск в чате - KeyResult::HandledNeedsRedraw - } - - Some(Command::OpenProfile) => { - // Открыть профиль - KeyResult::HandledNeedsRedraw - } - - _ => KeyResult::NotHandled, - } - } - - fn priority(&self) -> i32 { - 10 // Средний приоритет - } -} - -/// Обработчик для режима выбора сообщения -pub struct MessageSelectionKeyHandler; - -impl KeyHandler for MessageSelectionKeyHandler { - fn handle_key( - &self, - _app: &mut App, - _key: KeyEvent, - command: Option, - ) -> KeyResult { - match command { - Some(Command::DeleteMessage) => { - // Показать модалку подтверждения удаления - KeyResult::HandledNeedsRedraw - } - - Some(Command::ReplyMessage) => { - // Войти в режим ответа - KeyResult::HandledNeedsRedraw - } - - Some(Command::ForwardMessage) => { - // Войти в режим пересылки - KeyResult::HandledNeedsRedraw - } - - Some(Command::CopyMessage) => { - // Скопировать текст в буфер - KeyResult::HandledNeedsRedraw - } - - Some(Command::ReactMessage) => { - // Открыть emoji picker - KeyResult::HandledNeedsRedraw - } - - Some(Command::Cancel) => { - // Выйти из режима выбора - KeyResult::HandledNeedsRedraw - } - - _ => KeyResult::NotHandled, - } - } - - fn priority(&self) -> i32 { - 20 // Высокий приоритет - режимы должны обрабатываться первыми - } -} - -/// Цепочка обработчиков клавиш -/// -/// Позволяет комбинировать несколько обработчиков в порядке приоритета. -pub struct KeyHandlerChain { - handlers: Vec<(i32, Box)>, -} - -impl KeyHandlerChain { - /// Создаёт новую цепочку - pub fn new() -> Self { - Self { - handlers: Vec::new(), - } - } - - /// Добавляет обработчик в цепочку - pub fn add(mut self, handler: H) -> Self { - let priority = handler.priority(); - self.handlers.push((priority, Box::new(handler))); - // Сортируем по убыванию приоритета - self.handlers.sort_by(|a, b| b.0.cmp(&a.0)); - self - } - - /// Обрабатывает клавишу, вызывая обработчики по порядку - /// - /// Останавливается на первом обработчике, который вернул Handled/HandledNeedsRedraw/Quit - pub fn handle( - &self, - app: &mut App, - key: KeyEvent, - command: Option, - ) -> KeyResult { - for (_priority, handler) in &self.handlers { - let result = handler.handle_key(app, key, command); - if result != KeyResult::NotHandled { - return result; - } - } - KeyResult::NotHandled - } -} - -impl Default for KeyHandlerChain { - fn default() -> Self { - Self::new() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crossterm::event::KeyCode; - - #[test] - fn test_key_result_needs_redraw() { - assert!(!KeyResult::Handled.needs_redraw()); - assert!(KeyResult::HandledNeedsRedraw.needs_redraw()); - assert!(!KeyResult::NotHandled.needs_redraw()); - assert!(!KeyResult::Quit.needs_redraw()); - } - - #[test] - fn test_key_result_should_quit() { - assert!(!KeyResult::Handled.should_quit()); - assert!(!KeyResult::HandledNeedsRedraw.should_quit()); - assert!(!KeyResult::NotHandled.should_quit()); - assert!(KeyResult::Quit.should_quit()); - } - - // TODO: Enable these tests after App trait integration - // #[test] - // fn test_global_handler_quit() { - // let handler = GlobalKeyHandler; - // let mut app = App::new_for_test(); - // - // let result = handler.handle_key( - // &mut app, - // KeyEvent::from(KeyCode::Char('q')), - // Some(Command::Quit), - // ); - // - // assert_eq!(result, KeyResult::Quit); - // } - - // #[test] - // fn test_chat_list_handler_navigation() { - // let handler = ChatListKeyHandler; - // let mut app = App::new_for_test(); - // - // // Test move up (should be handled even at top) - // let result = handler.handle_key( - // &mut app, - // KeyEvent::from(KeyCode::Up), - // Some(Command::MoveUp), - // ); - // - // assert_eq!(result, KeyResult::Handled); - // } - - // #[test] - // fn test_handler_chain() { - // let chain = KeyHandlerChain::new() - // .add(ChatListKeyHandler) - // .add(GlobalKeyHandler); - // - // let mut app = App::new_for_test(); - // - // // ChatListHandler should handle MoveUp first - // let result = chain.handle( - // &mut app, - // KeyEvent::from(KeyCode::Up), - // Some(Command::MoveUp), - // ); - // - // assert_eq!(result, KeyResult::Handled); - // - // // GlobalHandler should handle Quit - // let result = chain.handle( - // &mut app, - // KeyEvent::from(KeyCode::Char('q')), - // Some(Command::Quit), - // ); - // - // assert_eq!(result, KeyResult::Quit); - // } - - #[test] - fn test_handler_priority() { - let handler1 = ChatListKeyHandler; - let handler2 = MessageSelectionKeyHandler; - let handler3 = GlobalKeyHandler; - - assert_eq!(handler1.priority(), 10); - assert_eq!(handler2.priority(), 20); - assert_eq!(handler3.priority(), -100); - - // В цепочке должны быть отсортированы: MessageSelection > ChatList > Global - } -} diff --git a/src/tdlib/messages/operations.rs b/src/tdlib/messages/operations.rs index 7dd2635..fb1ea93 100644 --- a/src/tdlib/messages/operations.rs +++ b/src/tdlib/messages/operations.rs @@ -280,15 +280,13 @@ impl MessageManager { /// /// * `chat_id` - ID чата /// - /// # Note + /// # Compatibility /// - /// TODO: В tdlib-rs 1.8.29 поле `pinned_message_id` было удалено из `Chat`. - /// Нужно использовать `getChatPinnedMessage` или альтернативный способ. - /// Временно отключено, возвращает `None`. + /// The current `tdlib-rs` schema no longer exposes `Chat.pinned_message_id`, and the + /// generated wrapper does not provide `getChatPinnedMessage`. The pinned-message modal + /// uses `get_pinned_messages` with `SearchMessagesFilter::Pinned`; this method keeps the + /// legacy single-header state empty until TDLib exposes a direct top-pinned-message API. pub async fn load_current_pinned_message(&mut self, _chat_id: ChatId) { - // TODO: В tdlib-rs 1.8.29 поле pinned_message_id было удалено из Chat. - // Нужно использовать getChatPinnedMessage или альтернативный способ. - // Временно отключено. self.current_pinned_message = None; } diff --git a/src/tdlib/reactions.rs b/src/tdlib/reactions.rs index 9682aa4..39afe86 100644 --- a/src/tdlib/reactions.rs +++ b/src/tdlib/reactions.rs @@ -1,7 +1,7 @@ use crate::types::{ChatId, MessageId}; -use tdlib_rs::enums::ReactionType; +use tdlib_rs::enums::{AvailableReactions, ReactionType}; use tdlib_rs::functions; -use tdlib_rs::types::ReactionTypeEmoji; +use tdlib_rs::types::{AvailableReaction, ReactionTypeEmoji}; /// Менеджер реакций на сообщения. /// @@ -49,11 +49,6 @@ impl ReactionManager { /// * `Ok(Vec)` - Список доступных emoji реакций /// * `Err(String)` - Ошибка получения /// - /// # Note - /// - /// В tdlib-rs 1.8.29 структура AvailableReactions изменилась. - /// Временно возвращается стандартный набор из 12 популярных реакций. - /// /// # Examples /// /// ```ignore @@ -86,54 +81,15 @@ impl ReactionManager { .await; match reactions_result { - Ok(_available) => { - // TODO: В tdlib-rs 1.8.29 структура AvailableReactions изменилась - // Временно используем fallback на стандартные реакции - let emojis: Vec = Vec::new(); - - // let emojis: Vec = if let tdlib_rs::enums::AvailableReactions::AvailableReactions(ar) = available { - // ar.top_reactions.iter().filter_map(...).collect() - // } else { - // Vec::new() - // }; - + Ok(available) => { + let emojis = available_reaction_emojis(&available); if emojis.is_empty() { - // Фолбек на стандартные реакции - Ok(vec![ - "👍".to_string(), - "👎".to_string(), - "❤️".to_string(), - "🔥".to_string(), - "😊".to_string(), - "😢".to_string(), - "😮".to_string(), - "🎉".to_string(), - "🤔".to_string(), - "😡".to_string(), - "😎".to_string(), - "🤝".to_string(), - ]) + Ok(default_reaction_emojis()) } else { Ok(emojis) } } - Err(_) => { - // В случае ошибки возвращаем стандартный набор - Ok(vec![ - "👍".to_string(), - "👎".to_string(), - "❤️".to_string(), - "🔥".to_string(), - "😊".to_string(), - "😢".to_string(), - "😮".to_string(), - "🎉".to_string(), - "🤔".to_string(), - "😡".to_string(), - "😎".to_string(), - "🤝".to_string(), - ]) - } + Err(_) => Ok(default_reaction_emojis()), } } @@ -196,3 +152,79 @@ impl ReactionManager { } } } + +fn default_reaction_emojis() -> Vec { + vec![ + "👍".to_string(), + "👎".to_string(), + "❤️".to_string(), + "🔥".to_string(), + "😊".to_string(), + "😢".to_string(), + "😮".to_string(), + "🎉".to_string(), + "🤔".to_string(), + "😡".to_string(), + "😎".to_string(), + "🤝".to_string(), + ] +} + +fn available_reaction_emojis(available: &AvailableReactions) -> Vec { + let AvailableReactions::AvailableReactions(available) = available; + + available + .top_reactions + .iter() + .chain(available.recent_reactions.iter()) + .chain(available.popular_reactions.iter()) + .filter_map(reaction_emoji) + .fold(Vec::new(), |mut emojis, emoji| { + if !emojis.contains(&emoji) { + emojis.push(emoji); + } + emojis + }) +} + +fn reaction_emoji(reaction: &AvailableReaction) -> Option { + match &reaction.r#type { + ReactionType::Emoji(emoji) => Some(emoji.emoji.clone()), + ReactionType::CustomEmoji(_) => None, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tdlib_rs::types::{AvailableReaction, AvailableReactions as AvailableReactionsData}; + + fn emoji_reaction(emoji: &str) -> AvailableReaction { + AvailableReaction { + r#type: ReactionType::Emoji(ReactionTypeEmoji { emoji: emoji.to_string() }), + needs_premium: false, + } + } + + #[test] + fn extracts_unique_emoji_reactions_in_display_order() { + let available = AvailableReactions::AvailableReactions(AvailableReactionsData { + top_reactions: vec![emoji_reaction("👍"), emoji_reaction("🔥")], + recent_reactions: vec![emoji_reaction("🔥"), emoji_reaction("❤️")], + popular_reactions: vec![emoji_reaction("🎉")], + allow_custom_emoji: false, + are_tags: false, + unavailability_reason: None, + }); + + assert_eq!( + available_reaction_emojis(&available), + vec![ + "👍".to_string(), + "🔥".to_string(), + "❤️".to_string(), + "🎉".to_string(), + ] + ); + } +}