From 264f18351051373c9aa664d6b4f214ba806a1f93 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Sun, 22 Feb 2026 17:09:51 +0300 Subject: [PATCH] style: auto-format entire codebase with cargo fmt (stable rustfmt.toml) --- benches/format_markdown.rs | 14 +- benches/formatting.rs | 9 +- benches/group_messages.rs | 13 +- rustfmt.toml | 9 - src/accounts/manager.rs | 15 +- src/app/chat_filter.rs | 20 +- src/app/methods/compose.rs | 10 +- src/app/methods/messages.rs | 14 +- src/app/methods/mod.rs | 12 +- src/app/methods/modal.rs | 70 +---- src/app/methods/navigation.rs | 2 +- src/app/methods/search.rs | 17 +- src/app/mod.rs | 29 +- src/audio/cache.rs | 3 +- src/audio/player.rs | 7 +- src/config/keybindings.rs | 291 +++++++++++--------- src/config/mod.rs | 48 +++- src/formatting.rs | 6 +- src/input/auth.rs | 6 +- src/input/handlers/chat.rs | 114 +++++--- src/input/handlers/chat_list.rs | 25 +- src/input/handlers/compose.rs | 19 +- src/input/handlers/global.rs | 5 +- src/input/handlers/mod.rs | 6 +- src/input/handlers/modal.rs | 117 ++++---- src/input/handlers/search.rs | 92 ++++--- src/input/main_input.rs | 37 ++- src/main.rs | 75 +++--- src/media/cache.rs | 8 +- src/media/image_renderer.rs | 14 +- src/message_grouping.rs | 10 +- src/notifications.rs | 26 +- src/tdlib/auth.rs | 5 +- src/tdlib/chat_helpers.rs | 10 +- src/tdlib/chats.rs | 34 +-- src/tdlib/client.rs | 121 ++++++--- src/tdlib/client_impl.rs | 29 +- src/tdlib/message_conversion.rs | 18 +- src/tdlib/message_converter.rs | 26 +- src/tdlib/messages/convert.rs | 11 +- src/tdlib/messages/mod.rs | 3 +- src/tdlib/messages/operations.rs | 77 +++--- src/tdlib/mod.rs | 2 +- src/tdlib/reactions.rs | 3 +- src/tdlib/trait.rs | 18 +- src/tdlib/types.rs | 34 +-- src/tdlib/update_handlers.rs | 42 +-- src/tdlib/users.rs | 11 +- src/types.rs | 2 +- src/ui/auth.rs | 2 +- src/ui/chat_list.rs | 6 +- src/ui/components/emoji_picker.rs | 12 +- src/ui/components/input_field.rs | 5 +- src/ui/components/message_bubble.rs | 125 ++++----- src/ui/components/message_list.rs | 9 +- src/ui/components/mod.rs | 14 +- src/ui/components/modal.rs | 5 +- src/ui/compose_bar.rs | 19 +- src/ui/footer.rs | 7 +- src/ui/messages.rs | 71 +++-- src/ui/mod.rs | 2 +- src/ui/modals/account_switcher.rs | 32 +-- src/ui/modals/delete_confirm.rs | 2 +- src/ui/modals/image_viewer.rs | 13 +- src/ui/modals/mod.rs | 4 +- src/ui/modals/pinned.rs | 18 +- src/ui/modals/reaction_picker.rs | 9 +- src/ui/modals/search.rs | 15 +- src/ui/profile.rs | 4 +- src/utils/mod.rs | 2 +- src/utils/retry.rs | 7 +- tests/account_switcher.rs | 18 +- tests/accounts.rs | 4 +- tests/chat_list.rs | 82 +++--- tests/config.rs | 25 +- tests/delete_message.rs | 60 ++++- tests/drafts.rs | 2 +- tests/e2e_user_journey.rs | 161 +++++------ tests/edit_message.rs | 59 +++- tests/helpers/app_builder.rs | 19 +- tests/helpers/fake_tdclient.rs | 403 ++++++++++++++++------------ tests/helpers/fake_tdclient_impl.rs | 38 ++- tests/helpers/test_data.rs | 6 +- tests/input_navigation.rs | 12 +- tests/modals.rs | 28 +- tests/network_typing.rs | 12 +- tests/reactions.rs | 50 +++- tests/reply_forward.rs | 27 +- tests/send_message.rs | 50 +++- tests/vim_mode.rs | 54 ++-- 90 files changed, 1632 insertions(+), 1450 deletions(-) diff --git a/benches/format_markdown.rs b/benches/format_markdown.rs index d26041a..e722f17 100644 --- a/benches/format_markdown.rs +++ b/benches/format_markdown.rs @@ -1,6 +1,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use tele_tui::formatting::format_text_with_entities; use tdlib_rs::enums::{TextEntity, TextEntityType}; +use tele_tui::formatting::format_text_with_entities; fn create_text_with_entities() -> (String, Vec) { let text = "This is bold and italic text with code and a link and mention".to_string(); @@ -41,9 +41,7 @@ fn benchmark_format_simple_text(c: &mut Criterion) { let entities = vec![]; c.bench_function("format_simple_text", |b| { - b.iter(|| { - format_text_with_entities(black_box(&text), black_box(&entities)) - }); + b.iter(|| format_text_with_entities(black_box(&text), black_box(&entities))); }); } @@ -51,9 +49,7 @@ fn benchmark_format_markdown_text(c: &mut Criterion) { let (text, entities) = create_text_with_entities(); c.bench_function("format_markdown_text", |b| { - b.iter(|| { - format_text_with_entities(black_box(&text), black_box(&entities)) - }); + b.iter(|| format_text_with_entities(black_box(&text), black_box(&entities))); }); } @@ -77,9 +73,7 @@ fn benchmark_format_long_text(c: &mut Criterion) { } c.bench_function("format_long_text_with_100_entities", |b| { - b.iter(|| { - format_text_with_entities(black_box(&text), black_box(&entities)) - }); + b.iter(|| format_text_with_entities(black_box(&text), black_box(&entities))); }); } diff --git a/benches/formatting.rs b/benches/formatting.rs index 029acca..bb84842 100644 --- a/benches/formatting.rs +++ b/benches/formatting.rs @@ -1,5 +1,5 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use tele_tui::utils::formatting::{format_timestamp_with_tz, format_date, get_day}; +use tele_tui::utils::formatting::{format_date, format_timestamp_with_tz, get_day}; fn benchmark_format_timestamp(c: &mut Criterion) { c.bench_function("format_timestamp_50_times", |b| { @@ -34,10 +34,5 @@ fn benchmark_get_day(c: &mut Criterion) { }); } -criterion_group!( - benches, - benchmark_format_timestamp, - benchmark_format_date, - benchmark_get_day -); +criterion_group!(benches, benchmark_format_timestamp, benchmark_format_date, benchmark_get_day); criterion_main!(benches); diff --git a/benches/group_messages.rs b/benches/group_messages.rs index 3925f5c..d4c604c 100644 --- a/benches/group_messages.rs +++ b/benches/group_messages.rs @@ -8,7 +8,10 @@ fn create_test_messages(count: usize) -> Vec { .map(|i| { let builder = MessageBuilder::new(MessageId::new(i as i64)) .sender_name(&format!("User{}", i % 10)) - .text(&format!("Test message number {} with some longer text to make it more realistic", i)) + .text(&format!( + "Test message number {} with some longer text to make it more realistic", + i + )) .date(1640000000 + (i as i32 * 60)); if i % 2 == 0 { @@ -24,9 +27,7 @@ fn benchmark_group_100_messages(c: &mut Criterion) { let messages = create_test_messages(100); c.bench_function("group_100_messages", |b| { - b.iter(|| { - group_messages(black_box(&messages)) - }); + b.iter(|| group_messages(black_box(&messages))); }); } @@ -34,9 +35,7 @@ fn benchmark_group_500_messages(c: &mut Criterion) { let messages = create_test_messages(500); c.bench_function("group_500_messages", |b| { - b.iter(|| { - group_messages(black_box(&messages)) - }); + b.iter(|| group_messages(black_box(&messages))); }); } diff --git a/rustfmt.toml b/rustfmt.toml index 1283a72..3f1638c 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -6,15 +6,6 @@ max_width = 100 tab_spaces = 4 newline_style = "Unix" -# Imports -imports_granularity = "Crate" -group_imports = "StdExternalCrate" - -# Comments -wrap_comments = true -comment_width = 80 -normalize_comments = true - # Formatting use_small_heuristics = "Default" fn_call_width = 80 diff --git a/src/accounts/manager.rs b/src/accounts/manager.rs index ee4e0ab..93e01cc 100644 --- a/src/accounts/manager.rs +++ b/src/accounts/manager.rs @@ -61,8 +61,8 @@ pub fn load_or_create() -> AccountsConfig { /// Saves `AccountsConfig` to `accounts.toml`. pub fn save(config: &AccountsConfig) -> Result<(), String> { - let config_path = accounts_config_path() - .ok_or_else(|| "Could not determine config directory".to_string())?; + let config_path = + accounts_config_path().ok_or_else(|| "Could not determine config directory".to_string())?; // Ensure parent directory exists if let Some(parent) = config_path.parent() { @@ -111,17 +111,10 @@ fn migrate_legacy() { // Move (rename) the directory match fs::rename(&legacy_path, &target) { Ok(()) => { - tracing::info!( - "Migrated ./tdlib_data/ -> {}", - target.display() - ); + tracing::info!("Migrated ./tdlib_data/ -> {}", target.display()); } Err(e) => { - tracing::error!( - "Could not migrate ./tdlib_data/ to {}: {}", - target.display(), - e - ); + tracing::error!("Could not migrate ./tdlib_data/ to {}: {}", target.display(), e); } } } diff --git a/src/app/chat_filter.rs b/src/app/chat_filter.rs index 094cad4..ec373be 100644 --- a/src/app/chat_filter.rs +++ b/src/app/chat_filter.rs @@ -6,7 +6,6 @@ /// - По статусу (archived, muted, и т.д.) /// /// Используется как в App, так и в UI слое для консистентной фильтрации. - use crate::tdlib::ChatInfo; /// Критерии фильтрации чатов @@ -42,18 +41,12 @@ impl ChatFilterCriteria { /// Фильтр только по папке pub fn by_folder(folder_id: Option) -> Self { - Self { - folder_id, - ..Default::default() - } + Self { folder_id, ..Default::default() } } /// Фильтр только по поисковому запросу pub fn by_search(query: String) -> Self { - Self { - search_query: Some(query), - ..Default::default() - } + Self { search_query: Some(query), ..Default::default() } } /// Builder: установить папку @@ -176,10 +169,7 @@ impl ChatFilter { /// /// let filtered = ChatFilter::filter(&all_chats, &criteria); /// ``` - pub fn filter<'a>( - chats: &'a [ChatInfo], - criteria: &ChatFilterCriteria, - ) -> Vec<&'a ChatInfo> { + pub fn filter<'a>(chats: &'a [ChatInfo], criteria: &ChatFilterCriteria) -> Vec<&'a ChatInfo> { chats.iter().filter(|chat| criteria.matches(chat)).collect() } @@ -309,8 +299,7 @@ mod tests { let filtered = ChatFilter::filter(&chats, &criteria); assert_eq!(filtered.len(), 2); // Chat 1 and Chat 3 have unread - let criteria = ChatFilterCriteria::new() - .pinned_only(true); + let criteria = ChatFilterCriteria::new().pinned_only(true); let filtered = ChatFilter::filter(&chats, &criteria); assert_eq!(filtered.len(), 1); // Only Chat 1 is pinned @@ -330,5 +319,4 @@ mod tests { assert_eq!(ChatFilter::count_unread(&chats, &criteria), 15); // 5 + 10 assert_eq!(ChatFilter::count_unread_mentions(&chats, &criteria), 3); // 1 + 2 } - } diff --git a/src/app/methods/compose.rs b/src/app/methods/compose.rs index 34dae41..9461a34 100644 --- a/src/app/methods/compose.rs +++ b/src/app/methods/compose.rs @@ -2,8 +2,8 @@ //! //! Handles reply, forward, and draft functionality -use crate::app::{App, ChatState}; use crate::app::methods::messages::MessageMethods; +use crate::app::{App, ChatState}; use crate::tdlib::{MessageInfo, TdClientTrait}; /// Compose methods for reply/forward/draft @@ -44,9 +44,7 @@ pub trait ComposeMethods { impl ComposeMethods for App { fn start_reply_to_selected(&mut self) -> bool { if let Some(msg) = self.get_selected_message() { - self.chat_state = ChatState::Reply { - message_id: msg.id(), - }; + self.chat_state = ChatState::Reply { message_id: msg.id() }; return true; } false @@ -72,9 +70,7 @@ impl ComposeMethods for App { fn start_forward_selected(&mut self) -> bool { if let Some(msg) = self.get_selected_message() { - self.chat_state = ChatState::Forward { - message_id: msg.id(), - }; + self.chat_state = ChatState::Forward { message_id: msg.id() }; // Сбрасываем выбор чата на первый self.chat_list_state.select(Some(0)); return true; diff --git a/src/app/methods/messages.rs b/src/app/methods/messages.rs index c4a2b5c..bba3fe3 100644 --- a/src/app/methods/messages.rs +++ b/src/app/methods/messages.rs @@ -61,8 +61,7 @@ impl MessageMethods for App { // Перескакиваем через все сообщения текущего альбома назад let mut new_index = *selected_index - 1; if current_album_id != 0 { - while new_index > 0 - && messages[new_index].media_album_id() == current_album_id + while new_index > 0 && messages[new_index].media_album_id() == current_album_id { new_index -= 1; } @@ -125,9 +124,9 @@ impl MessageMethods for App { } fn get_selected_message(&self) -> Option { - self.chat_state.selected_message_index().and_then(|idx| { - self.td_client.current_chat_messages().get(idx).cloned() - }) + self.chat_state + .selected_message_index() + .and_then(|idx| self.td_client.current_chat_messages().get(idx).cloned()) } fn start_editing_selected(&mut self) -> bool { @@ -158,10 +157,7 @@ impl MessageMethods for App { if let Some((id, content, idx)) = msg_data { self.cursor_position = content.chars().count(); self.message_input = content; - self.chat_state = ChatState::Editing { - message_id: id, - selected_index: idx, - }; + self.chat_state = ChatState::Editing { message_id: id, selected_index: idx }; return true; } false diff --git a/src/app/methods/mod.rs b/src/app/methods/mod.rs index f398849..7b4dcf0 100644 --- a/src/app/methods/mod.rs +++ b/src/app/methods/mod.rs @@ -7,14 +7,14 @@ //! - search: Search in chats and messages //! - modal: Modal dialogs (Profile, Pinned, Reactions, Delete) -pub mod navigation; -pub mod messages; pub mod compose; -pub mod search; +pub mod messages; pub mod modal; +pub mod navigation; +pub mod search; -pub use navigation::NavigationMethods; -pub use messages::MessageMethods; pub use compose::ComposeMethods; -pub use search::SearchMethods; +pub use messages::MessageMethods; pub use modal::ModalMethods; +pub use navigation::NavigationMethods; +pub use search::SearchMethods; diff --git a/src/app/methods/modal.rs b/src/app/methods/modal.rs index f6160f4..2c3c102 100644 --- a/src/app/methods/modal.rs +++ b/src/app/methods/modal.rs @@ -106,10 +106,7 @@ impl ModalMethods for App { fn enter_pinned_mode(&mut self, messages: Vec) { if !messages.is_empty() { - self.chat_state = ChatState::PinnedMessages { - messages, - selected_index: 0, - }; + self.chat_state = ChatState::PinnedMessages { messages, selected_index: 0 }; } } @@ -118,11 +115,7 @@ impl ModalMethods for App { } fn select_previous_pinned(&mut self) { - if let ChatState::PinnedMessages { - selected_index, - messages, - } = &mut self.chat_state - { + if let ChatState::PinnedMessages { selected_index, messages } = &mut self.chat_state { if *selected_index + 1 < messages.len() { *selected_index += 1; } @@ -138,11 +131,7 @@ impl ModalMethods for App { } fn get_selected_pinned(&self) -> Option<&MessageInfo> { - if let ChatState::PinnedMessages { - messages, - selected_index, - } = &self.chat_state - { + if let ChatState::PinnedMessages { messages, selected_index } = &self.chat_state { messages.get(*selected_index) } else { None @@ -170,10 +159,7 @@ impl ModalMethods for App { } fn select_previous_profile_action(&mut self) { - if let ChatState::Profile { - selected_action, .. - } = &mut self.chat_state - { + if let ChatState::Profile { selected_action, .. } = &mut self.chat_state { if *selected_action > 0 { *selected_action -= 1; } @@ -181,10 +167,7 @@ impl ModalMethods for App { } fn select_next_profile_action(&mut self, max_actions: usize) { - if let ChatState::Profile { - selected_action, .. - } = &mut self.chat_state - { + if let ChatState::Profile { selected_action, .. } = &mut self.chat_state { if *selected_action < max_actions.saturating_sub(1) { *selected_action += 1; } @@ -192,41 +175,25 @@ impl ModalMethods for App { } fn show_leave_group_confirmation(&mut self) { - if let ChatState::Profile { - leave_group_confirmation_step, - .. - } = &mut self.chat_state - { + if let ChatState::Profile { leave_group_confirmation_step, .. } = &mut self.chat_state { *leave_group_confirmation_step = 1; } } fn show_leave_group_final_confirmation(&mut self) { - if let ChatState::Profile { - leave_group_confirmation_step, - .. - } = &mut self.chat_state - { + if let ChatState::Profile { leave_group_confirmation_step, .. } = &mut self.chat_state { *leave_group_confirmation_step = 2; } } fn cancel_leave_group(&mut self) { - if let ChatState::Profile { - leave_group_confirmation_step, - .. - } = &mut self.chat_state - { + if let ChatState::Profile { leave_group_confirmation_step, .. } = &mut self.chat_state { *leave_group_confirmation_step = 0; } } fn get_leave_group_confirmation_step(&self) -> u8 { - if let ChatState::Profile { - leave_group_confirmation_step, - .. - } = &self.chat_state - { + if let ChatState::Profile { leave_group_confirmation_step, .. } = &self.chat_state { *leave_group_confirmation_step } else { 0 @@ -242,10 +209,7 @@ impl ModalMethods for App { } fn get_selected_profile_action(&self) -> Option { - if let ChatState::Profile { - selected_action, .. - } = &self.chat_state - { + if let ChatState::Profile { selected_action, .. } = &self.chat_state { Some(*selected_action) } else { None @@ -277,11 +241,8 @@ impl ModalMethods for App { } fn select_next_reaction(&mut self) { - if let ChatState::ReactionPicker { - selected_index, - available_reactions, - .. - } = &mut self.chat_state + if let ChatState::ReactionPicker { selected_index, available_reactions, .. } = + &mut self.chat_state { if *selected_index + 1 < available_reactions.len() { *selected_index += 1; @@ -290,11 +251,8 @@ impl ModalMethods for App { } fn get_selected_reaction(&self) -> Option<&String> { - if let ChatState::ReactionPicker { - available_reactions, - selected_index, - .. - } = &self.chat_state + if let ChatState::ReactionPicker { available_reactions, selected_index, .. } = + &self.chat_state { available_reactions.get(*selected_index) } else { diff --git a/src/app/methods/navigation.rs b/src/app/methods/navigation.rs index da4581c..a9ad35d 100644 --- a/src/app/methods/navigation.rs +++ b/src/app/methods/navigation.rs @@ -2,8 +2,8 @@ //! //! Handles chat list navigation and selection -use crate::app::{App, ChatState, InputMode}; use crate::app::methods::search::SearchMethods; +use crate::app::{App, ChatState, InputMode}; use crate::tdlib::TdClientTrait; /// Navigation methods for chat list diff --git a/src/app/methods/search.rs b/src/app/methods/search.rs index e21da36..f7adbba 100644 --- a/src/app/methods/search.rs +++ b/src/app/methods/search.rs @@ -71,8 +71,7 @@ impl SearchMethods for App { fn get_filtered_chats(&self) -> Vec<&ChatInfo> { // Используем ChatFilter для централизованной фильтрации - let mut criteria = ChatFilterCriteria::new() - .with_folder(self.selected_folder_id); + let mut criteria = ChatFilterCriteria::new().with_folder(self.selected_folder_id); if !self.search_query.is_empty() { criteria = criteria.with_search(self.search_query.clone()); @@ -113,12 +112,7 @@ impl SearchMethods for App { } fn select_next_search_result(&mut self) { - if let ChatState::SearchInChat { - selected_index, - results, - .. - } = &mut self.chat_state - { + if let ChatState::SearchInChat { selected_index, results, .. } = &mut self.chat_state { if *selected_index + 1 < results.len() { *selected_index += 1; } @@ -126,12 +120,7 @@ impl SearchMethods for App { } fn get_selected_search_result(&self) -> Option<&MessageInfo> { - if let ChatState::SearchInChat { - results, - selected_index, - .. - } = &self.chat_state - { + if let ChatState::SearchInChat { results, selected_index, .. } = &self.chat_state { results.get(*selected_index) } else { None diff --git a/src/app/mod.rs b/src/app/mod.rs index 8f94c73..623236a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -5,13 +5,13 @@ mod chat_filter; mod chat_state; -mod state; pub mod methods; +mod state; pub use chat_filter::{ChatFilter, ChatFilterCriteria}; pub use chat_state::{ChatState, InputMode}; -pub use state::AppScreen; pub use methods::*; +pub use state::AppScreen; use crate::accounts::AccountProfile; use crate::tdlib::{ChatInfo, TdClient, TdClientTrait}; @@ -165,9 +165,7 @@ impl App { let audio_cache_size_mb = config.audio.cache_size_mb; #[cfg(feature = "images")] - let image_cache = Some(crate::media::cache::ImageCache::new( - config.images.cache_size_mb, - )); + let image_cache = Some(crate::media::cache::ImageCache::new(config.images.cache_size_mb)); #[cfg(feature = "images")] let inline_image_renderer = crate::media::image_renderer::ImageRenderer::new_fast(); #[cfg(feature = "images")] @@ -275,11 +273,8 @@ impl App { /// Navigate to next item in account switcher list. pub fn account_switcher_select_next(&mut self) { - if let Some(AccountSwitcherState::SelectAccount { - accounts, - selected_index, - .. - }) = &mut self.account_switcher + if let Some(AccountSwitcherState::SelectAccount { accounts, selected_index, .. }) = + &mut self.account_switcher { // +1 for the "Add account" item at the end let max_index = accounts.len(); @@ -372,20 +367,6 @@ impl App { .and_then(|id| self.chats.iter().find(|c| c.id == id)) } - - - - - - - - - - - - - - // ========== Getter/Setter методы для инкапсуляции ========== // Config diff --git a/src/audio/cache.rs b/src/audio/cache.rs index 9861284..13a5374 100644 --- a/src/audio/cache.rs +++ b/src/audio/cache.rs @@ -97,8 +97,7 @@ impl VoiceCache { /// Evicts a specific file from cache fn evict(&mut self, file_id: &str) -> Result<(), String> { if let Some((path, _, _)) = self.files.remove(file_id) { - fs::remove_file(&path) - .map_err(|e| format!("Failed to remove cached file: {}", e))?; + fs::remove_file(&path).map_err(|e| format!("Failed to remove cached file: {}", e))?; } Ok(()) } diff --git a/src/audio/player.rs b/src/audio/player.rs index 1805727..a18f7f0 100644 --- a/src/audio/player.rs +++ b/src/audio/player.rs @@ -58,7 +58,8 @@ impl AudioPlayer { let mut cmd = Command::new("ffplay"); cmd.arg("-nodisp") .arg("-autoexit") - .arg("-loglevel").arg("quiet"); + .arg("-loglevel") + .arg("quiet"); if start_secs > 0.0 { cmd.arg("-ss").arg(format!("{:.1}", start_secs)); @@ -132,9 +133,7 @@ impl AudioPlayer { .arg("-CONT") .arg(pid.to_string()) .output(); - let _ = Command::new("kill") - .arg(pid.to_string()) - .output(); + let _ = Command::new("kill").arg(pid.to_string()).output(); } *self.paused.lock().unwrap() = false; } diff --git a/src/config/keybindings.rs b/src/config/keybindings.rs index e2e7833..58bf00d 100644 --- a/src/config/keybindings.rs +++ b/src/config/keybindings.rs @@ -4,7 +4,6 @@ /// - Загрузку из конфигурационного файла /// - Множественные binding для одной команды (EN/RU раскладки) /// - Type-safe команды через enum - use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -49,12 +48,12 @@ pub enum Command { SelectMessage, // Media - ViewImage, // v - просмотр фото + ViewImage, // v - просмотр фото // Voice playback - TogglePlayback, // Space - play/pause - SeekForward, // → - seek +5s - SeekBackward, // ← - seek -5s + TogglePlayback, // Space - play/pause + SeekForward, // → - seek +5s + SeekBackward, // ← - seek -5s // Input SubmitMessage, @@ -83,31 +82,19 @@ pub struct KeyBinding { impl KeyBinding { pub fn new(key: KeyCode) -> Self { - Self { - key, - modifiers: KeyModifiers::NONE, - } + Self { key, modifiers: KeyModifiers::NONE } } pub fn with_ctrl(key: KeyCode) -> Self { - Self { - key, - modifiers: KeyModifiers::CONTROL, - } + Self { key, modifiers: KeyModifiers::CONTROL } } pub fn with_shift(key: KeyCode) -> Self { - Self { - key, - modifiers: KeyModifiers::SHIFT, - } + Self { key, modifiers: KeyModifiers::SHIFT } } pub fn with_alt(key: KeyCode) -> Self { - Self { - key, - modifiers: KeyModifiers::ALT, - } + Self { key, modifiers: KeyModifiers::ALT } } pub fn matches(&self, event: &KeyEvent) -> bool { @@ -128,50 +115,65 @@ impl Keybindings { let mut bindings = HashMap::new(); // Navigation - bindings.insert(Command::MoveUp, vec![ - KeyBinding::new(KeyCode::Up), - KeyBinding::new(KeyCode::Char('k')), - KeyBinding::new(KeyCode::Char('р')), // RU (custom mapping, not standard ЙЦУКЕН) - ]); - bindings.insert(Command::MoveDown, vec![ - KeyBinding::new(KeyCode::Down), - KeyBinding::new(KeyCode::Char('j')), - KeyBinding::new(KeyCode::Char('о')), // RU - ]); - bindings.insert(Command::MoveLeft, vec![ - KeyBinding::new(KeyCode::Left), - KeyBinding::new(KeyCode::Char('h')), - KeyBinding::new(KeyCode::Char('л')), // RU (custom mapping, not standard ЙЦУКЕН) - ]); - bindings.insert(Command::MoveRight, vec![ - KeyBinding::new(KeyCode::Right), - KeyBinding::new(KeyCode::Char('l')), - KeyBinding::new(KeyCode::Char('д')), // RU - ]); - bindings.insert(Command::PageUp, vec![ - KeyBinding::new(KeyCode::PageUp), - KeyBinding::with_ctrl(KeyCode::Char('u')), - ]); - bindings.insert(Command::PageDown, vec![ - KeyBinding::new(KeyCode::PageDown), - KeyBinding::with_ctrl(KeyCode::Char('d')), - ]); + bindings.insert( + Command::MoveUp, + vec![ + KeyBinding::new(KeyCode::Up), + KeyBinding::new(KeyCode::Char('k')), + KeyBinding::new(KeyCode::Char('р')), // RU (custom mapping, not standard ЙЦУКЕН) + ], + ); + bindings.insert( + Command::MoveDown, + vec![ + KeyBinding::new(KeyCode::Down), + KeyBinding::new(KeyCode::Char('j')), + KeyBinding::new(KeyCode::Char('о')), // RU + ], + ); + bindings.insert( + Command::MoveLeft, + vec![ + KeyBinding::new(KeyCode::Left), + KeyBinding::new(KeyCode::Char('h')), + KeyBinding::new(KeyCode::Char('л')), // RU (custom mapping, not standard ЙЦУКЕН) + ], + ); + bindings.insert( + Command::MoveRight, + vec![ + KeyBinding::new(KeyCode::Right), + KeyBinding::new(KeyCode::Char('l')), + KeyBinding::new(KeyCode::Char('д')), // RU + ], + ); + bindings.insert( + Command::PageUp, + vec![ + KeyBinding::new(KeyCode::PageUp), + KeyBinding::with_ctrl(KeyCode::Char('u')), + ], + ); + bindings.insert( + Command::PageDown, + vec![ + KeyBinding::new(KeyCode::PageDown), + KeyBinding::with_ctrl(KeyCode::Char('d')), + ], + ); // Global - bindings.insert(Command::Quit, vec![ - KeyBinding::new(KeyCode::Char('q')), - KeyBinding::new(KeyCode::Char('й')), // RU - KeyBinding::with_ctrl(KeyCode::Char('c')), - ]); - bindings.insert(Command::OpenSearch, vec![ - KeyBinding::with_ctrl(KeyCode::Char('s')), - ]); - bindings.insert(Command::OpenSearchInChat, vec![ - KeyBinding::with_ctrl(KeyCode::Char('f')), - ]); - bindings.insert(Command::Help, vec![ - KeyBinding::new(KeyCode::Char('?')), - ]); + bindings.insert( + Command::Quit, + vec![ + KeyBinding::new(KeyCode::Char('q')), + KeyBinding::new(KeyCode::Char('й')), // RU + KeyBinding::with_ctrl(KeyCode::Char('c')), + ], + ); + bindings.insert(Command::OpenSearch, vec![KeyBinding::with_ctrl(KeyCode::Char('s'))]); + bindings.insert(Command::OpenSearchInChat, vec![KeyBinding::with_ctrl(KeyCode::Char('f'))]); + bindings.insert(Command::Help, vec![KeyBinding::new(KeyCode::Char('?'))]); // Chat list // Note: Enter обрабатывается через Command::SubmitMessage в handle_enter_key() @@ -188,90 +190,114 @@ impl Keybindings { 9 => Command::SelectFolder9, _ => unreachable!(), }; - bindings.insert(cmd, vec![ - KeyBinding::new(KeyCode::Char(char::from_digit(i, 10).unwrap())), - ]); + bindings.insert( + cmd, + vec![KeyBinding::new(KeyCode::Char( + char::from_digit(i, 10).unwrap(), + ))], + ); } // Message actions // Note: EditMessage (Up) обрабатывается напрямую в handle_open_chat_keyboard_input // в зависимости от контекста (пустой инпут). Не привязываем здесь, чтобы не // конфликтовать с Command::MoveUp в списке чатов. - bindings.insert(Command::DeleteMessage, vec![ - KeyBinding::new(KeyCode::Delete), - KeyBinding::new(KeyCode::Char('d')), - KeyBinding::new(KeyCode::Char('в')), // RU - ]); - bindings.insert(Command::ReplyMessage, vec![ - KeyBinding::new(KeyCode::Char('r')), - KeyBinding::new(KeyCode::Char('к')), // RU - ]); - bindings.insert(Command::ForwardMessage, vec![ - KeyBinding::new(KeyCode::Char('f')), - KeyBinding::new(KeyCode::Char('а')), // RU - ]); - bindings.insert(Command::CopyMessage, vec![ - KeyBinding::new(KeyCode::Char('y')), - KeyBinding::new(KeyCode::Char('н')), // RU - ]); - bindings.insert(Command::ReactMessage, vec![ - KeyBinding::new(KeyCode::Char('e')), - KeyBinding::new(KeyCode::Char('у')), // RU - ]); + bindings.insert( + Command::DeleteMessage, + vec![ + KeyBinding::new(KeyCode::Delete), + KeyBinding::new(KeyCode::Char('d')), + KeyBinding::new(KeyCode::Char('в')), // RU + ], + ); + bindings.insert( + Command::ReplyMessage, + vec![ + KeyBinding::new(KeyCode::Char('r')), + KeyBinding::new(KeyCode::Char('к')), // RU + ], + ); + bindings.insert( + Command::ForwardMessage, + vec![ + KeyBinding::new(KeyCode::Char('f')), + KeyBinding::new(KeyCode::Char('а')), // RU + ], + ); + bindings.insert( + Command::CopyMessage, + vec![ + KeyBinding::new(KeyCode::Char('y')), + KeyBinding::new(KeyCode::Char('н')), // RU + ], + ); + bindings.insert( + Command::ReactMessage, + vec![ + KeyBinding::new(KeyCode::Char('e')), + KeyBinding::new(KeyCode::Char('у')), // RU + ], + ); // Note: SelectMessage обрабатывается через Command::SubmitMessage в handle_enter_key() // Media - bindings.insert(Command::ViewImage, vec![ - KeyBinding::new(KeyCode::Char('v')), - KeyBinding::new(KeyCode::Char('м')), // RU - ]); + bindings.insert( + Command::ViewImage, + vec![ + KeyBinding::new(KeyCode::Char('v')), + KeyBinding::new(KeyCode::Char('м')), // RU + ], + ); // Voice playback - bindings.insert(Command::TogglePlayback, vec![ - KeyBinding::new(KeyCode::Char(' ')), - ]); - bindings.insert(Command::SeekForward, vec![ - KeyBinding::new(KeyCode::Right), - ]); - bindings.insert(Command::SeekBackward, vec![ - KeyBinding::new(KeyCode::Left), - ]); + bindings.insert(Command::TogglePlayback, vec![KeyBinding::new(KeyCode::Char(' '))]); + bindings.insert(Command::SeekForward, vec![KeyBinding::new(KeyCode::Right)]); + bindings.insert(Command::SeekBackward, vec![KeyBinding::new(KeyCode::Left)]); // Input - bindings.insert(Command::SubmitMessage, vec![ - KeyBinding::new(KeyCode::Enter), - ]); - bindings.insert(Command::Cancel, vec![ - KeyBinding::new(KeyCode::Esc), - ]); + bindings.insert(Command::SubmitMessage, vec![KeyBinding::new(KeyCode::Enter)]); + bindings.insert(Command::Cancel, vec![KeyBinding::new(KeyCode::Esc)]); bindings.insert(Command::NewLine, vec![]); - bindings.insert(Command::DeleteChar, vec![ - KeyBinding::new(KeyCode::Backspace), - ]); - bindings.insert(Command::DeleteWord, vec![ - KeyBinding::with_ctrl(KeyCode::Backspace), - KeyBinding::with_ctrl(KeyCode::Char('w')), - ]); - bindings.insert(Command::MoveToStart, vec![ - KeyBinding::new(KeyCode::Home), - KeyBinding::with_ctrl(KeyCode::Char('a')), - ]); - bindings.insert(Command::MoveToEnd, vec![ - KeyBinding::new(KeyCode::End), - KeyBinding::with_ctrl(KeyCode::Char('e')), - ]); + bindings.insert(Command::DeleteChar, vec![KeyBinding::new(KeyCode::Backspace)]); + bindings.insert( + Command::DeleteWord, + vec![ + KeyBinding::with_ctrl(KeyCode::Backspace), + KeyBinding::with_ctrl(KeyCode::Char('w')), + ], + ); + bindings.insert( + Command::MoveToStart, + vec![ + KeyBinding::new(KeyCode::Home), + KeyBinding::with_ctrl(KeyCode::Char('a')), + ], + ); + bindings.insert( + Command::MoveToEnd, + vec![ + KeyBinding::new(KeyCode::End), + KeyBinding::with_ctrl(KeyCode::Char('e')), + ], + ); // Vim mode - bindings.insert(Command::EnterInsertMode, vec![ - KeyBinding::new(KeyCode::Char('i')), - KeyBinding::new(KeyCode::Char('ш')), // RU - ]); + bindings.insert( + Command::EnterInsertMode, + vec![ + KeyBinding::new(KeyCode::Char('i')), + KeyBinding::new(KeyCode::Char('ш')), // RU + ], + ); // Profile - bindings.insert(Command::OpenProfile, vec![ - KeyBinding::with_ctrl(KeyCode::Char('u')), // Ctrl+U instead of Ctrl+I - KeyBinding::with_ctrl(KeyCode::Char('г')), // RU - ]); + bindings.insert( + Command::OpenProfile, + vec![ + KeyBinding::with_ctrl(KeyCode::Char('u')), // Ctrl+U instead of Ctrl+I + KeyBinding::with_ctrl(KeyCode::Char('г')), // RU + ], + ); Self { bindings } } @@ -395,9 +421,10 @@ mod key_code_serde { let s = String::deserialize(deserializer)?; if s.starts_with("Char('") && s.ends_with("')") { - let c = s.chars().nth(6).ok_or_else(|| { - serde::de::Error::custom("Invalid Char format") - })?; + let c = s + .chars() + .nth(6) + .ok_or_else(|| serde::de::Error::custom("Invalid Char format"))?; return Ok(KeyCode::Char(c)); } diff --git a/src/config/mod.rs b/src/config/mod.rs index fdd3844..7a0adca 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -284,10 +284,22 @@ mod tests { let keybindings = &config.keybindings; // Test that keybindings exist for common commands - assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)) == Some(Command::ReplyMessage)); - assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('к'), KeyModifiers::NONE)) == Some(Command::ReplyMessage)); - assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE)) == Some(Command::ForwardMessage)); - assert!(keybindings.get_command(&KeyEvent::new(KeyCode::Char('а'), KeyModifiers::NONE)) == Some(Command::ForwardMessage)); + assert!( + keybindings.get_command(&KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE)) + == Some(Command::ReplyMessage) + ); + assert!( + keybindings.get_command(&KeyEvent::new(KeyCode::Char('к'), KeyModifiers::NONE)) + == Some(Command::ReplyMessage) + ); + assert!( + keybindings.get_command(&KeyEvent::new(KeyCode::Char('f'), KeyModifiers::NONE)) + == Some(Command::ForwardMessage) + ); + assert!( + keybindings.get_command(&KeyEvent::new(KeyCode::Char('а'), KeyModifiers::NONE)) + == Some(Command::ForwardMessage) + ); } #[test] @@ -355,10 +367,24 @@ mod tests { #[test] fn test_config_validate_valid_all_standard_colors() { let colors = [ - "black", "red", "green", "yellow", "blue", "magenta", - "cyan", "gray", "grey", "white", "darkgray", "darkgrey", - "lightred", "lightgreen", "lightyellow", "lightblue", - "lightmagenta", "lightcyan" + "black", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "gray", + "grey", + "white", + "darkgray", + "darkgrey", + "lightred", + "lightgreen", + "lightyellow", + "lightblue", + "lightmagenta", + "lightcyan", ]; for color in colors { @@ -369,11 +395,7 @@ mod tests { config.colors.reaction_chosen = color.to_string(); config.colors.reaction_other = color.to_string(); - assert!( - config.validate().is_ok(), - "Color '{}' should be valid", - color - ); + assert!(config.validate().is_ok(), "Color '{}' should be valid", color); } } diff --git a/src/formatting.rs b/src/formatting.rs index 1fe6d4e..42f6b80 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -277,11 +277,7 @@ mod tests { #[test] fn test_format_text_with_bold() { let text = "Hello"; - let entities = vec![TextEntity { - offset: 0, - length: 5, - r#type: TextEntityType::Bold, - }]; + let entities = vec![TextEntity { offset: 0, length: 5, r#type: TextEntityType::Bold }]; let spans = format_text_with_entities(text, &entities, Color::White); assert_eq!(spans.len(), 1); diff --git a/src/input/auth.rs b/src/input/auth.rs index 4a43a2a..8670b86 100644 --- a/src/input/auth.rs +++ b/src/input/auth.rs @@ -20,7 +20,8 @@ pub async fn handle(app: &mut App, key_code: KeyCode) { app.status_message = Some("Отправка номера...".to_string()); match with_timeout_msg( Duration::from_secs(10), - app.td_client.send_phone_number(app.phone_input().to_string()), + app.td_client + .send_phone_number(app.phone_input().to_string()), "Таймаут отправки номера", ) .await @@ -84,7 +85,8 @@ pub async fn handle(app: &mut App, key_code: KeyCode) { app.status_message = Some("Проверка пароля...".to_string()); match with_timeout_msg( Duration::from_secs(10), - app.td_client.send_password(app.password_input().to_string()), + app.td_client + .send_password(app.password_input().to_string()), "Таймаут проверки пароля", ) .await diff --git a/src/input/handlers/chat.rs b/src/input/handlers/chat.rs index defeaee..a78c605 100644 --- a/src/input/handlers/chat.rs +++ b/src/input/handlers/chat.rs @@ -6,22 +6,22 @@ //! - Editing and sending messages //! - Loading older messages +use super::chat_list::open_chat_and_load_data; +use crate::app::methods::{ + compose::ComposeMethods, messages::MessageMethods, modal::ModalMethods, + navigation::NavigationMethods, +}; use crate::app::App; use crate::app::InputMode; -use crate::app::methods::{ - compose::ComposeMethods, messages::MessageMethods, - modal::ModalMethods, navigation::NavigationMethods, -}; -use crate::tdlib::{TdClientTrait, ChatAction}; +use crate::input::handlers::{copy_to_clipboard, format_message_for_clipboard}; +use crate::tdlib::{ChatAction, TdClientTrait}; use crate::types::{ChatId, MessageId}; use crate::utils::{is_non_empty, with_timeout, with_timeout_msg}; -use crate::input::handlers::{copy_to_clipboard, format_message_for_clipboard}; -use super::chat_list::open_chat_and_load_data; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::time::{Duration, Instant}; /// Обработка режима выбора сообщения для действий -/// +/// /// Обрабатывает: /// - Навигацию по сообщениям (Up/Down) /// - Удаление сообщения (d/в/Delete) @@ -29,7 +29,11 @@ use std::time::{Duration, Instant}; /// - Пересылку сообщения (f/а) /// - Копирование сообщения (y/н) /// - Добавление реакции (e/у) -pub async fn handle_message_selection(app: &mut App, _key: KeyEvent, command: Option) { +pub async fn handle_message_selection( + app: &mut App, + _key: KeyEvent, + command: Option, +) { match command { Some(crate::config::Command::MoveUp) => { app.select_previous_message(); @@ -44,9 +48,7 @@ pub async fn handle_message_selection(app: &mut App, _key: let can_delete = msg.can_be_deleted_only_for_self() || msg.can_be_deleted_for_all_users(); if can_delete { - app.chat_state = crate::app::ChatState::DeleteConfirmation { - message_id: msg.id(), - }; + app.chat_state = crate::app::ChatState::DeleteConfirmation { message_id: msg.id() }; } } Some(crate::config::Command::EnterInsertMode) => { @@ -129,17 +131,22 @@ pub async fn handle_message_selection(app: &mut App, _key: } /// Редактирование существующего сообщения -pub async fn edit_message(app: &mut App, chat_id: i64, msg_id: MessageId, text: String) { +pub async fn edit_message( + app: &mut App, + chat_id: i64, + msg_id: MessageId, + text: String, +) { // Проверяем, что сообщение есть в локальном кэше - let msg_exists = app.td_client.current_chat_messages() + let msg_exists = app + .td_client + .current_chat_messages() .iter() .any(|m| m.id() == msg_id); if !msg_exists { - app.error_message = Some(format!( - "Сообщение {} не найдено в кэше чата {}", - msg_id.as_i64(), chat_id - )); + app.error_message = + Some(format!("Сообщение {} не найдено в кэше чата {}", msg_id.as_i64(), chat_id)); app.chat_state = crate::app::ChatState::Normal; app.message_input.clear(); app.cursor_position = 0; @@ -148,7 +155,8 @@ pub async fn edit_message(app: &mut App, chat_id: i64, msg_ match with_timeout_msg( Duration::from_secs(5), - app.td_client.edit_message(ChatId::new(chat_id), msg_id, text), + app.td_client + .edit_message(ChatId::new(chat_id), msg_id, text), "Таймаут редактирования", ) .await @@ -160,8 +168,12 @@ pub async fn edit_message(app: &mut App, chat_id: i64, msg_ let old_reply_to = messages[pos].interactions.reply_to.clone(); // Если в старом сообщении был reply и в новом он "Unknown" - сохраняем старый if let Some(old_reply) = old_reply_to { - if edited_msg.interactions.reply_to.as_ref() - .map_or(true, |r| r.sender_name == "Unknown") { + if edited_msg + .interactions + .reply_to + .as_ref() + .map_or(true, |r| r.sender_name == "Unknown") + { edited_msg.interactions.reply_to = Some(old_reply); } } @@ -189,13 +201,13 @@ pub async fn send_new_message(app: &mut App, chat_id: i64, }; // Создаём ReplyInfo ДО отправки, пока сообщение точно доступно - let reply_info = app.get_replying_to_message().map(|m| { - crate::tdlib::ReplyInfo { + let reply_info = app + .get_replying_to_message() + .map(|m| crate::tdlib::ReplyInfo { message_id: m.id(), sender_name: m.sender_name().to_string(), text: m.text().to_string(), - } - }); + }); app.message_input.clear(); app.cursor_position = 0; @@ -206,11 +218,14 @@ pub async fn send_new_message(app: &mut App, chat_id: i64, app.last_typing_sent = None; // Отменяем typing status - app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel).await; + app.td_client + .send_chat_action(ChatId::new(chat_id), ChatAction::Cancel) + .await; match with_timeout_msg( Duration::from_secs(5), - app.td_client.send_message(ChatId::new(chat_id), text, reply_to_id, reply_info), + app.td_client + .send_message(ChatId::new(chat_id), text, reply_to_id, reply_info), "Таймаут отправки", ) .await @@ -228,7 +243,7 @@ pub async fn send_new_message(app: &mut App, chat_id: i64, } /// Обработка клавиши Enter -/// +/// /// Обрабатывает три сценария: /// 1. В режиме выбора сообщения: начать редактирование /// 2. В открытом чате: отправить новое или редактировать существующее сообщение @@ -304,7 +319,8 @@ pub async fn send_reaction(app: &mut App) { // Send reaction with timeout let result = with_timeout_msg( Duration::from_secs(5), - app.td_client.toggle_reaction(chat_id, message_id, emoji.clone()), + app.td_client + .toggle_reaction(chat_id, message_id, emoji.clone()), "Таймаут отправки реакции", ) .await; @@ -353,7 +369,8 @@ pub async fn load_older_messages_if_needed(app: &mut App) { // Load older messages with timeout let Ok(older) = with_timeout( Duration::from_secs(3), - app.td_client.load_older_messages(ChatId::new(chat_id), oldest_msg_id), + app.td_client + .load_older_messages(ChatId::new(chat_id), oldest_msg_id), ) .await else { @@ -368,7 +385,7 @@ pub async fn load_older_messages_if_needed(app: &mut App) { } /// Обработка ввода клавиатуры в открытом чате -/// +/// /// Обрабатывает: /// - Backspace/Delete: удаление символов относительно курсора /// - Char: вставка символов в позицию курсора + typing status @@ -408,7 +425,8 @@ pub async fn handle_open_chat_keyboard_input(app: &mut App, // Игнорируем символы с Ctrl/Alt модификаторами (кроме Shift) // Это позволяет обрабатывать хоткеи типа Ctrl+U для профиля if key.modifiers.contains(KeyModifiers::CONTROL) - || key.modifiers.contains(KeyModifiers::ALT) { + || key.modifiers.contains(KeyModifiers::ALT) + { return; } @@ -434,7 +452,9 @@ pub async fn handle_open_chat_keyboard_input(app: &mut App, .unwrap_or(true); if should_send_typing { if let Some(chat_id) = app.get_selected_chat_id() { - app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Typing).await; + app.td_client + .send_chat_action(ChatId::new(chat_id), ChatAction::Typing) + .await; app.last_typing_sent = Some(Instant::now()); } } @@ -621,8 +641,7 @@ async fn handle_view_image(app: &mut App) { for msg in app.td_client.current_chat_messages_mut() { if let Some(photo) = msg.photo_info_mut() { if photo.file_id == file_id { - photo.download_state = - PhotoDownloadState::Downloaded(path.clone()); + photo.download_state = PhotoDownloadState::Downloaded(path.clone()); break; } } @@ -640,8 +659,7 @@ async fn handle_view_image(app: &mut App) { for msg in app.td_client.current_chat_messages_mut() { if let Some(photo) = msg.photo_info_mut() { if photo.file_id == file_id { - photo.download_state = - PhotoDownloadState::Error(e.clone()); + photo.download_state = PhotoDownloadState::Error(e.clone()); break; } } @@ -660,8 +678,7 @@ async fn handle_view_image(app: &mut App) { for msg in app.td_client.current_chat_messages_mut() { if let Some(photo) = msg.photo_info_mut() { if photo.file_id == file_id { - photo.download_state = - PhotoDownloadState::Downloaded(path.clone()); + photo.download_state = PhotoDownloadState::Downloaded(path.clone()); break; } } @@ -748,13 +765,25 @@ async fn handle_play_voice(app: &mut App) { if let Ok(entries) = std::fs::read_dir(parent) { for entry in entries.flatten() { let entry_name = entry.file_name(); - if entry_name.to_string_lossy().starts_with(&stem.to_string_lossy().to_string()) { + if entry_name + .to_string_lossy() + .starts_with(&stem.to_string_lossy().to_string()) + { let found_path = entry.path().to_string_lossy().to_string(); // Кэшируем найденный файл if let Some(ref mut cache) = app.voice_cache { - let _ = cache.store(&file_id.to_string(), Path::new(&found_path)); + let _ = cache.store( + &file_id.to_string(), + Path::new(&found_path), + ); } - return handle_play_voice_from_path(app, &found_path, &voice, &msg).await; + return handle_play_voice_from_path( + app, + &found_path, + &voice, + &msg, + ) + .await; } } } @@ -826,4 +855,3 @@ async fn _download_and_expand(app: &mut App, msg_id: crate: // Закомментировано - будет реализовано в Этапе 4 } */ - diff --git a/src/input/handlers/chat_list.rs b/src/input/handlers/chat_list.rs index 81dbab2..6a747c3 100644 --- a/src/input/handlers/chat_list.rs +++ b/src/input/handlers/chat_list.rs @@ -5,9 +5,11 @@ //! - Folder selection //! - Opening chats +use crate::app::methods::{ + compose::ComposeMethods, messages::MessageMethods, navigation::NavigationMethods, +}; use crate::app::App; use crate::app::InputMode; -use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods, navigation::NavigationMethods}; use crate::tdlib::TdClientTrait; use crate::types::{ChatId, MessageId}; use crate::utils::{with_timeout, with_timeout_msg}; @@ -15,11 +17,15 @@ use crossterm::event::KeyEvent; use std::time::Duration; /// Обработка навигации в списке чатов -/// +/// /// Обрабатывает: /// - Up/Down/j/k: навигация между чатами /// - Цифры 1-9: переключение папок (1=All, 2-9=папки из TDLib) -pub async fn handle_chat_list_navigation(app: &mut App, _key: KeyEvent, command: Option) { +pub async fn handle_chat_list_navigation( + app: &mut App, + _key: KeyEvent, + command: Option, +) { match command { Some(crate::config::Command::MoveDown) => { app.next_chat(); @@ -65,11 +71,9 @@ pub async fn select_folder(app: &mut App, folder_idx: usize let folder_id = folder.id; app.selected_folder_id = Some(folder_id); app.status_message = Some("Загрузка чатов папки...".to_string()); - let _ = with_timeout( - Duration::from_secs(5), - app.td_client.load_folder_chats(folder_id, 50), - ) - .await; + let _ = + with_timeout(Duration::from_secs(5), app.td_client.load_folder_chats(folder_id, 50)) + .await; app.status_message = None; app.chat_list_state.select(Some(0)); } @@ -114,7 +118,8 @@ pub async fn open_chat_and_load_data(app: &mut App, chat_id // ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории // Это предотвращает race condition с Update::NewMessage - app.td_client.set_current_chat_id(Some(ChatId::new(chat_id))); + app.td_client + .set_current_chat_id(Some(ChatId::new(chat_id))); // Загружаем черновик (локальная операция, мгновенно) app.load_draft(); @@ -132,4 +137,4 @@ pub async fn open_chat_and_load_data(app: &mut App, chat_id app.status_message = None; } } -} \ No newline at end of file +} diff --git a/src/input/handlers/compose.rs b/src/input/handlers/compose.rs index 1195177..86af305 100644 --- a/src/input/handlers/compose.rs +++ b/src/input/handlers/compose.rs @@ -6,10 +6,10 @@ //! - Edit mode //! - Cursor movement and text editing -use crate::app::App; use crate::app::methods::{ compose::ComposeMethods, navigation::NavigationMethods, search::SearchMethods, }; +use crate::app::App; use crate::tdlib::TdClientTrait; use crate::types::ChatId; use crate::utils::with_timeout_msg; @@ -17,12 +17,16 @@ use crossterm::event::KeyEvent; use std::time::Duration; /// Обработка режима выбора чата для пересылки сообщения -/// +/// /// Обрабатывает: /// - Навигацию по списку чатов (Up/Down) /// - Пересылку сообщения в выбранный чат (Enter) /// - Отмену пересылки (Esc) -pub async fn handle_forward_mode(app: &mut App, _key: KeyEvent, command: Option) { +pub async fn handle_forward_mode( + app: &mut App, + _key: KeyEvent, + command: Option, +) { match command { Some(crate::config::Command::Cancel) => { app.cancel_forward(); @@ -63,11 +67,8 @@ pub async fn forward_selected_message(app: &mut App) { // Forward the message with timeout let result = with_timeout_msg( Duration::from_secs(5), - app.td_client.forward_messages( - to_chat_id, - ChatId::new(from_chat_id), - vec![msg_id], - ), + app.td_client + .forward_messages(to_chat_id, ChatId::new(from_chat_id), vec![msg_id]), "Таймаут пересылки", ) .await; @@ -81,4 +82,4 @@ pub async fn forward_selected_message(app: &mut App) { app.error_message = Some(e); } } -} \ No newline at end of file +} diff --git a/src/input/handlers/global.rs b/src/input/handlers/global.rs index 9799778..23d935d 100644 --- a/src/input/handlers/global.rs +++ b/src/input/handlers/global.rs @@ -6,8 +6,8 @@ //! - Ctrl+P: View pinned messages //! - Ctrl+F: Search messages in chat -use crate::app::App; use crate::app::methods::{modal::ModalMethods, search::SearchMethods}; +use crate::app::App; use crate::tdlib::TdClientTrait; use crate::types::ChatId; use crate::utils::{with_timeout, with_timeout_msg}; @@ -47,7 +47,8 @@ pub async fn handle_global_commands(app: &mut App, key: Key KeyCode::Char('r') if has_ctrl => { // Ctrl+R - обновить список чатов app.status_message = Some("Обновление чатов...".to_string()); - let _ = with_timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await; + let _ = + with_timeout(Duration::from_secs(5), app.td_client.load_chats(50)).await; // Синхронизируем muted чаты после обновления app.td_client.sync_notification_muted_chats(); app.status_message = None; diff --git a/src/input/handlers/mod.rs b/src/input/handlers/mod.rs index 2f949e0..998a4ac 100644 --- a/src/input/handlers/mod.rs +++ b/src/input/handlers/mod.rs @@ -10,13 +10,13 @@ //! - modal: Modal dialogs (delete confirmation, emoji picker, etc.) //! - search: Search functionality (chat search, message search) -pub mod clipboard; -pub mod global; -pub mod profile; pub mod chat; pub mod chat_list; +pub mod clipboard; pub mod compose; +pub mod global; pub mod modal; +pub mod profile; pub mod search; pub use clipboard::*; diff --git a/src/input/handlers/modal.rs b/src/input/handlers/modal.rs index 3bf61b6..6820250 100644 --- a/src/input/handlers/modal.rs +++ b/src/input/handlers/modal.rs @@ -7,13 +7,13 @@ //! - Pinned messages view //! - Profile information modal -use crate::app::{AccountSwitcherState, App}; +use super::scroll_to_message; use crate::app::methods::{modal::ModalMethods, navigation::NavigationMethods}; +use crate::app::{AccountSwitcherState, App}; +use crate::input::handlers::get_available_actions_count; use crate::tdlib::TdClientTrait; use crate::types::{ChatId, MessageId}; -use crate::utils::{with_timeout_msg, modal_handler::handle_yes_no}; -use crate::input::handlers::get_available_actions_count; -use super::scroll_to_message; +use crate::utils::{modal_handler::handle_yes_no, with_timeout_msg}; use crossterm::event::{KeyCode, KeyEvent}; use std::time::Duration; @@ -65,58 +65,60 @@ pub async fn handle_account_switcher( } } } - AccountSwitcherState::AddAccount { .. } => { - match key.code { - KeyCode::Esc => { - app.account_switcher_back(); - } - KeyCode::Enter => { - app.account_switcher_confirm_add(); - } - KeyCode::Backspace => { - if let Some(AccountSwitcherState::AddAccount { - name_input, - cursor_position, - error, - }) = &mut app.account_switcher - { - if *cursor_position > 0 { - let mut chars: Vec = name_input.chars().collect(); - chars.remove(*cursor_position - 1); - *name_input = chars.into_iter().collect(); - *cursor_position -= 1; - *error = None; - } - } - } - KeyCode::Char(c) => { - if let Some(AccountSwitcherState::AddAccount { - name_input, - cursor_position, - error, - }) = &mut app.account_switcher - { + AccountSwitcherState::AddAccount { .. } => match key.code { + KeyCode::Esc => { + app.account_switcher_back(); + } + KeyCode::Enter => { + app.account_switcher_confirm_add(); + } + KeyCode::Backspace => { + if let Some(AccountSwitcherState::AddAccount { + name_input, + cursor_position, + error, + }) = &mut app.account_switcher + { + if *cursor_position > 0 { let mut chars: Vec = name_input.chars().collect(); - chars.insert(*cursor_position, c); + chars.remove(*cursor_position - 1); *name_input = chars.into_iter().collect(); - *cursor_position += 1; + *cursor_position -= 1; *error = None; } } - _ => {} } - } + KeyCode::Char(c) => { + if let Some(AccountSwitcherState::AddAccount { + name_input, + cursor_position, + error, + }) = &mut app.account_switcher + { + let mut chars: Vec = name_input.chars().collect(); + chars.insert(*cursor_position, c); + *name_input = chars.into_iter().collect(); + *cursor_position += 1; + *error = None; + } + } + _ => {} + }, } } /// Обработка режима профиля пользователя/чата -/// +/// /// Обрабатывает: /// - Модалку подтверждения выхода из группы (двухшаговая) /// - Навигацию по действиям профиля (Up/Down) /// - Выполнение выбранного действия (Enter): открыть в браузере, скопировать ID, покинуть группу /// - Выход из режима профиля (Esc) -pub async fn handle_profile_mode(app: &mut App, key: KeyEvent, command: Option) { +pub async fn handle_profile_mode( + app: &mut App, + key: KeyEvent, + command: Option, +) { // Обработка подтверждения выхода из группы let confirmation_step = app.get_leave_group_confirmation_step(); if confirmation_step > 0 { @@ -189,10 +191,7 @@ pub async fn handle_profile_mode(app: &mut App, key: KeyEve // Действие: Открыть в браузере if let Some(username) = &profile.username { if action_index == current_idx { - let url = format!( - "https://t.me/{}", - username.trim_start_matches('@') - ); + let url = format!("https://t.me/{}", username.trim_start_matches('@')); #[cfg(feature = "url-open")] { match open::that(&url) { @@ -208,7 +207,7 @@ pub async fn handle_profile_mode(app: &mut App, key: KeyEve #[cfg(not(feature = "url-open"))] { app.error_message = Some( - "Открытие URL недоступно (требуется feature 'url-open')".to_string() + "Открытие URL недоступно (требуется feature 'url-open')".to_string(), ); } return; @@ -233,7 +232,7 @@ pub async fn handle_profile_mode(app: &mut App, key: KeyEve } /// Обработка Ctrl+U для открытия профиля чата/пользователя -/// +/// /// Загружает информацию о профиле и переключает в режим просмотра профиля pub async fn handle_profile_open(app: &mut App) { let Some(chat_id) = app.selected_chat_id else { @@ -319,12 +318,16 @@ pub async fn handle_delete_confirmation(app: &mut App, key: } /// Обработка режима выбора реакции (emoji picker) -/// +/// /// Обрабатывает: /// - Навигацию по сетке реакций: Left/Right, Up/Down (сетка 8x6) /// - Добавление/удаление реакции (Enter) /// - Выход из режима (Esc) -pub async fn handle_reaction_picker_mode(app: &mut App, _key: KeyEvent, command: Option) { +pub async fn handle_reaction_picker_mode( + app: &mut App, + _key: KeyEvent, + command: Option, +) { match command { Some(crate::config::Command::MoveLeft) => { app.select_previous_reaction(); @@ -335,10 +338,8 @@ pub async fn handle_reaction_picker_mode(app: &mut App, _ke app.needs_redraw = true; } Some(crate::config::Command::MoveUp) => { - if let crate::app::ChatState::ReactionPicker { - selected_index, - .. - } = &mut app.chat_state + if let crate::app::ChatState::ReactionPicker { selected_index, .. } = + &mut app.chat_state { if *selected_index >= 8 { *selected_index = selected_index.saturating_sub(8); @@ -372,12 +373,16 @@ pub async fn handle_reaction_picker_mode(app: &mut App, _ke } /// Обработка режима просмотра закреплённых сообщений -/// +/// /// Обрабатывает: /// - Навигацию по закреплённым сообщениям (Up/Down) /// - Переход к сообщению в истории (Enter) /// - Выход из режима (Esc) -pub async fn handle_pinned_mode(app: &mut App, _key: KeyEvent, command: Option) { +pub async fn handle_pinned_mode( + app: &mut App, + _key: KeyEvent, + command: Option, +) { match command { Some(crate::config::Command::Cancel) => { app.exit_pinned_mode(); @@ -396,4 +401,4 @@ pub async fn handle_pinned_mode(app: &mut App, _key: KeyEve } _ => {} } -} \ No newline at end of file +} diff --git a/src/input/handlers/search.rs b/src/input/handlers/search.rs index 9cb28bc..1bb151c 100644 --- a/src/input/handlers/search.rs +++ b/src/input/handlers/search.rs @@ -5,8 +5,8 @@ //! - Message search mode //! - Search query input -use crate::app::App; use crate::app::methods::{navigation::NavigationMethods, search::SearchMethods}; +use crate::app::App; use crate::tdlib::TdClientTrait; use crate::types::{ChatId, MessageId}; use crate::utils::with_timeout; @@ -17,13 +17,17 @@ use super::chat_list::open_chat_and_load_data; use super::scroll_to_message; /// Обработка режима поиска по чатам -/// +/// /// Обрабатывает: /// - Редактирование поискового запроса (Backspace, Char) /// - Навигацию по отфильтрованному списку (Up/Down) /// - Открытие выбранного чата (Enter) /// - Отмену поиска (Esc) -pub async fn handle_chat_search_mode(app: &mut App, key: KeyEvent, command: Option) { +pub async fn handle_chat_search_mode( + app: &mut App, + key: KeyEvent, + command: Option, +) { match command { Some(crate::config::Command::Cancel) => { app.cancel_search(); @@ -40,30 +44,32 @@ pub async fn handle_chat_search_mode(app: &mut App, key: Ke Some(crate::config::Command::MoveUp) => { app.previous_filtered_chat(); } - _ => { - match key.code { - KeyCode::Backspace => { - app.search_query.pop(); - app.chat_list_state.select(Some(0)); - } - KeyCode::Char(c) => { - app.search_query.push(c); - app.chat_list_state.select(Some(0)); - } - _ => {} + _ => match key.code { + KeyCode::Backspace => { + app.search_query.pop(); + app.chat_list_state.select(Some(0)); } - } + KeyCode::Char(c) => { + app.search_query.push(c); + app.chat_list_state.select(Some(0)); + } + _ => {} + }, } } /// Обработка режима поиска по сообщениям в открытом чате -/// +/// /// Обрабатывает: /// - Навигацию по результатам поиска (Up/Down/N/n) /// - Переход к выбранному сообщению (Enter) /// - Редактирование поискового запроса (Backspace, Char) /// - Выход из режима поиска (Esc) -pub async fn handle_message_search_mode(app: &mut App, key: KeyEvent, command: Option) { +pub async fn handle_message_search_mode( + app: &mut App, + key: KeyEvent, + command: Option, +) { match command { Some(crate::config::Command::Cancel) => { app.exit_message_search_mode(); @@ -80,33 +86,31 @@ pub async fn handle_message_search_mode(app: &mut App, key: app.exit_message_search_mode(); } } - _ => { - match key.code { - KeyCode::Char('N') => { - app.select_previous_search_result(); - } - KeyCode::Char('n') => { - app.select_next_search_result(); - } - KeyCode::Backspace => { - let Some(mut query) = app.get_search_query().map(|s| s.to_string()) else { - return; - }; - query.pop(); - app.update_search_query(query.clone()); - perform_message_search(app, &query).await; - } - KeyCode::Char(c) => { - let Some(mut query) = app.get_search_query().map(|s| s.to_string()) else { - return; - }; - query.push(c); - app.update_search_query(query.clone()); - perform_message_search(app, &query).await; - } - _ => {} + _ => match key.code { + KeyCode::Char('N') => { + app.select_previous_search_result(); } - } + KeyCode::Char('n') => { + app.select_next_search_result(); + } + KeyCode::Backspace => { + let Some(mut query) = app.get_search_query().map(|s| s.to_string()) else { + return; + }; + query.pop(); + app.update_search_query(query.clone()); + perform_message_search(app, &query).await; + } + KeyCode::Char(c) => { + let Some(mut query) = app.get_search_query().map(|s| s.to_string()) else { + return; + }; + query.push(c); + app.update_search_query(query.clone()); + perform_message_search(app, &query).await; + } + _ => {} + }, } } @@ -129,4 +133,4 @@ pub async fn perform_message_search(app: &mut App, query: & { app.set_search_results(results); } -} \ No newline at end of file +} diff --git a/src/input/main_input.rs b/src/input/main_input.rs index 696ac91..62e3d3e 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -3,35 +3,26 @@ //! Dispatches keyboard events to specialized handlers based on current app mode. //! Priority order: modals → search → compose → chat → chat list. +use crate::app::methods::{ + compose::ComposeMethods, messages::MessageMethods, modal::ModalMethods, + navigation::NavigationMethods, search::SearchMethods, +}; use crate::app::App; use crate::app::InputMode; -use crate::app::methods::{ - compose::ComposeMethods, - messages::MessageMethods, - modal::ModalMethods, - navigation::NavigationMethods, - search::SearchMethods, -}; -use crate::tdlib::TdClientTrait; use crate::input::handlers::{ + chat::{handle_enter_key, handle_message_selection, handle_open_chat_keyboard_input}, + chat_list::handle_chat_list_navigation, + compose::handle_forward_mode, handle_global_commands, modal::{ - handle_account_switcher, - handle_profile_mode, handle_profile_open, handle_delete_confirmation, - handle_reaction_picker_mode, handle_pinned_mode, + handle_account_switcher, handle_delete_confirmation, handle_pinned_mode, + handle_profile_mode, handle_profile_open, handle_reaction_picker_mode, }, search::{handle_chat_search_mode, handle_message_search_mode}, - compose::handle_forward_mode, - chat_list::handle_chat_list_navigation, - chat::{ - handle_message_selection, handle_enter_key, - handle_open_chat_keyboard_input, - }, }; +use crate::tdlib::TdClientTrait; use crossterm::event::KeyEvent; - - /// Обработка клавиши Esc в Normal mode /// /// Закрывает чат с сохранением черновика @@ -55,7 +46,10 @@ async fn handle_escape_normal(app: &mut App) { let _ = app.td_client.set_draft_message(chat_id, draft_text).await; } else { // Очищаем черновик если инпут пустой - let _ = app.td_client.set_draft_message(chat_id, String::new()).await; + let _ = app + .td_client + .set_draft_message(chat_id, String::new()) + .await; } app.close_chat(); @@ -252,7 +246,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { } /// Обработка модального окна просмотра изображения -/// +/// /// Hotkeys: /// - Esc/q: закрыть модальное окно /// - ←: предыдущее фото в чате @@ -331,4 +325,3 @@ async fn navigate_to_adjacent_photo(app: &mut App, directio }; app.status_message = Some(msg.to_string()); } - diff --git a/src/main.rs b/src/main.rs index b0286e2..66ef8a6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -57,7 +57,7 @@ async fn main() -> Result<(), io::Error> { tracing_subscriber::fmt() .with_env_filter( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")) + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("warn")), ) .init(); @@ -70,15 +70,16 @@ async fn main() -> Result<(), io::Error> { // Резолвим аккаунт из CLI или default let account_arg = parse_account_arg(); let (account_name, db_path) = - accounts::resolve_account(&accounts_config, account_arg.as_deref()) - .unwrap_or_else(|e| { - eprintln!("Error: {}", e); - std::process::exit(1); - }); + accounts::resolve_account(&accounts_config, account_arg.as_deref()).unwrap_or_else(|e| { + eprintln!("Error: {}", e); + std::process::exit(1); + }); // Создаём директорию аккаунта если её нет let db_path = accounts::ensure_account_dir( - account_arg.as_deref().unwrap_or(&accounts_config.default_account), + account_arg + .as_deref() + .unwrap_or(&accounts_config.default_account), ) .unwrap_or(db_path); @@ -112,14 +113,14 @@ async fn main() -> Result<(), io::Error> { tokio::spawn(async move { let _ = tdlib_rs::functions::set_tdlib_parameters( - false, // use_test_dc - db_path_str, // database_directory - "".to_string(), // files_directory - "".to_string(), // database_encryption_key - true, // use_file_database - true, // use_chat_info_database - true, // use_message_database - false, // use_secret_chats + false, // use_test_dc + db_path_str, // database_directory + "".to_string(), // files_directory + "".to_string(), // database_encryption_key + true, // use_file_database + true, // use_chat_info_database + true, // use_message_database + false, // use_secret_chats api_id, api_hash, "en".to_string(), // system_language_code @@ -292,7 +293,11 @@ async fn run_app( let _ = tdlib_rs::functions::close(app.td_client.client_id()).await; // Ждём завершения polling задачи (с таймаутом) - with_timeout_ignore(Duration::from_secs(SHUTDOWN_TIMEOUT_SECS), polling_handle).await; + with_timeout_ignore( + Duration::from_secs(SHUTDOWN_TIMEOUT_SECS), + polling_handle, + ) + .await; return Ok(()); } @@ -330,11 +335,8 @@ async fn run_app( // Process pending chat initialization (reply info, pinned, photos) if let Some(chat_id) = app.pending_chat_init.take() { // Загружаем недостающие reply info (игнорируем ошибки) - with_timeout_ignore( - Duration::from_secs(5), - app.td_client.fetch_missing_reply_info(), - ) - .await; + with_timeout_ignore(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()) + .await; // Загружаем последнее закреплённое сообщение (игнорируем ошибки) with_timeout_ignore( @@ -372,25 +374,22 @@ async fn run_app( for file_id in photo_file_ids { let tx = tx.clone(); tokio::spawn(async move { - let result = tokio::time::timeout( - Duration::from_secs(5), - async { - match tdlib_rs::functions::download_file( - file_id, 1, 0, 0, true, client_id, - ) - .await + let result = tokio::time::timeout(Duration::from_secs(5), async { + match tdlib_rs::functions::download_file( + file_id, 1, 0, 0, true, client_id, + ) + .await + { + Ok(tdlib_rs::enums::File::File(file)) + if file.local.is_downloading_completed + && !file.local.path.is_empty() => { - Ok(tdlib_rs::enums::File::File(file)) - if file.local.is_downloading_completed - && !file.local.path.is_empty() => - { - Ok(file.local.path) - } - Ok(_) => Err("Файл не скачан".to_string()), - Err(e) => Err(format!("{:?}", e)), + Ok(file.local.path) } - }, - ) + Ok(_) => Err("Файл не скачан".to_string()), + Err(e) => Err(format!("{:?}", e)), + } + }) .await; let result = match result { diff --git a/src/media/cache.rs b/src/media/cache.rs index 468902d..d7cd6ce 100644 --- a/src/media/cache.rs +++ b/src/media/cache.rs @@ -33,10 +33,7 @@ impl ImageCache { let path = self.cache_dir.join(format!("{}.jpg", file_id)); if path.exists() { // Обновляем mtime для LRU - let _ = filetime::set_file_mtime( - &path, - filetime::FileTime::now(), - ); + let _ = filetime::set_file_mtime(&path, filetime::FileTime::now()); Some(path) } else { None @@ -47,8 +44,7 @@ impl ImageCache { pub fn cache_file(&self, file_id: i32, source_path: &str) -> Result { let dest = self.cache_dir.join(format!("{}.jpg", file_id)); - fs::copy(source_path, &dest) - .map_err(|e| format!("Ошибка кэширования: {}", e))?; + fs::copy(source_path, &dest).map_err(|e| format!("Ошибка кэширования: {}", e))?; // Evict если превышен лимит self.evict_if_needed(); diff --git a/src/media/image_renderer.rs b/src/media/image_renderer.rs index e8a043e..8a35490 100644 --- a/src/media/image_renderer.rs +++ b/src/media/image_renderer.rs @@ -28,7 +28,7 @@ impl ImageRenderer { /// Создаёт ImageRenderer с автодетектом протокола (высокое качество для modal) pub fn new() -> Option { let picker = Picker::from_query_stdio().ok()?; - + Some(Self { picker, protocols: HashMap::new(), @@ -41,7 +41,7 @@ impl ImageRenderer { pub fn new_fast() -> Option { let mut picker = Picker::from_fontsize((8, 12)); picker.set_protocol_type(ProtocolType::Halfblocks); - + Some(Self { picker, protocols: HashMap::new(), @@ -51,7 +51,7 @@ impl ImageRenderer { } /// Загружает изображение из файла и создаёт протокол рендеринга. - /// + /// /// Если протокол уже существует, не загружает повторно (кэширование). /// Использует LRU eviction при превышении лимита. pub fn load_image(&mut self, msg_id: MessageId, path: &str) -> Result<(), String> { @@ -76,7 +76,7 @@ impl ImageRenderer { let protocol = self.picker.new_resize_protocol(img); self.protocols.insert(msg_id_i64, protocol); - + // Обновляем access order self.access_counter += 1; self.access_order.insert(msg_id_i64, self.access_counter); @@ -93,17 +93,17 @@ impl ImageRenderer { } /// Получает мутабельную ссылку на протокол для рендеринга. - /// + /// /// Обновляет access time для LRU. pub fn get_protocol(&mut self, msg_id: &MessageId) -> Option<&mut StatefulProtocol> { let msg_id_i64 = msg_id.as_i64(); - + if self.protocols.contains_key(&msg_id_i64) { // Обновляем access time self.access_counter += 1; self.access_order.insert(msg_id_i64, self.access_counter); } - + self.protocols.get_mut(&msg_id_i64) } diff --git a/src/message_grouping.rs b/src/message_grouping.rs index 020c12b..f674af1 100644 --- a/src/message_grouping.rs +++ b/src/message_grouping.rs @@ -12,7 +12,10 @@ pub enum MessageGroup { /// Разделитель даты (день в формате timestamp) DateSeparator(i32), /// Заголовок отправителя (is_outgoing, sender_name) - SenderHeader { is_outgoing: bool, sender_name: String }, + SenderHeader { + is_outgoing: bool, + sender_name: String, + }, /// Сообщение Message(MessageInfo), /// Альбом (группа фото с одинаковым media_album_id) @@ -106,10 +109,7 @@ pub fn group_messages(messages: &[MessageInfo]) -> Vec { if show_sender_header { // Flush аккумулятор перед сменой отправителя flush_album(&mut album_acc, &mut result); - result.push(MessageGroup::SenderHeader { - is_outgoing: msg.is_outgoing(), - sender_name, - }); + result.push(MessageGroup::SenderHeader { is_outgoing: msg.is_outgoing(), sender_name }); last_sender = Some(current_sender); } diff --git a/src/notifications.rs b/src/notifications.rs index 7863dcd..3364c66 100644 --- a/src/notifications.rs +++ b/src/notifications.rs @@ -39,11 +39,7 @@ impl NotificationManager { } /// Creates a notification manager with custom settings - pub fn with_config( - enabled: bool, - only_mentions: bool, - show_preview: bool, - ) -> Self { + pub fn with_config(enabled: bool, only_mentions: bool, show_preview: bool) -> Self { Self { enabled, muted_chats: HashSet::new(), @@ -311,22 +307,13 @@ mod tests { #[test] fn test_beautify_media_labels() { // Test photo - assert_eq!( - NotificationManager::beautify_media_labels("[Фото]"), - "📷 Фото" - ); + assert_eq!(NotificationManager::beautify_media_labels("[Фото]"), "📷 Фото"); // Test video - assert_eq!( - NotificationManager::beautify_media_labels("[Видео]"), - "🎥 Видео" - ); + assert_eq!(NotificationManager::beautify_media_labels("[Видео]"), "🎥 Видео"); // Test sticker with emoji - assert_eq!( - NotificationManager::beautify_media_labels("[Стикер: 😊]"), - "🎨 Стикер: 😊]" - ); + assert_eq!(NotificationManager::beautify_media_labels("[Стикер: 😊]"), "🎨 Стикер: 😊]"); // Test audio with title assert_eq!( @@ -341,10 +328,7 @@ mod tests { ); // Test regular text (no changes) - assert_eq!( - NotificationManager::beautify_media_labels("Hello, world!"), - "Hello, world!" - ); + assert_eq!(NotificationManager::beautify_media_labels("Hello, world!"), "Hello, world!"); // Test mixed content assert_eq!( diff --git a/src/tdlib/auth.rs b/src/tdlib/auth.rs index eeef949..3ecabb0 100644 --- a/src/tdlib/auth.rs +++ b/src/tdlib/auth.rs @@ -83,10 +83,7 @@ impl AuthManager { /// /// Новый экземпляр `AuthManager` в состоянии `WaitTdlibParameters`. pub fn new(client_id: i32) -> Self { - Self { - state: AuthState::WaitTdlibParameters, - client_id, - } + Self { state: AuthState::WaitTdlibParameters, client_id } } /// Проверяет, завершена ли авторизация. diff --git a/src/tdlib/chat_helpers.rs b/src/tdlib/chat_helpers.rs index 2895316..b022ee7 100644 --- a/src/tdlib/chat_helpers.rs +++ b/src/tdlib/chat_helpers.rs @@ -3,7 +3,7 @@ //! This module contains utility functions for managing chats, //! including finding, updating, and adding/removing chats. -use crate::constants::{MAX_CHAT_USER_IDS, MAX_CHATS}; +use crate::constants::{MAX_CHATS, MAX_CHAT_USER_IDS}; use crate::types::{ChatId, MessageId, UserId}; use tdlib_rs::enums::{Chat as TdChat, ChatList, ChatType}; @@ -33,7 +33,9 @@ pub fn add_or_update_chat(client: &mut TdClient, td_chat_enum: &TdChat) { // Пропускаем удалённые аккаунты if td_chat.title == "Deleted Account" || td_chat.title.is_empty() { // Удаляем из списка если уже был добавлен - client.chats_mut().retain(|c| c.id != ChatId::new(td_chat.id)); + client + .chats_mut() + .retain(|c| c.id != ChatId::new(td_chat.id)); return; } @@ -70,7 +72,9 @@ pub fn add_or_update_chat(client: &mut TdClient, td_chat_enum: &TdChat) { let user_id = UserId::new(private.user_id); client.user_cache.chat_user_ids.insert(chat_id, user_id); // Проверяем, есть ли уже username в кэше (peek не обновляет LRU) - client.user_cache.user_usernames + client + .user_cache + .user_usernames .peek(&user_id) .map(|u| format!("@{}", u)) } diff --git a/src/tdlib/chats.rs b/src/tdlib/chats.rs index 2ab2f91..1d48d6f 100644 --- a/src/tdlib/chats.rs +++ b/src/tdlib/chats.rs @@ -197,10 +197,7 @@ impl ChatManager { ChatType::Secret(_) => "Секретный чат", }; - let is_group = matches!( - &chat.r#type, - ChatType::Supergroup(_) | ChatType::BasicGroup(_) - ); + let is_group = matches!(&chat.r#type, ChatType::Supergroup(_) | ChatType::BasicGroup(_)); // Для личных чатов получаем информацию о пользователе let (bio, phone_number, username, online_status) = if let ChatType::Private(private_chat) = @@ -208,13 +205,15 @@ impl ChatManager { { match functions::get_user(private_chat.user_id, self.client_id).await { Ok(tdlib_rs::enums::User::User(user)) => { - let bio_opt = if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = - functions::get_user_full_info(private_chat.user_id, self.client_id).await - { - full_info.bio.map(|b| b.text) - } else { - None - }; + let bio_opt = + if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = + functions::get_user_full_info(private_chat.user_id, self.client_id) + .await + { + full_info.bio.map(|b| b.text) + } else { + None + }; let online_status_str = match user.status { tdlib_rs::enums::UserStatus::Online(_) => Some("В сети".to_string()), @@ -234,10 +233,7 @@ impl ChatManager { _ => None, }; - let username_opt = user - .usernames - .as_ref() - .map(|u| u.editable_username.clone()); + let username_opt = user.usernames.as_ref().map(|u| u.editable_username.clone()); (bio_opt, Some(user.phone_number.clone()), username_opt, online_status_str) } @@ -257,7 +253,10 @@ impl ChatManager { } else { None }; - let link = full_info.invite_link.as_ref().map(|l| l.invite_link.clone()); + let link = full_info + .invite_link + .as_ref() + .map(|l| l.invite_link.clone()); (Some(full_info.member_count), desc, link) } _ => (None, None, None), @@ -324,7 +323,8 @@ impl ChatManager { /// ).await; /// ``` pub async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) { - let _ = functions::send_chat_action(chat_id.as_i64(), 0, Some(action), self.client_id).await; + let _ = + functions::send_chat_action(chat_id.as_i64(), 0, Some(action), self.client_id).await; } /// Очищает устаревший typing-статус. diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs index 2c5d4c7..e34dbd9 100644 --- a/src/tdlib/client.rs +++ b/src/tdlib/client.rs @@ -1,20 +1,17 @@ use crate::types::{ChatId, MessageId, UserId}; use std::env; use std::path::PathBuf; -use tdlib_rs::enums::{ - ChatList, ConnectionState, Update, UserStatus, - Chat as TdChat -}; -use tdlib_rs::types::Message as TdMessage; +use tdlib_rs::enums::{Chat as TdChat, ChatList, ConnectionState, Update, UserStatus}; use tdlib_rs::functions; - - +use tdlib_rs::types::Message as TdMessage; use super::auth::{AuthManager, AuthState}; use super::chats::ChatManager; use super::messages::MessageManager; use super::reactions::ReactionManager; -use super::types::{ChatInfo, FolderInfo, MessageInfo, NetworkState, ProfileInfo, UserOnlineStatus}; +use super::types::{ + ChatInfo, FolderInfo, MessageInfo, NetworkState, ProfileInfo, UserOnlineStatus, +}; use super::users::UserCache; use crate::notifications::NotificationManager; @@ -75,16 +72,15 @@ impl TdClient { /// A new `TdClient` instance ready for authentication. pub fn new(db_path: PathBuf) -> Self { // Пробуем загрузить credentials из Config (файл или env) - let (api_id, api_hash) = crate::config::Config::load_credentials() - .unwrap_or_else(|_| { - // Fallback на прямое чтение из env (старое поведение) - let api_id = env::var("API_ID") - .unwrap_or_else(|_| "0".to_string()) - .parse() - .unwrap_or(0); - let api_hash = env::var("API_HASH").unwrap_or_default(); - (api_id, api_hash) - }); + let (api_id, api_hash) = crate::config::Config::load_credentials().unwrap_or_else(|_| { + // Fallback на прямое чтение из env (старое поведение) + let api_id = env::var("API_ID") + .unwrap_or_else(|_| "0".to_string()) + .parse() + .unwrap_or(0); + let api_hash = env::var("API_HASH").unwrap_or_default(); + (api_id, api_hash) + }); let client_id = tdlib_rs::create_client(); @@ -106,9 +102,11 @@ impl TdClient { /// Configures notification manager from app config pub fn configure_notifications(&mut self, config: &crate::config::NotificationsConfig) { self.notification_manager.set_enabled(config.enabled); - self.notification_manager.set_only_mentions(config.only_mentions); + self.notification_manager + .set_only_mentions(config.only_mentions); self.notification_manager.set_timeout(config.timeout_ms); - self.notification_manager.set_urgency(config.urgency.clone()); + self.notification_manager + .set_urgency(config.urgency.clone()); // Note: show_preview is used when formatting notification body } @@ -116,7 +114,8 @@ impl TdClient { /// /// Should be called after chats are loaded to ensure muted chats don't trigger notifications. pub fn sync_notification_muted_chats(&mut self) { - self.notification_manager.sync_muted_chats(&self.chat_manager.chats); + self.notification_manager + .sync_muted_chats(&self.chat_manager.chats); } // Делегирование к auth @@ -257,12 +256,17 @@ impl TdClient { .await } - pub async fn get_pinned_messages(&mut self, chat_id: ChatId) -> Result, String> { + pub async fn get_pinned_messages( + &mut self, + chat_id: ChatId, + ) -> Result, String> { self.message_manager.get_pinned_messages(chat_id).await } pub async fn load_current_pinned_message(&mut self, chat_id: ChatId) { - self.message_manager.load_current_pinned_message(chat_id).await + self.message_manager + .load_current_pinned_message(chat_id) + .await } pub async fn search_messages( @@ -442,7 +446,10 @@ impl TdClient { self.chat_manager.typing_status.as_ref() } - pub fn set_typing_status(&mut self, status: Option<(crate::types::UserId, String, std::time::Instant)>) { + pub fn set_typing_status( + &mut self, + status: Option<(crate::types::UserId, String, std::time::Instant)>, + ) { self.chat_manager.typing_status = status; } @@ -450,7 +457,9 @@ impl TdClient { &self.message_manager.pending_view_messages } - pub fn pending_view_messages_mut(&mut self) -> &mut Vec<(crate::types::ChatId, Vec)> { + pub fn pending_view_messages_mut( + &mut self, + ) -> &mut Vec<(crate::types::ChatId, Vec)> { &mut self.message_manager.pending_view_messages } @@ -519,7 +528,11 @@ impl TdClient { }); // Обновляем позиции если они пришли - for pos in update.positions.iter().filter(|p| matches!(p.list, ChatList::Main)) { + for pos in update + .positions + .iter() + .filter(|p| matches!(p.list, ChatList::Main)) + { crate::tdlib::chat_helpers::update_chat(self, chat_id, |chat| { chat.order = pos.order; chat.is_pinned = pos.is_pinned; @@ -530,27 +543,43 @@ impl TdClient { self.chats_mut().sort_by(|a, b| b.order.cmp(&a.order)); } Update::ChatReadInbox(update) => { - crate::tdlib::chat_helpers::update_chat(self, ChatId::new(update.chat_id), |chat| { - chat.unread_count = update.unread_count; - }); + crate::tdlib::chat_helpers::update_chat( + self, + ChatId::new(update.chat_id), + |chat| { + chat.unread_count = update.unread_count; + }, + ); } Update::ChatUnreadMentionCount(update) => { - crate::tdlib::chat_helpers::update_chat(self, ChatId::new(update.chat_id), |chat| { - chat.unread_mention_count = update.unread_mention_count; - }); + crate::tdlib::chat_helpers::update_chat( + self, + ChatId::new(update.chat_id), + |chat| { + chat.unread_mention_count = update.unread_mention_count; + }, + ); } Update::ChatNotificationSettings(update) => { - crate::tdlib::chat_helpers::update_chat(self, ChatId::new(update.chat_id), |chat| { - // mute_for > 0 означает что чат замьючен - chat.is_muted = update.notification_settings.mute_for > 0; - }); + crate::tdlib::chat_helpers::update_chat( + self, + ChatId::new(update.chat_id), + |chat| { + // mute_for > 0 означает что чат замьючен + chat.is_muted = update.notification_settings.mute_for > 0; + }, + ); } Update::ChatReadOutbox(update) => { // Обновляем last_read_outbox_message_id когда собеседник прочитал сообщения let last_read_msg_id = MessageId::new(update.last_read_outbox_message_id); - crate::tdlib::chat_helpers::update_chat(self, ChatId::new(update.chat_id), |chat| { - chat.last_read_outbox_message_id = last_read_msg_id; - }); + crate::tdlib::chat_helpers::update_chat( + self, + ChatId::new(update.chat_id), + |chat| { + chat.last_read_outbox_message_id = last_read_msg_id; + }, + ); // Если это текущий открытый чат — обновляем is_read у сообщений if Some(ChatId::new(update.chat_id)) == self.current_chat_id() { for msg in self.current_chat_messages_mut().iter_mut() { @@ -588,7 +617,9 @@ impl TdClient { UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth, UserStatus::Empty => UserOnlineStatus::LongTimeAgo, }; - self.user_cache.user_statuses.insert(UserId::new(update.user_id), status); + self.user_cache + .user_statuses + .insert(UserId::new(update.user_id), status); } Update::ConnectionState(update) => { // Обновляем состояние сетевого соединения @@ -616,13 +647,15 @@ impl TdClient { } } - - // Helper functions - pub fn extract_message_text_static(message: &TdMessage) -> (String, Vec) { + pub fn extract_message_text_static( + message: &TdMessage, + ) -> (String, Vec) { use tdlib_rs::enums::MessageContent; match &message.content { - MessageContent::MessageText(text) => (text.text.text.clone(), text.text.entities.clone()), + MessageContent::MessageText(text) => { + (text.text.text.clone(), text.text.entities.clone()) + } _ => (String::new(), Vec::new()), } } diff --git a/src/tdlib/client_impl.rs b/src/tdlib/client_impl.rs index ce8bb28..8318199 100644 --- a/src/tdlib/client_impl.rs +++ b/src/tdlib/client_impl.rs @@ -4,7 +4,10 @@ use super::client::TdClient; use super::r#trait::TdClientTrait; -use super::{AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache, UserOnlineStatus}; +use super::{ + AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache, + UserOnlineStatus, +}; use crate::types::{ChatId, MessageId, UserId}; use async_trait::async_trait; use std::path::PathBuf; @@ -52,11 +55,19 @@ impl TdClientTrait for TdClient { } // ============ Message methods ============ - async fn get_chat_history(&mut self, chat_id: ChatId, limit: i32) -> Result, String> { + async fn get_chat_history( + &mut self, + chat_id: ChatId, + limit: i32, + ) -> Result, String> { self.get_chat_history(chat_id, limit).await } - async fn load_older_messages(&mut self, chat_id: ChatId, from_message_id: MessageId) -> Result, String> { + async fn load_older_messages( + &mut self, + chat_id: ChatId, + from_message_id: MessageId, + ) -> Result, String> { self.load_older_messages(chat_id, from_message_id).await } @@ -68,7 +79,11 @@ impl TdClientTrait for TdClient { self.load_current_pinned_message(chat_id).await } - async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result, String> { + async fn search_messages( + &self, + chat_id: ChatId, + query: &str, + ) -> Result, String> { self.search_messages(chat_id, query).await } @@ -148,7 +163,8 @@ impl TdClientTrait for TdClient { chat_id: ChatId, message_id: MessageId, ) -> Result, String> { - self.get_message_available_reactions(chat_id, message_id).await + self.get_message_available_reactions(chat_id, message_id) + .await } async fn toggle_reaction( @@ -276,7 +292,8 @@ impl TdClientTrait for TdClient { // ============ Notification methods ============ fn sync_notification_muted_chats(&mut self) { - self.notification_manager.sync_muted_chats(&self.chat_manager.chats); + self.notification_manager + .sync_muted_chats(&self.chat_manager.chats); } // ============ Account switching ============ diff --git a/src/tdlib/message_conversion.rs b/src/tdlib/message_conversion.rs index 1240a7d..cf529c4 100644 --- a/src/tdlib/message_conversion.rs +++ b/src/tdlib/message_conversion.rs @@ -7,7 +7,10 @@ use crate::types::MessageId; use tdlib_rs::enums::{MessageContent, MessageSender}; use tdlib_rs::types::Message as TdMessage; -use super::types::{ForwardInfo, MediaInfo, PhotoDownloadState, PhotoInfo, ReactionInfo, ReplyInfo, VoiceDownloadState, VoiceInfo}; +use super::types::{ + ForwardInfo, MediaInfo, PhotoDownloadState, PhotoInfo, ReactionInfo, ReplyInfo, + VoiceDownloadState, VoiceInfo, +}; /// Извлекает текст контента из TDLib Message /// @@ -95,9 +98,9 @@ pub async fn extract_sender_name(msg: &TdMessage, client_id: i32) -> String { match &msg.sender_id { MessageSender::User(user) => { match tdlib_rs::functions::get_user(user.user_id, client_id).await { - Ok(tdlib_rs::enums::User::User(u)) => { - format!("{} {}", u.first_name, u.last_name).trim().to_string() - } + Ok(tdlib_rs::enums::User::User(u)) => format!("{} {}", u.first_name, u.last_name) + .trim() + .to_string(), _ => format!("User {}", user.user_id), } } @@ -155,12 +158,7 @@ pub fn extract_media_info(msg: &TdMessage) -> Option { PhotoDownloadState::NotDownloaded }; - Some(MediaInfo::Photo(PhotoInfo { - file_id, - width, - height, - download_state, - })) + Some(MediaInfo::Photo(PhotoInfo { file_id, width, height, download_state })) } MessageContent::MessageVoiceNote(v) => { let file_id = v.voice_note.voice.id; diff --git a/src/tdlib/message_converter.rs b/src/tdlib/message_converter.rs index 6e72d8f..091be35 100644 --- a/src/tdlib/message_converter.rs +++ b/src/tdlib/message_converter.rs @@ -11,11 +11,7 @@ use super::client::TdClient; use super::types::{ForwardInfo, MessageInfo, ReactionInfo, ReplyInfo}; /// Конвертирует TDLib сообщение в MessageInfo -pub fn convert_message( - client: &mut TdClient, - message: &TdMessage, - chat_id: ChatId, -) -> MessageInfo { +pub fn convert_message(client: &mut TdClient, message: &TdMessage, chat_id: ChatId) -> MessageInfo { let sender_name = match &message.sender_id { tdlib_rs::enums::MessageSender::User(user) => { // Пробуем получить имя из кеша (get обновляет LRU порядок) @@ -138,12 +134,7 @@ pub fn extract_reply_info(client: &TdClient, message: &TdMessage) -> Option Option None, } @@ -219,12 +206,7 @@ pub fn update_reply_info_from_loaded_messages(client: &mut TdClient) { let msg_data: std::collections::HashMap = client .current_chat_messages() .iter() - .map(|m| { - ( - m.id().as_i64(), - (m.sender_name().to_string(), m.text().to_string()), - ) - }) + .map(|m| (m.id().as_i64(), (m.sender_name().to_string(), m.text().to_string()))) .collect(); // Обновляем reply_to для сообщений с неполными данными diff --git a/src/tdlib/messages/convert.rs b/src/tdlib/messages/convert.rs index bdf7d5d..0e4a4e6 100644 --- a/src/tdlib/messages/convert.rs +++ b/src/tdlib/messages/convert.rs @@ -12,8 +12,8 @@ impl MessageManager { /// Конвертировать TdMessage в MessageInfo pub(crate) async fn convert_message(&self, msg: &TdMessage) -> Option { use crate::tdlib::message_conversion::{ - extract_content_text, extract_entities, extract_forward_info, - extract_media_info, extract_reactions, extract_reply_info, extract_sender_name, + extract_content_text, extract_entities, extract_forward_info, extract_media_info, + extract_reactions, extract_reply_info, extract_sender_name, }; // Извлекаем все части сообщения используя вспомогательные функции @@ -122,12 +122,7 @@ impl MessageManager { }; // Extract text preview (first 50 chars) - let text_preview: String = orig_info - .content - .text - .chars() - .take(50) - .collect(); + let text_preview: String = orig_info.content.text.chars().take(50).collect(); // Update reply info in all messages that reference this message self.current_chat_messages diff --git a/src/tdlib/messages/mod.rs b/src/tdlib/messages/mod.rs index 1668c94..f08419d 100644 --- a/src/tdlib/messages/mod.rs +++ b/src/tdlib/messages/mod.rs @@ -95,7 +95,8 @@ impl MessageManager { // Ограничиваем размер списка (удаляем старые с начала) if self.current_chat_messages.len() > MAX_MESSAGES_IN_CHAT { - self.current_chat_messages.drain(0..(self.current_chat_messages.len() - MAX_MESSAGES_IN_CHAT)); + self.current_chat_messages + .drain(0..(self.current_chat_messages.len() - MAX_MESSAGES_IN_CHAT)); } } } diff --git a/src/tdlib/messages/operations.rs b/src/tdlib/messages/operations.rs index 8084e2f..b6dd800 100644 --- a/src/tdlib/messages/operations.rs +++ b/src/tdlib/messages/operations.rs @@ -2,9 +2,13 @@ use crate::constants::TDLIB_MESSAGE_LIMIT; use crate::types::{ChatId, MessageId}; -use tdlib_rs::enums::{InputMessageContent, InputMessageReplyTo, SearchMessagesFilter, TextParseMode}; +use tdlib_rs::enums::{ + InputMessageContent, InputMessageReplyTo, SearchMessagesFilter, TextParseMode, +}; use tdlib_rs::functions; -use tdlib_rs::types::{FormattedText, InputMessageReplyToMessage, InputMessageText, TextParseModeMarkdown}; +use tdlib_rs::types::{ + FormattedText, InputMessageReplyToMessage, InputMessageText, TextParseModeMarkdown, +}; use tokio::time::{sleep, Duration}; use crate::tdlib::types::{MessageInfo, ReplyInfo}; @@ -103,9 +107,10 @@ impl MessageManager { // Если это первая загрузка и получили мало сообщений - продолжаем попытки // TDLib может подгружать данные с сервера постепенно - if all_messages.is_empty() && - received_count < (chunk_size as usize) && - attempt < max_attempts_per_chunk { + if all_messages.is_empty() + && received_count < (chunk_size as usize) + && attempt < max_attempts_per_chunk + { // Даём TDLib время на синхронизацию с сервером sleep(Duration::from_millis(100)).await; continue; @@ -233,17 +238,20 @@ impl MessageManager { /// let pinned = msg_manager.get_pinned_messages(chat_id).await?; /// println!("Found {} pinned messages", pinned.len()); /// ``` - pub async fn get_pinned_messages(&mut self, chat_id: ChatId) -> Result, String> { + pub async fn get_pinned_messages( + &mut self, + chat_id: ChatId, + ) -> Result, String> { let result = functions::search_chat_messages( chat_id.as_i64(), String::new(), None, - 0, // from_message_id - 0, // offset - 100, // limit + 0, // from_message_id + 0, // offset + 100, // limit Some(SearchMessagesFilter::Pinned), - 0, // message_thread_id - 0, // saved_messages_topic_id + 0, // message_thread_id + 0, // saved_messages_topic_id self.client_id, ) .await; @@ -310,8 +318,8 @@ impl MessageManager { 0, // offset 100, // limit None, - 0, // message_thread_id - 0, // saved_messages_topic_id + 0, // message_thread_id + 0, // saved_messages_topic_id self.client_id, ) .await; @@ -381,15 +389,9 @@ impl MessageManager { .await { Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => { - FormattedText { - text: ft.text, - entities: ft.entities, - } + FormattedText { text: ft.text, entities: ft.entities } } - Err(_) => FormattedText { - text: text.clone(), - entities: vec![], - }, + Err(_) => FormattedText { text: text.clone(), entities: vec![] }, }; let content = InputMessageContent::InputMessageText(InputMessageText { @@ -460,15 +462,9 @@ impl MessageManager { .await { Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => { - FormattedText { - text: ft.text, - entities: ft.entities, - } + FormattedText { text: ft.text, entities: ft.entities } } - Err(_) => FormattedText { - text: text.clone(), - entities: vec![], - }, + Err(_) => FormattedText { text: text.clone(), entities: vec![] }, }; let content = InputMessageContent::InputMessageText(InputMessageText { @@ -477,8 +473,13 @@ impl MessageManager { clear_draft: true, }); - let result = - functions::edit_message_text(chat_id.as_i64(), message_id.as_i64(), content, self.client_id).await; + let result = functions::edit_message_text( + chat_id.as_i64(), + message_id.as_i64(), + content, + self.client_id, + ) + .await; match result { Ok(tdlib_rs::enums::Message::Message(msg)) => self @@ -509,7 +510,8 @@ impl MessageManager { ) -> Result<(), String> { let message_ids_i64: Vec = message_ids.into_iter().map(|id| id.as_i64()).collect(); let result = - functions::delete_messages(chat_id.as_i64(), message_ids_i64, revoke, self.client_id).await; + functions::delete_messages(chat_id.as_i64(), message_ids_i64, revoke, self.client_id) + .await; match result { Ok(_) => Ok(()), Err(e) => Err(format!("Ошибка удаления: {:?}", e)), @@ -577,17 +579,15 @@ impl MessageManager { reply_to: None, date: 0, input_message_text: InputMessageContent::InputMessageText(InputMessageText { - text: FormattedText { - text: text.clone(), - entities: vec![], - }, + text: FormattedText { text: text.clone(), entities: vec![] }, link_preview_options: None, clear_draft: false, }), }) }; - let result = functions::set_chat_draft_message(chat_id.as_i64(), 0, draft, self.client_id).await; + let result = + functions::set_chat_draft_message(chat_id.as_i64(), 0, draft, self.client_id).await; match result { Ok(_) => Ok(()), @@ -612,7 +612,8 @@ impl MessageManager { for (chat_id, message_ids) in batch { let ids: Vec = message_ids.iter().map(|id| id.as_i64()).collect(); - let _ = functions::view_messages(chat_id.as_i64(), ids, None, true, self.client_id).await; + let _ = + functions::view_messages(chat_id.as_i64(), ids, None, true, self.client_id).await; } } } diff --git a/src/tdlib/mod.rs b/src/tdlib/mod.rs index 09948ef..4d54f19 100644 --- a/src/tdlib/mod.rs +++ b/src/tdlib/mod.rs @@ -4,8 +4,8 @@ mod chat_helpers; // Chat management helpers pub mod chats; pub mod client; mod client_impl; // Private module for trait implementation -mod message_converter; // Message conversion utilities (for client.rs) mod message_conversion; // Message conversion utilities (for messages.rs) +mod message_converter; // Message conversion utilities (for client.rs) pub mod messages; pub mod reactions; pub mod r#trait; diff --git a/src/tdlib/reactions.rs b/src/tdlib/reactions.rs index 5aa285a..9682aa4 100644 --- a/src/tdlib/reactions.rs +++ b/src/tdlib/reactions.rs @@ -69,7 +69,8 @@ impl ReactionManager { message_id: MessageId, ) -> Result, String> { // Получаем сообщение - let msg_result = functions::get_message(chat_id.as_i64(), message_id.as_i64(), self.client_id).await; + let msg_result = + functions::get_message(chat_id.as_i64(), message_id.as_i64(), self.client_id).await; let _msg = match msg_result { Ok(m) => m, Err(e) => return Err(format!("Ошибка получения сообщения: {:?}", e)), diff --git a/src/tdlib/trait.rs b/src/tdlib/trait.rs index 826e522..a46ee2f 100644 --- a/src/tdlib/trait.rs +++ b/src/tdlib/trait.rs @@ -32,11 +32,23 @@ pub trait TdClientTrait: Send { fn clear_stale_typing_status(&mut self) -> bool; // ============ Message methods ============ - async fn get_chat_history(&mut self, chat_id: ChatId, limit: i32) -> Result, String>; - async fn load_older_messages(&mut self, chat_id: ChatId, from_message_id: MessageId) -> Result, String>; + async fn get_chat_history( + &mut self, + chat_id: ChatId, + limit: i32, + ) -> Result, String>; + async fn load_older_messages( + &mut self, + chat_id: ChatId, + from_message_id: MessageId, + ) -> Result, String>; async fn get_pinned_messages(&mut self, chat_id: ChatId) -> Result, String>; async fn load_current_pinned_message(&mut self, chat_id: ChatId); - async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result, String>; + async fn search_messages( + &self, + chat_id: ChatId, + query: &str, + ) -> Result, String>; async fn send_message( &mut self, diff --git a/src/tdlib/types.rs b/src/tdlib/types.rs index a929a83..ab0d955 100644 --- a/src/tdlib/types.rs +++ b/src/tdlib/types.rs @@ -179,11 +179,7 @@ impl MessageInfo { edit_date, media_album_id: 0, }, - content: MessageContent { - text: content, - entities, - media: None, - }, + content: MessageContent { text: content, entities, media: None }, state: MessageState { is_outgoing, is_read, @@ -191,11 +187,7 @@ impl MessageInfo { can_be_deleted_only_for_self, can_be_deleted_for_all_users, }, - interactions: MessageInteractions { - reply_to, - forward_from, - reactions, - }, + interactions: MessageInteractions { reply_to, forward_from, reactions }, } } @@ -251,10 +243,7 @@ impl MessageInfo { /// Checks if the message contains a mention (@username or user mention) pub fn has_mention(&self) -> bool { self.content.entities.iter().any(|entity| { - matches!( - entity.r#type, - TextEntityType::Mention | TextEntityType::MentionName(_) - ) + matches!(entity.r#type, TextEntityType::Mention | TextEntityType::MentionName(_)) }) } @@ -314,13 +303,13 @@ impl MessageInfo { } /// Builder для удобного создания MessageInfo с fluent API -/// +/// /// # Примеры -/// +/// /// ``` /// use tele_tui::tdlib::MessageBuilder; /// use tele_tui::types::MessageId; -/// +/// /// let message = MessageBuilder::new(MessageId::new(123)) /// .sender_name("Alice") /// .text("Hello, world!") @@ -500,7 +489,6 @@ impl MessageBuilder { } } - #[cfg(test)] mod tests { use super::*; @@ -568,9 +556,7 @@ mod tests { #[test] fn test_message_builder_with_reactions() { let reaction = ReactionInfo { - emoji: "👍".to_string(), - count: 5, - is_chosen: true, + emoji: "👍".to_string(), count: 5, is_chosen: true }; let message = MessageBuilder::new(MessageId::new(300)) @@ -628,9 +614,9 @@ mod tests { .entities(vec![TextEntity { offset: 6, length: 4, - r#type: TextEntityType::MentionName( - tdlib_rs::types::TextEntityTypeMentionName { user_id: 123 }, - ), + r#type: TextEntityType::MentionName(tdlib_rs::types::TextEntityTypeMentionName { + user_id: 123, + }), }]) .build(); assert!(message_with_mention_name.has_mention()); diff --git a/src/tdlib/update_handlers.rs b/src/tdlib/update_handlers.rs index cd933e5..379c963 100644 --- a/src/tdlib/update_handlers.rs +++ b/src/tdlib/update_handlers.rs @@ -5,12 +5,10 @@ use crate::types::{ChatId, MessageId, UserId}; use std::time::Instant; -use tdlib_rs::enums::{ - AuthorizationState, ChatAction, ChatList, MessageSender, -}; +use tdlib_rs::enums::{AuthorizationState, ChatAction, ChatList, MessageSender}; use tdlib_rs::types::{ - UpdateChatAction, UpdateChatDraftMessage, UpdateChatPosition, - UpdateMessageInteractionInfo, UpdateMessageSendSucceeded, UpdateNewMessage, UpdateUser, + UpdateChatAction, UpdateChatDraftMessage, UpdateChatPosition, UpdateMessageInteractionInfo, + UpdateMessageSendSucceeded, UpdateNewMessage, UpdateUser, }; use super::auth::AuthState; @@ -25,24 +23,24 @@ pub fn handle_new_message_update(client: &mut TdClient, new_msg: UpdateNewMessag if Some(chat_id) != client.current_chat_id() { // Find and clone chat info to avoid borrow checker issues if let Some(chat) = client.chats().iter().find(|c| c.id == chat_id).cloned() { - let msg_info = crate::tdlib::message_converter::convert_message(client, &new_msg.message, chat_id); + let msg_info = + crate::tdlib::message_converter::convert_message(client, &new_msg.message, chat_id); // Get sender name (from message or user cache) let sender_name = msg_info.sender_name(); // Send notification - let _ = client.notification_manager.notify_new_message( - &chat, - &msg_info, - sender_name, - ); + let _ = client + .notification_manager + .notify_new_message(&chat, &msg_info, sender_name); } return; } // Добавляем новое сообщение если это текущий открытый чат - let msg_info = crate::tdlib::message_converter::convert_message(client, &new_msg.message, chat_id); + let msg_info = + crate::tdlib::message_converter::convert_message(client, &new_msg.message, chat_id); let msg_id = msg_info.id(); let is_incoming = !msg_info.is_outgoing(); @@ -74,7 +72,9 @@ pub fn handle_new_message_update(client: &mut TdClient, new_msg: UpdateNewMessag client.push_message(msg_info.clone()); // Если это входящее сообщение — добавляем в очередь для отметки как прочитанное if is_incoming { - client.pending_view_messages_mut().push((chat_id, vec![msg_id])); + client + .pending_view_messages_mut() + .push((chat_id, vec![msg_id])); } } } @@ -181,14 +181,21 @@ pub fn handle_user_update(client: &mut TdClient, update: UpdateUser) { } else { format!("{} {}", user.first_name, user.last_name) }; - client.user_cache.user_names.insert(UserId::new(user.id), display_name); + client + .user_cache + .user_names + .insert(UserId::new(user.id), display_name); // Сохраняем username если есть (с упрощённым извлечением через and_then) - if let Some(username) = user.usernames + if let Some(username) = user + .usernames .as_ref() .and_then(|u| u.active_usernames.first()) { - client.user_cache.user_usernames.insert(UserId::new(user.id), username.to_string()); + client + .user_cache + .user_usernames + .insert(UserId::new(user.id), username.to_string()); // Обновляем username в чатах, связанных с этим пользователем for (&chat_id, &user_id) in &client.user_cache.chat_user_ids.clone() { if user_id == UserId::new(user.id) { @@ -273,7 +280,8 @@ pub fn handle_message_send_succeeded_update( }; // Конвертируем новое сообщение - let mut new_msg = crate::tdlib::message_converter::convert_message(client, &update.message, chat_id); + let mut new_msg = + crate::tdlib::message_converter::convert_message(client, &update.message, chat_id); // Сохраняем reply_info из старого сообщения (если было) let old_reply = client.current_chat_messages()[idx] diff --git a/src/tdlib/users.rs b/src/tdlib/users.rs index 641a36b..3090362 100644 --- a/src/tdlib/users.rs +++ b/src/tdlib/users.rs @@ -175,7 +175,9 @@ impl UserCache { } // Сохраняем имя - let display_name = format!("{} {}", user.first_name, user.last_name).trim().to_string(); + let display_name = format!("{} {}", user.first_name, user.last_name) + .trim() + .to_string(); self.user_names.insert(UserId::new(user_id), display_name); // Обновляем статус @@ -220,7 +222,9 @@ impl UserCache { // Загружаем пользователя match functions::get_user(user_id.as_i64(), self.client_id).await { Ok(User::User(user)) => { - let name = format!("{} {}", user.first_name, user.last_name).trim().to_string(); + let name = format!("{} {}", user.first_name, user.last_name) + .trim() + .to_string(); name } _ => format!("User {}", user_id.as_i64()), @@ -257,8 +261,7 @@ impl UserCache { } Err(_) => { // Если не удалось загрузить, сохраняем placeholder - self.user_names - .insert(user_id, format!("User {}", user_id)); + self.user_names.insert(user_id, format!("User {}", user_id)); } } } diff --git a/src/types.rs b/src/types.rs index 7d80a7d..4926792 100644 --- a/src/types.rs +++ b/src/types.rs @@ -136,7 +136,7 @@ mod tests { // let chat_id = ChatId::new(1); // let message_id = MessageId::new(1); // if chat_id == message_id { } // ERROR: mismatched types - + // Runtime values can be the same, but types are different let chat_id = ChatId::new(1); let message_id = MessageId::new(1); diff --git a/src/ui/auth.rs b/src/ui/auth.rs index ac45d61..eecc2ab 100644 --- a/src/ui/auth.rs +++ b/src/ui/auth.rs @@ -1,6 +1,6 @@ use crate::app::App; -use crate::tdlib::TdClientTrait; use crate::tdlib::AuthState; +use crate::tdlib::TdClientTrait; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, diff --git a/src/ui/chat_list.rs b/src/ui/chat_list.rs index 3e1119b..2e5cc64 100644 --- a/src/ui/chat_list.rs +++ b/src/ui/chat_list.rs @@ -1,7 +1,7 @@ //! Chat list panel: search box, chat items, and user online status. -use crate::app::App; use crate::app::methods::{compose::ComposeMethods, search::SearchMethods}; +use crate::app::App; use crate::tdlib::TdClientTrait; use crate::tdlib::UserOnlineStatus; use crate::ui::components; @@ -76,7 +76,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { app.selected_chat_id } else { let filtered = app.get_filtered_chats(); - app.chat_list_state.selected().and_then(|i| filtered.get(i).map(|c| c.id)) + app.chat_list_state + .selected() + .and_then(|i| filtered.get(i).map(|c| c.id)) }; let (status_text, status_color) = match status_chat_id { Some(chat_id) => format_user_status(app.td_client.get_user_status_by_chat_id(chat_id)), diff --git a/src/ui/components/emoji_picker.rs b/src/ui/components/emoji_picker.rs index e0a384c..d17e2f0 100644 --- a/src/ui/components/emoji_picker.rs +++ b/src/ui/components/emoji_picker.rs @@ -29,12 +29,7 @@ pub fn render_emoji_picker( let x = area.x + (area.width.saturating_sub(modal_width)) / 2; let y = area.y + (area.height.saturating_sub(modal_height)) / 2; - let modal_area = Rect::new( - x, - y, - modal_width.min(area.width), - modal_height.min(area.height), - ); + let modal_area = Rect::new(x, y, modal_width.min(area.width), modal_height.min(area.height)); // Очищаем область под модалкой f.render_widget(Clear, modal_area); @@ -87,10 +82,7 @@ pub fn render_emoji_picker( .add_modifier(Modifier::BOLD), ), Span::raw("Добавить "), - Span::styled( - " [Esc] ", - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - ), + Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::raw("Отмена"), ])); diff --git a/src/ui/components/input_field.rs b/src/ui/components/input_field.rs index ddca359..66a259a 100644 --- a/src/ui/components/input_field.rs +++ b/src/ui/components/input_field.rs @@ -34,10 +34,7 @@ pub fn render_input_field( // Символ под курсором (или █ если курсор в конце) if safe_cursor_pos < chars.len() { let cursor_char = chars[safe_cursor_pos].to_string(); - spans.push(Span::styled( - cursor_char, - Style::default().fg(Color::Black).bg(color), - )); + spans.push(Span::styled(cursor_char, Style::default().fg(Color::Black).bg(color))); } else { // Курсор в конце - показываем блок spans.push(Span::styled("█", Style::default().fg(color))); diff --git a/src/ui/components/message_bubble.rs b/src/ui/components/message_bubble.rs index 4a4a521..d463363 100644 --- a/src/ui/components/message_bubble.rs +++ b/src/ui/components/message_bubble.rs @@ -7,9 +7,9 @@ use crate::config::Config; use crate::formatting; -use crate::tdlib::{MessageInfo, PlaybackState, PlaybackStatus}; #[cfg(feature = "images")] use crate::tdlib::PhotoDownloadState; +use crate::tdlib::{MessageInfo, PlaybackState, PlaybackStatus}; use crate::types::MessageId; use crate::utils::{format_date, format_timestamp_with_tz}; use ratatui::{ @@ -36,10 +36,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec { } if all_lines.is_empty() { - all_lines.push(WrappedLine { - text: String::new(), - start_offset: 0, - }); + all_lines.push(WrappedLine { text: String::new(), start_offset: 0 }); } all_lines @@ -48,10 +45,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec { /// Разбивает один абзац (без `\n`) на строки по ширине fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec { if max_width == 0 { - return vec![WrappedLine { - text: text.to_string(), - start_offset: base_offset, - }]; + return vec![WrappedLine { text: text.to_string(), start_offset: base_offset }]; } let mut result = Vec::new(); @@ -122,10 +116,7 @@ fn wrap_paragraph(text: &str, max_width: usize, base_offset: usize) -> Vec Vec Vec> { +pub fn render_date_separator( + date: i32, + content_width: usize, + is_first: bool, +) -> Vec> { let mut lines = Vec::new(); if !is_first { @@ -276,10 +271,8 @@ pub fn render_message_bubble( Span::styled(reply_line, Style::default().fg(Color::Cyan)), ])); } else { - lines.push(Line::from(vec![Span::styled( - reply_line, - Style::default().fg(Color::Cyan), - )])); + lines + .push(Line::from(vec![Span::styled(reply_line, Style::default().fg(Color::Cyan))])); } } @@ -301,9 +294,13 @@ pub fn render_message_bubble( let is_last_line = i == total_wrapped - 1; let line_len = wrapped.text.chars().count(); - let line_entities = - formatting::adjust_entities_for_substring(msg.entities(), wrapped.start_offset, line_len); - let formatted_spans = formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color); + let line_entities = formatting::adjust_entities_for_substring( + msg.entities(), + wrapped.start_offset, + line_len, + ); + let formatted_spans = + formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color); if is_last_line { let full_len = line_len + time_mark_len + marker_len; @@ -313,14 +310,19 @@ pub fn render_message_bubble( // Одна строка — маркер на ней line_spans.push(Span::styled( selection_marker, - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), )); } else if is_selected { // Последняя строка multi-line — пробелы вместо маркера line_spans.push(Span::raw(" ".repeat(marker_len))); } line_spans.extend(formatted_spans); - line_spans.push(Span::styled(format!(" {}", time_mark), Style::default().fg(Color::Gray))); + line_spans.push(Span::styled( + format!(" {}", time_mark), + Style::default().fg(Color::Gray), + )); lines.push(Line::from(line_spans)); } else { let padding = content_width.saturating_sub(line_len + marker_len + 1); @@ -328,7 +330,9 @@ pub fn render_message_bubble( if i == 0 && is_selected { line_spans.push(Span::styled( selection_marker, - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), )); } else if is_selected { // Средние строки multi-line — пробелы вместо маркера @@ -350,19 +354,26 @@ pub fn render_message_bubble( for (i, wrapped) in wrapped_lines.into_iter().enumerate() { let line_len = wrapped.text.chars().count(); - let line_entities = - formatting::adjust_entities_for_substring(msg.entities(), wrapped.start_offset, line_len); - let formatted_spans = formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color); + let line_entities = formatting::adjust_entities_for_substring( + msg.entities(), + wrapped.start_offset, + line_len, + ); + let formatted_spans = + formatting::format_text_with_entities(&wrapped.text, &line_entities, msg_color); if i == 0 { let mut line_spans = vec![]; if is_selected { line_spans.push(Span::styled( selection_marker, - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), )); } - line_spans.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray))); + line_spans + .push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray))); line_spans.push(Span::raw(" ")); line_spans.extend(formatted_spans); lines.push(Line::from(line_spans)); @@ -439,10 +450,7 @@ pub fn render_message_bubble( _ => "⏹", }; let bar = render_progress_bar(ps.position, ps.duration, 20); - format!( - "{} {} {:.0}s/{:.0}s", - icon, bar, ps.position, ps.duration - ) + format!("{} {} {:.0}s/{:.0}s", icon, bar, ps.position, ps.duration) } else { let waveform = render_waveform(&voice.waveform, 20); format!(" {} {:.0}s", waveform, voice.duration) @@ -456,10 +464,7 @@ pub fn render_message_bubble( Span::styled(status_line, Style::default().fg(Color::Cyan)), ])); } else { - lines.push(Line::from(Span::styled( - status_line, - Style::default().fg(Color::Cyan), - ))); + lines.push(Line::from(Span::styled(status_line, Style::default().fg(Color::Cyan)))); } } } @@ -477,10 +482,8 @@ pub fn render_message_bubble( Span::styled(status, Style::default().fg(Color::Yellow)), ])); } else { - lines.push(Line::from(Span::styled( - status, - Style::default().fg(Color::Yellow), - ))); + lines + .push(Line::from(Span::styled(status, Style::default().fg(Color::Yellow)))); } } PhotoDownloadState::Error(e) => { @@ -492,10 +495,7 @@ pub fn render_message_bubble( Span::styled(status, Style::default().fg(Color::Red)), ])); } else { - lines.push(Line::from(Span::styled( - status, - Style::default().fg(Color::Red), - ))); + lines.push(Line::from(Span::styled(status, Style::default().fg(Color::Red)))); } } PhotoDownloadState::Downloaded(_) => { @@ -540,7 +540,9 @@ pub fn render_album_bubble( content_width: usize, selected_msg_id: Option, ) -> (Vec>, Vec) { - use crate::constants::{ALBUM_GRID_MAX_COLS, ALBUM_PHOTO_GAP, ALBUM_PHOTO_HEIGHT, ALBUM_PHOTO_WIDTH}; + use crate::constants::{ + ALBUM_GRID_MAX_COLS, ALBUM_PHOTO_GAP, ALBUM_PHOTO_HEIGHT, ALBUM_PHOTO_WIDTH, + }; let mut lines: Vec> = Vec::new(); let mut deferred: Vec = Vec::new(); @@ -569,12 +571,12 @@ pub fn render_album_bubble( // Добавляем маркер выбора на первую строку if is_selected { - lines.push(Line::from(vec![ - Span::styled( - selection_marker, - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), - ), - ])); + lines.push(Line::from(vec![Span::styled( + selection_marker, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )])); } let grid_start_line = lines.len(); @@ -608,7 +610,9 @@ pub fn render_album_bubble( let x_off = if is_outgoing { let grid_width = cols as u16 * ALBUM_PHOTO_WIDTH + (cols as u16).saturating_sub(1) * ALBUM_PHOTO_GAP; - let padding = content_width.saturating_sub(grid_width as usize + 1) as u16; + let padding = content_width + .saturating_sub(grid_width as usize + 1) + as u16; padding + col as u16 * (ALBUM_PHOTO_WIDTH + ALBUM_PHOTO_GAP) } else { col as u16 * (ALBUM_PHOTO_WIDTH + ALBUM_PHOTO_GAP) @@ -617,7 +621,8 @@ pub fn render_album_bubble( deferred.push(DeferredImageRender { message_id: msg.id(), photo_path: path.clone(), - line_offset: grid_start_line + row * ALBUM_PHOTO_HEIGHT as usize, + line_offset: grid_start_line + + row * ALBUM_PHOTO_HEIGHT as usize, x_offset: x_off, width: ALBUM_PHOTO_WIDTH, height: ALBUM_PHOTO_HEIGHT, @@ -644,10 +649,7 @@ pub fn render_album_bubble( } PhotoDownloadState::NotDownloaded => { if line_in_row == ALBUM_PHOTO_HEIGHT / 2 { - spans.push(Span::styled( - "📷", - Style::default().fg(Color::Gray), - )); + spans.push(Span::styled("📷", Style::default().fg(Color::Gray))); } } } @@ -706,9 +708,10 @@ pub fn render_album_bubble( Span::styled(time_text, Style::default().fg(Color::Gray)), ])); } else { - lines.push(Line::from(vec![ - Span::styled(format!(" {}", time_text), Style::default().fg(Color::Gray)), - ])); + lines.push(Line::from(vec![Span::styled( + format!(" {}", time_text), + Style::default().fg(Color::Gray), + )])); } } diff --git a/src/ui/components/message_list.rs b/src/ui/components/message_list.rs index 5e397ce..e5b5156 100644 --- a/src/ui/components/message_list.rs +++ b/src/ui/components/message_list.rs @@ -91,7 +91,10 @@ pub fn calculate_scroll_offset( } /// Renders a help bar with keyboard shortcuts -pub fn render_help_bar(shortcuts: &[(&str, &str, Color)], border_color: Color) -> Paragraph<'static> { +pub fn render_help_bar( + shortcuts: &[(&str, &str, Color)], + border_color: Color, +) -> Paragraph<'static> { let mut spans: Vec> = Vec::new(); for (i, (key, label, color)) in shortcuts.iter().enumerate() { if i > 0 { @@ -99,9 +102,7 @@ pub fn render_help_bar(shortcuts: &[(&str, &str, Color)], border_color: Color) - } spans.push(Span::styled( format!(" {} ", key), - Style::default() - .fg(*color) - .add_modifier(Modifier::BOLD), + Style::default().fg(*color).add_modifier(Modifier::BOLD), )); spans.push(Span::raw(label.to_string())); } diff --git a/src/ui/components/mod.rs b/src/ui/components/mod.rs index d338a9d..d904d3c 100644 --- a/src/ui/components/mod.rs +++ b/src/ui/components/mod.rs @@ -1,17 +1,17 @@ //! Reusable UI components: message bubbles, input fields, modals, lists. -pub mod modal; +pub mod chat_list_item; +pub mod emoji_picker; pub mod input_field; pub mod message_bubble; pub mod message_list; -pub mod chat_list_item; -pub mod emoji_picker; +pub mod modal; // Экспорт основных функций -pub use input_field::render_input_field; pub use chat_list_item::render_chat_list_item; pub use emoji_picker::render_emoji_picker; -pub use message_bubble::{render_date_separator, render_message_bubble, render_sender_header}; +pub use input_field::render_input_field; #[cfg(feature = "images")] -pub use message_bubble::{DeferredImageRender, calculate_image_height, render_album_bubble}; -pub use message_list::{render_message_item, calculate_scroll_offset, render_help_bar}; +pub use message_bubble::{calculate_image_height, render_album_bubble, DeferredImageRender}; +pub use message_bubble::{render_date_separator, render_message_bubble, render_sender_header}; +pub use message_list::{calculate_scroll_offset, render_help_bar, render_message_item}; diff --git a/src/ui/components/modal.rs b/src/ui/components/modal.rs index 8c15102..73b7ca6 100644 --- a/src/ui/components/modal.rs +++ b/src/ui/components/modal.rs @@ -74,10 +74,7 @@ pub fn render_delete_confirm_modal(f: &mut Frame, area: Rect) { ), Span::raw("Да"), Span::raw(" "), - Span::styled( - " [n/Esc] ", - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - ), + Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::raw("Нет"), ]), ]; diff --git a/src/ui/compose_bar.rs b/src/ui/compose_bar.rs index daa5224..134cf90 100644 --- a/src/ui/compose_bar.rs +++ b/src/ui/compose_bar.rs @@ -1,8 +1,8 @@ //! Compose bar / input box rendering +use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods}; use crate::app::App; use crate::app::InputMode; -use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods}; use crate::tdlib::TdClientTrait; use crate::ui::components; use ratatui::{ @@ -124,13 +124,18 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } else if app.input_mode == InputMode::Normal { // Normal mode — dim, no cursor if app.message_input.is_empty() { - let line = Line::from(vec![ - Span::styled("> Press i to type...", Style::default().fg(Color::DarkGray)), - ]); + let line = Line::from(vec![Span::styled( + "> Press i to type...", + Style::default().fg(Color::DarkGray), + )]); (line, "") } else { let draft_preview: String = app.message_input.chars().take(60).collect(); - let ellipsis = if app.message_input.chars().count() > 60 { "..." } else { "" }; + let ellipsis = if app.message_input.chars().count() > 60 { + "..." + } else { + "" + }; let line = Line::from(Span::styled( format!("> {}{}", draft_preview, ellipsis), Style::default().fg(Color::DarkGray), @@ -163,7 +168,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } else { Style::default().fg(Color::DarkGray) }; - Block::default().borders(Borders::ALL).border_style(border_style) + Block::default() + .borders(Borders::ALL) + .border_style(border_style) } else { let title_color = if app.is_replying() || app.is_forwarding() { Color::Cyan diff --git a/src/ui/footer.rs b/src/ui/footer.rs index 2daed6e..135c399 100644 --- a/src/ui/footer.rs +++ b/src/ui/footer.rs @@ -1,7 +1,7 @@ use crate::app::App; use crate::app::InputMode; -use crate::tdlib::TdClientTrait; use crate::tdlib::NetworkState; +use crate::tdlib::TdClientTrait; use ratatui::{ layout::Rect, style::{Color, Style}, @@ -31,7 +31,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } else if let Some(err) = &app.error_message { format!(" {}{}Error: {} ", account_indicator, network_indicator, err) } else if app.is_searching { - format!(" {}{}↑/↓: Navigate | Enter: Select | Esc: Cancel ", account_indicator, network_indicator) + format!( + " {}{}↑/↓: Navigate | Enter: Select | Esc: Cancel ", + account_indicator, network_indicator + ) } else if app.selected_chat_id.is_some() { let mode_str = match app.input_mode { InputMode::Normal => "[NORMAL] j/k: Nav | i: Insert | d/r/f/y: Actions | Esc: Close", diff --git a/src/ui/messages.rs b/src/ui/messages.rs index 843e73b..b931e8e 100644 --- a/src/ui/messages.rs +++ b/src/ui/messages.rs @@ -3,10 +3,10 @@ //! Renders message bubbles grouped by date/sender, pinned bar, and delegates //! to modals (search, pinned, reactions, delete) and compose_bar. -use crate::app::App; use crate::app::methods::{messages::MessageMethods, modal::ModalMethods, search::SearchMethods}; -use crate::tdlib::TdClientTrait; +use crate::app::App; use crate::message_grouping::{group_messages, MessageGroup}; +use crate::tdlib::TdClientTrait; use crate::ui::components; use crate::ui::{compose_bar, modals}; use ratatui::{ @@ -18,7 +18,12 @@ use ratatui::{ }; /// Рендерит заголовок чата с typing status -fn render_chat_header(f: &mut Frame, area: Rect, app: &App, chat: &crate::tdlib::ChatInfo) { +fn render_chat_header( + f: &mut Frame, + area: Rect, + app: &App, + chat: &crate::tdlib::ChatInfo, +) { let typing_action = app .td_client .typing_status() @@ -34,10 +39,7 @@ fn render_chat_header(f: &mut Frame, area: Rect, app: &App, .add_modifier(Modifier::BOLD), )]; if let Some(username) = &chat.username { - spans.push(Span::styled( - format!(" {}", username), - Style::default().fg(Color::Gray), - )); + spans.push(Span::styled(format!(" {}", username), Style::default().fg(Color::Gray))); } spans.push(Span::styled( format!(" {}", action), @@ -90,8 +92,7 @@ fn render_pinned_bar(f: &mut Frame, area: Rect, app: &App) Span::raw(" ".repeat(padding)), Span::styled(pinned_hint, Style::default().fg(Color::Gray)), ]); - let pinned_bar = - Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40))); + let pinned_bar = Paragraph::new(pinned_line).style(Style::default().bg(Color::Rgb(40, 20, 40))); f.render_widget(pinned_bar, area); } @@ -104,9 +105,7 @@ pub(super) struct WrappedLine { /// (используется только для search/pinned режимов, основной рендеринг через message_bubble) pub(super) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec { if max_width == 0 { - return vec![WrappedLine { - text: text.to_string(), - }]; + return vec![WrappedLine { text: text.to_string() }]; } let mut result = Vec::new(); @@ -131,9 +130,7 @@ pub(super) fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec Vec(f: &mut Frame, area: Rect, app: &mut Ap is_first_date = false; is_first_sender = true; // Сбрасываем счётчик заголовков после даты } - MessageGroup::SenderHeader { - is_outgoing, - sender_name, - } => { + MessageGroup::SenderHeader { is_outgoing, sender_name } => { // Рендерим заголовок отправителя lines.extend(components::render_sender_header( is_outgoing, @@ -240,9 +228,16 @@ fn render_message_list(f: &mut Frame, area: Rect, app: &mut Ap // Собираем deferred image renders для всех загруженных фото #[cfg(feature = "images")] if let Some(photo) = msg.photo_info() { - if let crate::tdlib::PhotoDownloadState::Downloaded(path) = &photo.download_state { - let inline_width = content_width.min(crate::constants::INLINE_IMAGE_MAX_WIDTH); - let img_height = components::calculate_image_height(photo.width, photo.height, inline_width); + if let crate::tdlib::PhotoDownloadState::Downloaded(path) = + &photo.download_state + { + let inline_width = + content_width.min(crate::constants::INLINE_IMAGE_MAX_WIDTH); + let img_height = components::calculate_image_height( + photo.width, + photo.height, + inline_width, + ); let img_width = inline_width as u16; let bubble_len = bubble_lines.len(); let placeholder_start = lines.len() + bubble_len - img_height as usize; @@ -352,7 +347,8 @@ fn render_message_list(f: &mut Frame, area: Rect, app: &mut Ap use ratatui_image::StatefulImage; // THROTTLING: Рендерим изображения максимум 15 FPS (каждые 66ms) - let should_render_images = app.last_image_render_time + let should_render_images = app + .last_image_render_time .map(|t| t.elapsed() > std::time::Duration::from_millis(66)) .unwrap_or(true); @@ -384,7 +380,7 @@ fn render_message_list(f: &mut Frame, area: Rect, app: &mut Ap if let Some(renderer) = &mut app.inline_image_renderer { // Загружаем только если видимо (early return если уже в кеше) let _ = renderer.load_image(d.message_id, &d.photo_path); - + if let Some(protocol) = renderer.get_protocol(&d.message_id) { f.render_stateful_widget(StatefulImage::default(), img_rect, protocol); } @@ -487,14 +483,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { } // Модалка выбора реакции - if let crate::app::ChatState::ReactionPicker { - available_reactions, - selected_index, - .. - } = &app.chat_state + if let crate::app::ChatState::ReactionPicker { available_reactions, selected_index, .. } = + &app.chat_state { modals::render_reaction_picker(f, area, available_reactions, *selected_index); } } - - diff --git a/src/ui/mod.rs b/src/ui/mod.rs index ff0b766..05e2d0f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,8 +4,8 @@ mod auth; pub mod chat_list; -mod compose_bar; pub mod components; +mod compose_bar; pub mod footer; mod loading; mod main_screen; diff --git a/src/ui/modals/account_switcher.rs b/src/ui/modals/account_switcher.rs index 106c711..76b25c3 100644 --- a/src/ui/modals/account_switcher.rs +++ b/src/ui/modals/account_switcher.rs @@ -20,18 +20,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { }; match state { - AccountSwitcherState::SelectAccount { - accounts, - selected_index, - current_account, - } => { + AccountSwitcherState::SelectAccount { accounts, selected_index, current_account } => { render_select_account(f, area, accounts, *selected_index, current_account); } - AccountSwitcherState::AddAccount { - name_input, - cursor_position, - error, - } => { + AccountSwitcherState::AddAccount { name_input, cursor_position, error } => { render_add_account(f, area, name_input, *cursor_position, error.as_deref()); } } @@ -53,10 +45,7 @@ fn render_select_account( let marker = if is_current { "● " } else { " " }; let suffix = if is_current { " (текущий)" } else { "" }; - let display = format!( - "{}{} ({}){}", - marker, account.name, account.display_name, suffix - ); + let display = format!("{}{} ({}){}", marker, account.name, account.display_name, suffix); let style = if is_selected { Style::default() @@ -86,10 +75,7 @@ fn render_select_account( } else { Style::default().fg(Color::Cyan) }; - lines.push(Line::from(Span::styled( - " + Добавить аккаунт", - add_style, - ))); + lines.push(Line::from(Span::styled(" + Добавить аккаунт", add_style))); lines.push(Line::from("")); @@ -148,10 +134,7 @@ fn render_add_account( let input_display = if name_input.is_empty() { Span::styled("_", Style::default().fg(Color::DarkGray)) } else { - Span::styled( - format!("{}_", name_input), - Style::default().fg(Color::White), - ) + Span::styled(format!("{}_", name_input), Style::default().fg(Color::White)) }; lines.push(Line::from(vec![ Span::styled(" Имя: ", Style::default().fg(Color::Cyan)), @@ -168,10 +151,7 @@ fn render_add_account( // Error if let Some(err) = error { - lines.push(Line::from(Span::styled( - format!(" {}", err), - Style::default().fg(Color::Red), - ))); + lines.push(Line::from(Span::styled(format!(" {}", err), Style::default().fg(Color::Red)))); lines.push(Line::from("")); } diff --git a/src/ui/modals/delete_confirm.rs b/src/ui/modals/delete_confirm.rs index a76cd6a..d27804c 100644 --- a/src/ui/modals/delete_confirm.rs +++ b/src/ui/modals/delete_confirm.rs @@ -1,6 +1,6 @@ //! Delete confirmation modal -use ratatui::{Frame, layout::Rect}; +use ratatui::{layout::Rect, Frame}; /// Renders delete confirmation modal pub fn render(f: &mut Frame, area: Rect) { diff --git a/src/ui/modals/image_viewer.rs b/src/ui/modals/image_viewer.rs index 9a25edc..afbd5fc 100644 --- a/src/ui/modals/image_viewer.rs +++ b/src/ui/modals/image_viewer.rs @@ -19,19 +19,12 @@ use ratatui::{ use ratatui_image::StatefulImage; /// Рендерит модальное окно с полноэкранным изображением -pub fn render( - f: &mut Frame, - app: &mut App, - modal_state: &ImageModalState, -) { +pub fn render(f: &mut Frame, app: &mut App, modal_state: &ImageModalState) { let area = f.area(); // Затемняем весь фон f.render_widget(Clear, area); - f.render_widget( - Block::default().style(Style::default().bg(Color::Black)), - area, - ); + f.render_widget(Block::default().style(Style::default().bg(Color::Black)), area); // Резервируем место для подсказок (2 строки внизу) let image_area_height = area.height.saturating_sub(2); @@ -76,7 +69,7 @@ pub fn render( // Загружаем изображение (может занять время для iTerm2/Sixel) let _ = renderer.load_image(modal_state.message_id, &modal_state.photo_path); - + // Триггерим перерисовку для показа загруженного изображения app.needs_redraw = true; } diff --git a/src/ui/modals/mod.rs b/src/ui/modals/mod.rs index 25f5337..81ae8d4 100644 --- a/src/ui/modals/mod.rs +++ b/src/ui/modals/mod.rs @@ -10,18 +10,18 @@ pub mod account_switcher; pub mod delete_confirm; +pub mod pinned; pub mod reaction_picker; pub mod search; -pub mod pinned; #[cfg(feature = "images")] pub mod image_viewer; pub use account_switcher::render as render_account_switcher; pub use delete_confirm::render as render_delete_confirm; +pub use pinned::render as render_pinned; pub use reaction_picker::render as render_reaction_picker; pub use search::render as render_search; -pub use pinned::render as render_pinned; #[cfg(feature = "images")] pub use image_viewer::render as render_image_viewer; diff --git a/src/ui/modals/pinned.rs b/src/ui/modals/pinned.rs index f446765..6caac5e 100644 --- a/src/ui/modals/pinned.rs +++ b/src/ui/modals/pinned.rs @@ -2,7 +2,7 @@ use crate::app::App; use crate::tdlib::TdClientTrait; -use crate::ui::components::{render_message_item, calculate_scroll_offset, render_help_bar}; +use crate::ui::components::{calculate_scroll_offset, render_help_bar, render_message_item}; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -14,15 +14,13 @@ use ratatui::{ /// Renders pinned messages mode pub fn render(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 (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) diff --git a/src/ui/modals/reaction_picker.rs b/src/ui/modals/reaction_picker.rs index f86b9e3..eb2782c 100644 --- a/src/ui/modals/reaction_picker.rs +++ b/src/ui/modals/reaction_picker.rs @@ -1,13 +1,8 @@ //! Reaction picker modal -use ratatui::{Frame, layout::Rect}; +use ratatui::{layout::Rect, Frame}; /// Renders emoji reaction picker modal -pub fn render( - f: &mut Frame, - area: Rect, - available_reactions: &[String], - selected_index: usize, -) { +pub fn render(f: &mut Frame, area: Rect, available_reactions: &[String], selected_index: usize) { crate::ui::components::render_emoji_picker(f, area, available_reactions, selected_index); } diff --git a/src/ui/modals/search.rs b/src/ui/modals/search.rs index b356b80..e82bc4e 100644 --- a/src/ui/modals/search.rs +++ b/src/ui/modals/search.rs @@ -2,7 +2,7 @@ use crate::app::App; use crate::tdlib::TdClientTrait; -use crate::ui::components::{render_message_item, calculate_scroll_offset, render_help_bar}; +use crate::ui::components::{calculate_scroll_offset, render_help_bar, render_message_item}; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -15,11 +15,8 @@ use ratatui::{ pub fn render(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 + if let crate::app::ChatState::SearchInChat { query, results, selected_index } = + &app.chat_state { (query.as_str(), results.as_slice(), *selected_index) } else { @@ -37,11 +34,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { // Search input let total = results.len(); - let current = if total > 0 { - selected_index + 1 - } else { - 0 - }; + let current = if total > 0 { selected_index + 1 } else { 0 }; let input_line = if query.is_empty() { Line::from(vec![ diff --git a/src/ui/profile.rs b/src/ui/profile.rs index 7c3ef59..f9ea91f 100644 --- a/src/ui/profile.rs +++ b/src/ui/profile.rs @@ -1,7 +1,7 @@ -use crate::app::App; use crate::app::methods::modal::ModalMethods; -use crate::tdlib::TdClientTrait; +use crate::app::App; use crate::tdlib::ProfileInfo; +use crate::tdlib::TdClientTrait; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 877935e..131bc8e 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -6,6 +6,6 @@ pub mod validation; pub use formatting::*; // pub use modal_handler::*; // Используется через явный import -pub use retry::{with_timeout, with_timeout_msg, with_timeout_ignore}; +pub use retry::{with_timeout, with_timeout_ignore, with_timeout_msg}; pub use tdlib::*; pub use validation::*; diff --git a/src/utils/retry.rs b/src/utils/retry.rs index 5a139be..19e77a5 100644 --- a/src/utils/retry.rs +++ b/src/utils/retry.rs @@ -105,10 +105,9 @@ mod tests { #[tokio::test] async fn test_with_timeout_success() { - let result = with_timeout(Duration::from_secs(1), async { - Ok::<_, String>("success".to_string()) - }) - .await; + let result = + with_timeout(Duration::from_secs(1), async { Ok::<_, String>("success".to_string()) }) + .await; assert!(result.is_ok()); assert_eq!(result.unwrap(), "success"); diff --git a/tests/account_switcher.rs b/tests/account_switcher.rs index 68f426f..26449db 100644 --- a/tests/account_switcher.rs +++ b/tests/account_switcher.rs @@ -17,11 +17,7 @@ fn test_open_account_switcher() { assert!(app.account_switcher.is_some()); match &app.account_switcher { - Some(AccountSwitcherState::SelectAccount { - accounts, - selected_index, - current_account, - }) => { + Some(AccountSwitcherState::SelectAccount { accounts, selected_index, current_account }) => { assert!(!accounts.is_empty()); assert_eq!(*selected_index, 0); assert_eq!(current_account, "default"); @@ -58,11 +54,7 @@ fn test_account_switcher_navigate_down() { } match &app.account_switcher { - Some(AccountSwitcherState::SelectAccount { - selected_index, - accounts, - .. - }) => { + Some(AccountSwitcherState::SelectAccount { selected_index, accounts, .. }) => { // Should be at the "Add account" item (index == accounts.len()) assert_eq!(*selected_index, accounts.len()); } @@ -137,11 +129,7 @@ fn test_confirm_add_account_transitions_to_add_state() { app.account_switcher_confirm(); match &app.account_switcher { - Some(AccountSwitcherState::AddAccount { - name_input, - cursor_position, - error, - }) => { + Some(AccountSwitcherState::AddAccount { name_input, cursor_position, error }) => { assert!(name_input.is_empty()); assert_eq!(*cursor_position, 0); assert!(error.is_none()); diff --git a/tests/accounts.rs b/tests/accounts.rs index 6eea1f7..cf43876 100644 --- a/tests/accounts.rs +++ b/tests/accounts.rs @@ -1,8 +1,6 @@ // Integration tests for accounts module -use tele_tui::accounts::{ - account_db_path, validate_account_name, AccountProfile, AccountsConfig, -}; +use tele_tui::accounts::{account_db_path, validate_account_name, AccountProfile, AccountsConfig}; #[test] fn test_default_single_config() { diff --git a/tests/chat_list.rs b/tests/chat_list.rs index ff5c158..9695123 100644 --- a/tests/chat_list.rs +++ b/tests/chat_list.rs @@ -65,16 +65,14 @@ fn test_incoming_message_shows_unread_badge() { .last_message("Как дела?") .build(); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); // Рендерим UI - должно быть без "(1)" let buffer_before = render_to_buffer(80, 24, |f| { tele_tui::ui::chat_list::render(f, f.area(), &mut app); }); let output_before = buffer_to_string(&buffer_before); - + // Проверяем что нет "(1)" в первой строке чата assert!(!output_before.contains("(1)"), "Before: should not contain (1)"); @@ -87,9 +85,13 @@ fn test_incoming_message_shows_unread_badge() { tele_tui::ui::chat_list::render(f, f.area(), &mut app); }); let output_after = buffer_to_string(&buffer_after); - + // Проверяем что появилось "(1)" в первой строке чата - assert!(output_after.contains("(1)"), "After: should contain (1)\nActual output:\n{}", output_after); + assert!( + output_after.contains("(1)"), + "After: should contain (1)\nActual output:\n{}", + output_after + ); } #[tokio::test] @@ -127,39 +129,44 @@ async fn test_opening_chat_clears_unread_badge() { tele_tui::ui::chat_list::render(f, f.area(), &mut app); }); let output_before = buffer_to_string(&buffer_before); - + // Проверяем что есть "(3)" в списке чатов - assert!(output_before.contains("(3)"), "Before opening: should contain (3)\nActual output:\n{}", output_before); + assert!( + output_before.contains("(3)"), + "Before opening: should contain (3)\nActual output:\n{}", + output_before + ); // Симулируем открытие чата - загружаем историю let chat_id = ChatId::new(999); let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap(); - + // Собираем ID входящих сообщений (как в реальном коде) let incoming_message_ids: Vec = loaded_messages .iter() .filter(|msg| !msg.is_outgoing()) .map(|msg| msg.id()) .collect(); - + // Проверяем что нашли 3 входящих сообщения assert_eq!(incoming_message_ids.len(), 3, "Should have 3 incoming messages"); // Добавляем в очередь для отметки как прочитанные (напрямую через Mutex) - app.td_client.pending_view_messages + app.td_client + .pending_view_messages .lock() .unwrap() .push((chat_id, incoming_message_ids)); - + // Обрабатываем очередь (как в main loop) app.td_client.process_pending_view_messages().await; - + // В FakeTdClient это должно записаться в viewed_messages let viewed = app.td_client.get_viewed_messages(); assert_eq!(viewed.len(), 1, "Should have one batch of viewed messages"); assert_eq!(viewed[0].0, 999, "Should be for chat 999"); assert_eq!(viewed[0].1.len(), 3, "Should have viewed 3 messages"); - + // В реальном приложении TDLib отправит Update::ChatReadInbox // который обновит unread_count в чате. Симулируем это: app.chats[0].unread_count = 0; @@ -169,9 +176,13 @@ async fn test_opening_chat_clears_unread_badge() { tele_tui::ui::chat_list::render(f, f.area(), &mut app); }); let output_after = buffer_to_string(&buffer_after); - + // Проверяем что "(3)" больше нет - assert!(!output_after.contains("(3)"), "After opening: should not contain (3)\nActual output:\n{}", output_after); + assert!( + !output_after.contains("(3)"), + "After opening: should not contain (3)\nActual output:\n{}", + output_after + ); } #[tokio::test] @@ -202,7 +213,7 @@ async fn test_opening_chat_loads_many_messages() { // Открываем чат - загружаем историю (запрашиваем 100 сообщений) let chat_id = ChatId::new(888); let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap(); - + // Проверяем что загрузились ВСЕ 50 сообщений, а не только последние 2-3 assert_eq!( loaded_messages.len(), @@ -244,7 +255,7 @@ async fn test_chat_history_chunked_loading() { // Тест 1: Загружаем 100 сообщений (больше чем 50, меньше чем 120) let chat_id = ChatId::new(999); let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap(); - + assert_eq!( loaded_messages.len(), 100, @@ -254,13 +265,13 @@ async fn test_chat_history_chunked_loading() { // Проверяем что сообщения в правильном порядке (от старых к новым) assert_eq!(loaded_messages[0].text(), "Message 1"); - assert_eq!(loaded_messages[49].text(), "Message 50"); // Граница первого чанка - assert_eq!(loaded_messages[50].text(), "Message 51"); // Начало второго чанка + assert_eq!(loaded_messages[49].text(), "Message 50"); // Граница первого чанка + assert_eq!(loaded_messages[50].text(), "Message 51"); // Начало второго чанка assert_eq!(loaded_messages[99].text(), "Message 100"); // Тест 2: Загружаем все 120 сообщений let all_messages = app.td_client.get_chat_history(chat_id, 120).await.unwrap(); - + assert_eq!( all_messages.len(), 120, @@ -273,7 +284,7 @@ async fn test_chat_history_chunked_loading() { // Тест 3: Запрашиваем 200 сообщений, но есть только 120 let limited_messages = app.td_client.get_chat_history(chat_id, 200).await.unwrap(); - + assert_eq!( limited_messages.len(), 120, @@ -307,8 +318,12 @@ async fn test_chat_history_loads_all_without_limit() { // Загружаем без лимита (i32::MAX) let chat_id = ChatId::new(1001); - let all = app.td_client.get_chat_history(chat_id, i32::MAX).await.unwrap(); - + let all = app + .td_client + .get_chat_history(chat_id, i32::MAX) + .await + .unwrap(); + assert_eq!(all.len(), 200, "Should load all 200 messages without limit"); assert_eq!(all[0].text(), "Msg 1", "First message should be oldest"); assert_eq!(all[199].text(), "Msg 200", "Last message should be newest"); @@ -338,25 +353,29 @@ async fn test_load_older_messages_pagination() { .build(); let chat_id = ChatId::new(1002); - + // Шаг 1: Загружаем только последние 30 сообщений // get_chat_history загружает от конца, поэтому получим сообщения 1-30 let initial_batch = app.td_client.get_chat_history(chat_id, 30).await.unwrap(); assert_eq!(initial_batch.len(), 30, "Should load 30 messages initially"); assert_eq!(initial_batch[0].text(), "Msg 1", "First message should be Msg 1"); assert_eq!(initial_batch[29].text(), "Msg 30", "Last should be Msg 30"); - + // Шаг 2: Загружаем все 150 сообщений для проверки load_older let all_messages = app.td_client.get_chat_history(chat_id, 150).await.unwrap(); assert_eq!(all_messages.len(), 150); - + // Имитируем ситуацию: у нас есть сообщения 101-150, хотим загрузить 51-100 // Берем ID сообщения 101 (первое в нашем "окне") let msg_101_id = all_messages[100].id(); // index 100 = Msg 101 - + // Загружаем сообщения старше 101 - let older_batch = app.td_client.load_older_messages(chat_id, msg_101_id).await.unwrap(); - + let older_batch = app + .td_client + .load_older_messages(chat_id, msg_101_id) + .await + .unwrap(); + // Должны получить сообщения 1-100 (все что старше 101) assert_eq!(older_batch.len(), 100, "Should load 100 older messages"); assert_eq!(older_batch[0].text(), "Msg 1", "Oldest should be Msg 1"); @@ -473,7 +492,7 @@ fn snapshot_chat_search_mode() { fn snapshot_chat_with_online_status() { use tele_tui::tdlib::UserOnlineStatus; use tele_tui::types::ChatId; - + let chat = TestChatBuilder::new("Alice", 123) .last_message("Hey there!") .build(); @@ -493,4 +512,3 @@ fn snapshot_chat_with_online_status() { let output = buffer_to_string(&buffer); assert_snapshot!("chat_with_online_status", output); } - diff --git a/tests/config.rs b/tests/config.rs index 631dcd7..7039c4e 100644 --- a/tests/config.rs +++ b/tests/config.rs @@ -1,6 +1,9 @@ // Integration tests for config flow -use tele_tui::config::{AudioConfig, Config, ColorsConfig, GeneralConfig, ImagesConfig, Keybindings, NotificationsConfig}; +use tele_tui::config::{ + AudioConfig, ColorsConfig, Config, GeneralConfig, ImagesConfig, Keybindings, + NotificationsConfig, +}; /// Test: Дефолтные значения конфигурации #[test] @@ -22,9 +25,7 @@ fn test_config_default_values() { #[test] fn test_config_custom_values() { let config = Config { - general: GeneralConfig { - timezone: "+05:00".to_string(), - }, + general: GeneralConfig { timezone: "+05:00".to_string() }, colors: ColorsConfig { incoming_message: "cyan".to_string(), outgoing_message: "blue".to_string(), @@ -108,9 +109,7 @@ fn test_parse_color_case_insensitive() { #[test] fn test_config_toml_serialization() { let original_config = Config { - general: GeneralConfig { - timezone: "-05:00".to_string(), - }, + general: GeneralConfig { timezone: "-05:00".to_string() }, colors: ColorsConfig { incoming_message: "cyan".to_string(), outgoing_message: "blue".to_string(), @@ -164,25 +163,19 @@ mod timezone_tests { #[test] fn test_timezone_formats() { let positive = Config { - general: GeneralConfig { - timezone: "+03:00".to_string(), - }, + general: GeneralConfig { timezone: "+03:00".to_string() }, ..Default::default() }; assert_eq!(positive.general.timezone, "+03:00"); let negative = Config { - general: GeneralConfig { - timezone: "-05:00".to_string(), - }, + general: GeneralConfig { timezone: "-05:00".to_string() }, ..Default::default() }; assert_eq!(negative.general.timezone, "-05:00"); let zero = Config { - general: GeneralConfig { - timezone: "+00:00".to_string(), - }, + general: GeneralConfig { timezone: "+00:00".to_string() }, ..Default::default() }; assert_eq!(zero.general.timezone, "+00:00"); diff --git a/tests/delete_message.rs b/tests/delete_message.rs index 1ee2649..49cefbf 100644 --- a/tests/delete_message.rs +++ b/tests/delete_message.rs @@ -12,13 +12,19 @@ async fn test_delete_message_removes_from_list() { let client = FakeTdClient::new(); // Отправляем сообщение - let msg = client.send_message(ChatId::new(123), "Delete me".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Delete me".to_string(), None, None) + .await + .unwrap(); // Проверяем что сообщение есть assert_eq!(client.get_messages(123).len(), 1); // Удаляем сообщение - client.delete_messages(ChatId::new(123), vec![msg.id()], false).await.unwrap(); + client + .delete_messages(ChatId::new(123), vec![msg.id()], false) + .await + .unwrap(); // Проверяем что удаление записалось assert_eq!(client.get_deleted_messages().len(), 1); @@ -34,15 +40,30 @@ async fn test_delete_multiple_messages() { let client = FakeTdClient::new(); // Отправляем 3 сообщения - let msg1 = client.send_message(ChatId::new(123), "Message 1".to_string(), None, None).await.unwrap(); - let msg2 = client.send_message(ChatId::new(123), "Message 2".to_string(), None, None).await.unwrap(); - let msg3 = client.send_message(ChatId::new(123), "Message 3".to_string(), None, None).await.unwrap(); + let msg1 = client + .send_message(ChatId::new(123), "Message 1".to_string(), None, None) + .await + .unwrap(); + let msg2 = client + .send_message(ChatId::new(123), "Message 2".to_string(), None, None) + .await + .unwrap(); + let msg3 = client + .send_message(ChatId::new(123), "Message 3".to_string(), None, None) + .await + .unwrap(); assert_eq!(client.get_messages(123).len(), 3); // Удаляем первое и третье - client.delete_messages(ChatId::new(123), vec![msg1.id()], false).await.unwrap(); - client.delete_messages(ChatId::new(123), vec![msg3.id()], false).await.unwrap(); + client + .delete_messages(ChatId::new(123), vec![msg1.id()], false) + .await + .unwrap(); + client + .delete_messages(ChatId::new(123), vec![msg3.id()], false) + .await + .unwrap(); // Проверяем историю удалений assert_eq!(client.get_deleted_messages().len(), 2); @@ -89,12 +110,18 @@ async fn test_delete_nonexistent_message() { let client = FakeTdClient::new(); // Отправляем одно сообщение - let msg = client.send_message(ChatId::new(123), "Exists".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Exists".to_string(), None, None) + .await + .unwrap(); assert_eq!(client.get_messages(123).len(), 1); // Пытаемся удалить несуществующее - client.delete_messages(ChatId::new(123), vec![MessageId::new(999)], false).await.unwrap(); + client + .delete_messages(ChatId::new(123), vec![MessageId::new(999)], false) + .await + .unwrap(); // Удаление записалось в историю assert_eq!(client.get_deleted_messages().len(), 1); @@ -112,7 +139,10 @@ async fn test_delete_nonexistent_message() { async fn test_delete_with_confirmation_flow() { let client = FakeTdClient::new(); - let msg = client.send_message(ChatId::new(123), "To delete".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "To delete".to_string(), None, None) + .await + .unwrap(); // Шаг 1: Пользователь нажал 'd' -> показывается модалка (в App) // В FakeTdClient просто проверяем что сообщение ещё есть @@ -120,7 +150,10 @@ async fn test_delete_with_confirmation_flow() { assert_eq!(client.get_deleted_messages().len(), 0); // Шаг 2: Пользователь подтвердил 'y' -> удаляем - client.delete_messages(ChatId::new(123), vec![msg.id()], false).await.unwrap(); + client + .delete_messages(ChatId::new(123), vec![msg.id()], false) + .await + .unwrap(); // Проверяем что удалено assert_eq!(client.get_messages(123).len(), 0); @@ -132,7 +165,10 @@ async fn test_delete_with_confirmation_flow() { async fn test_cancel_delete_keeps_message() { let client = FakeTdClient::new(); - let msg = client.send_message(ChatId::new(123), "Keep me".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Keep me".to_string(), None, None) + .await + .unwrap(); // Шаг 1: Пользователь нажал 'd' -> показалась модалка assert_eq!(client.get_messages(123).len(), 1); diff --git a/tests/drafts.rs b/tests/drafts.rs index 8ab8c64..69f0c27 100644 --- a/tests/drafts.rs +++ b/tests/drafts.rs @@ -3,8 +3,8 @@ mod helpers; use helpers::test_data::{create_test_chat, TestChatBuilder}; -use tele_tui::types::{ChatId, MessageId}; use std::collections::HashMap; +use tele_tui::types::{ChatId, MessageId}; /// Простая структура для хранения черновиков (как в реальном App) struct DraftManager { diff --git a/tests/e2e_user_journey.rs b/tests/e2e_user_journey.rs index fd7b582..e086223 100644 --- a/tests/e2e_user_journey.rs +++ b/tests/e2e_user_journey.rs @@ -23,10 +23,7 @@ async fn test_user_journey_app_launch_to_chat_list() { let chat2 = TestChatBuilder::new("Work Group", 102).build(); let chat3 = TestChatBuilder::new("Boss", 103).build(); - let client = client - .with_chat(chat1) - .with_chat(chat2) - .with_chat(chat3); + let client = client.with_chat(chat1).with_chat(chat2).with_chat(chat3); // 4. Симулируем загрузку чатов через load_chats let loaded_chats = client.load_chats(50).await.unwrap(); @@ -58,9 +55,7 @@ async fn test_user_journey_open_chat_send_message() { .outgoing() .build(); - let client = client - .with_message(123, msg1) - .with_message(123, msg2); + let client = client.with_message(123, msg1).with_message(123, msg2); // 3. Открываем чат client.open_chat(ChatId::new(123)).await.unwrap(); @@ -77,12 +72,10 @@ async fn test_user_journey_open_chat_send_message() { assert_eq!(history[1].text(), "I'm good, thanks!"); // 7. Отправляем новое сообщение - let _new_msg = client.send_message( - ChatId::new(123), - "What's for dinner?".to_string(), - None, - None - ).await.unwrap(); + let _new_msg = client + .send_message(ChatId::new(123), "What's for dinner?".to_string(), None, None) + .await + .unwrap(); // 8. Проверяем что сообщение отправлено assert_eq!(client.get_sent_messages().len(), 1); @@ -153,34 +146,43 @@ async fn test_user_journey_multi_step_conversation() { client.set_update_channel(tx); // 4. Входящее сообщение от Alice - client.simulate_incoming_message(ChatId::new(789), "How's the project going?".to_string(), "Alice"); + client.simulate_incoming_message( + ChatId::new(789), + "How's the project going?".to_string(), + "Alice", + ); // Проверяем update let update = rx.try_recv().ok(); assert!(matches!(update, Some(TdUpdate::NewMessage { .. }))); // 5. Отвечаем - client.send_message( - ChatId::new(789), - "Almost done! Just need to finish tests.".to_string(), - None, - None - ).await.unwrap(); + client + .send_message( + ChatId::new(789), + "Almost done! Just need to finish tests.".to_string(), + None, + None, + ) + .await + .unwrap(); // 6. Проверяем историю после первого обмена let history1 = client.get_chat_history(ChatId::new(789), 50).await.unwrap(); assert_eq!(history1.len(), 2); // 7. Еще одно входящее сообщение - client.simulate_incoming_message(ChatId::new(789), "Great! Let me know if you need help.".to_string(), "Alice"); + client.simulate_incoming_message( + ChatId::new(789), + "Great! Let me know if you need help.".to_string(), + "Alice", + ); // 8. Снова отвечаем - client.send_message( - ChatId::new(789), - "Will do, thanks!".to_string(), - None, - None - ).await.unwrap(); + client + .send_message(ChatId::new(789), "Will do, thanks!".to_string(), None, None) + .await + .unwrap(); // 9. Финальная проверка истории let final_history = client.get_chat_history(ChatId::new(789), 50).await.unwrap(); @@ -219,24 +221,20 @@ async fn test_user_journey_switch_chats() { assert_eq!(client.get_current_chat_id(), Some(111)); // 3. Отправляем сообщение в первом чате - client.send_message( - ChatId::new(111), - "Message in chat 1".to_string(), - None, - None - ).await.unwrap(); + client + .send_message(ChatId::new(111), "Message in chat 1".to_string(), None, None) + .await + .unwrap(); // 4. Переключаемся на второй чат client.open_chat(ChatId::new(222)).await.unwrap(); assert_eq!(client.get_current_chat_id(), Some(222)); // 5. Отправляем сообщение во втором чате - client.send_message( - ChatId::new(222), - "Message in chat 2".to_string(), - None, - None - ).await.unwrap(); + client + .send_message(ChatId::new(222), "Message in chat 2".to_string(), None, None) + .await + .unwrap(); // 6. Переключаемся на третий чат client.open_chat(ChatId::new(333)).await.unwrap(); @@ -270,12 +268,10 @@ async fn test_user_journey_edit_during_conversation() { client.open_chat(ChatId::new(555)).await.unwrap(); // 2. Отправляем сообщение с опечаткой - let msg = client.send_message( - ChatId::new(555), - "I'll be there at 5pm tomorow".to_string(), - None, - None - ).await.unwrap(); + let msg = client + .send_message(ChatId::new(555), "I'll be there at 5pm tomorow".to_string(), None, None) + .await + .unwrap(); // 3. Проверяем что сообщение отправлено let history = client.get_chat_history(ChatId::new(555), 50).await.unwrap(); @@ -283,17 +279,19 @@ async fn test_user_journey_edit_during_conversation() { assert_eq!(history[0].text(), "I'll be there at 5pm tomorow"); // 4. Исправляем опечатку - client.edit_message( - ChatId::new(555), - msg.id(), - "I'll be there at 5pm tomorrow".to_string() - ).await.unwrap(); + client + .edit_message(ChatId::new(555), msg.id(), "I'll be there at 5pm tomorrow".to_string()) + .await + .unwrap(); // 5. Проверяем что сообщение отредактировано let edited_history = client.get_chat_history(ChatId::new(555), 50).await.unwrap(); assert_eq!(edited_history.len(), 1); assert_eq!(edited_history[0].text(), "I'll be there at 5pm tomorrow"); - assert!(edited_history[0].metadata.edit_date > 0, "Должна быть установлена дата редактирования"); + assert!( + edited_history[0].metadata.edit_date > 0, + "Должна быть установлена дата редактирования" + ); // 6. Проверяем историю редактирований assert_eq!(client.get_edited_messages().len(), 1); @@ -315,7 +313,11 @@ async fn test_user_journey_reply_in_conversation() { client.set_update_channel(tx); // 3. Входящее сообщение с вопросом - client.simulate_incoming_message(ChatId::new(666), "Can you send me the report?".to_string(), "Charlie"); + client.simulate_incoming_message( + ChatId::new(666), + "Can you send me the report?".to_string(), + "Charlie", + ); let update = rx.try_recv().ok(); assert!(matches!(update, Some(TdUpdate::NewMessage { .. }))); @@ -324,12 +326,10 @@ async fn test_user_journey_reply_in_conversation() { let question_msg_id = history[0].id(); // 4. Отправляем другое сообщение (не связанное) - client.send_message( - ChatId::new(666), - "Working on it now".to_string(), - None, - None - ).await.unwrap(); + client + .send_message(ChatId::new(666), "Working on it now".to_string(), None, None) + .await + .unwrap(); // 5. Отвечаем на конкретный вопрос (reply) let reply_info = Some(tele_tui::tdlib::ReplyInfo { @@ -338,12 +338,15 @@ async fn test_user_journey_reply_in_conversation() { text: "Can you send me the report?".to_string(), }); - client.send_message( - ChatId::new(666), - "Sure, sending now!".to_string(), - Some(question_msg_id), - reply_info - ).await.unwrap(); + client + .send_message( + ChatId::new(666), + "Sure, sending now!".to_string(), + Some(question_msg_id), + reply_info, + ) + .await + .unwrap(); // 6. Проверяем что reply сохранён let final_history = client.get_chat_history(ChatId::new(666), 50).await.unwrap(); @@ -376,12 +379,10 @@ async fn test_user_journey_network_state_changes() { // 4. Открываем чат и отправляем сообщение client.open_chat(ChatId::new(888)).await.unwrap(); - client.send_message( - ChatId::new(888), - "Test message".to_string(), - None, - None - ).await.unwrap(); + client + .send_message(ChatId::new(888), "Test message".to_string(), None, None) + .await + .unwrap(); // Очищаем канал от update NewMessage let _ = rx.try_recv(); @@ -391,8 +392,14 @@ async fn test_user_journey_network_state_changes() { // Проверяем update let update = rx.try_recv().ok(); - assert!(matches!(update, Some(TdUpdate::ConnectionState { state: NetworkState::WaitingForNetwork })), - "Expected ConnectionState update, got: {:?}", update); + assert!( + matches!( + update, + Some(TdUpdate::ConnectionState { state: NetworkState::WaitingForNetwork }) + ), + "Expected ConnectionState update, got: {:?}", + update + ); // 6. Проверяем что состояние изменилось assert_eq!(client.get_network_state(), NetworkState::WaitingForNetwork); @@ -405,12 +412,10 @@ async fn test_user_journey_network_state_changes() { assert_eq!(client.get_network_state(), NetworkState::Ready); // 8. Отправляем сообщение после восстановления - client.send_message( - ChatId::new(888), - "Connection restored!".to_string(), - None, - None - ).await.unwrap(); + client + .send_message(ChatId::new(888), "Connection restored!".to_string(), None, None) + .await + .unwrap(); // 9. Проверяем что оба сообщения в истории let history = client.get_chat_history(ChatId::new(888), 50).await.unwrap(); diff --git a/tests/edit_message.rs b/tests/edit_message.rs index 66881b8..ecb77af 100644 --- a/tests/edit_message.rs +++ b/tests/edit_message.rs @@ -12,10 +12,16 @@ async fn test_edit_message_changes_text() { let client = FakeTdClient::new(); // Отправляем сообщение - let msg = client.send_message(ChatId::new(123), "Original text".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Original text".to_string(), None, None) + .await + .unwrap(); // Редактируем сообщение - client.edit_message(ChatId::new(123), msg.id(), "Edited text".to_string()).await.unwrap(); + client + .edit_message(ChatId::new(123), msg.id(), "Edited text".to_string()) + .await + .unwrap(); // Проверяем что редактирование записалось assert_eq!(client.get_edited_messages().len(), 1); @@ -34,7 +40,10 @@ async fn test_edit_message_sets_edit_date() { let client = FakeTdClient::new(); // Отправляем сообщение - let msg = client.send_message(ChatId::new(123), "Original".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Original".to_string(), None, None) + .await + .unwrap(); // Получаем дату до редактирования let messages_before = client.get_messages(123); @@ -42,7 +51,10 @@ async fn test_edit_message_sets_edit_date() { assert_eq!(messages_before[0].metadata.edit_date, 0); // Не редактировалось // Редактируем сообщение - client.edit_message(ChatId::new(123), msg.id(), "Edited".to_string()).await.unwrap(); + client + .edit_message(ChatId::new(123), msg.id(), "Edited".to_string()) + .await + .unwrap(); // Проверяем что edit_date установлена let messages_after = client.get_messages(123); @@ -78,16 +90,28 @@ async fn test_can_only_edit_own_messages() { async fn test_multiple_edits_of_same_message() { let client = FakeTdClient::new(); - let msg = client.send_message(ChatId::new(123), "Version 1".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Version 1".to_string(), None, None) + .await + .unwrap(); // Первое редактирование - client.edit_message(ChatId::new(123), msg.id(), "Version 2".to_string()).await.unwrap(); + client + .edit_message(ChatId::new(123), msg.id(), "Version 2".to_string()) + .await + .unwrap(); // Второе редактирование - client.edit_message(ChatId::new(123), msg.id(), "Version 3".to_string()).await.unwrap(); + client + .edit_message(ChatId::new(123), msg.id(), "Version 3".to_string()) + .await + .unwrap(); // Третье редактирование - client.edit_message(ChatId::new(123), msg.id(), "Final version".to_string()).await.unwrap(); + client + .edit_message(ChatId::new(123), msg.id(), "Final version".to_string()) + .await + .unwrap(); // Проверяем что все 3 редактирования записаны assert_eq!(client.get_edited_messages().len(), 3); @@ -107,7 +131,9 @@ async fn test_edit_nonexistent_message() { let client = FakeTdClient::new(); // Пытаемся отредактировать несуществующее сообщение - let result = client.edit_message(ChatId::new(123), MessageId::new(999), "New text".to_string()).await; + let result = client + .edit_message(ChatId::new(123), MessageId::new(999), "New text".to_string()) + .await; // Должна вернуться ошибка assert!(result.is_err()); @@ -124,7 +150,10 @@ async fn test_edit_nonexistent_message() { async fn test_edit_history_tracking() { let client = FakeTdClient::new(); - let msg = client.send_message(ChatId::new(123), "Original".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Original".to_string(), None, None) + .await + .unwrap(); // Симулируем начало редактирования -> изменение -> отмена // Отменять на уровне FakeTdClient нельзя, но можно проверить что original сохранён @@ -134,14 +163,20 @@ async fn test_edit_history_tracking() { let original = messages_before[0].text().to_string(); // Редактируем - client.edit_message(ChatId::new(123), msg.id(), "Edited".to_string()).await.unwrap(); + client + .edit_message(ChatId::new(123), msg.id(), "Edited".to_string()) + .await + .unwrap(); // Проверяем что изменилось let messages_edited = client.get_messages(123); assert_eq!(messages_edited[0].text(), "Edited"); // Можем "отменить" редактирование вернув original - client.edit_message(ChatId::new(123), msg.id(), original).await.unwrap(); + client + .edit_message(ChatId::new(123), msg.id(), original) + .await + .unwrap(); // Проверяем что вернулось let messages_restored = client.get_messages(123); diff --git a/tests/helpers/app_builder.rs b/tests/helpers/app_builder.rs index e89f286..71e2cb3 100644 --- a/tests/helpers/app_builder.rs +++ b/tests/helpers/app_builder.rs @@ -1,8 +1,8 @@ // Test App builder +use super::FakeTdClient; use ratatui::widgets::ListState; use std::collections::HashMap; -use super::FakeTdClient; use tele_tui::app::{App, AppScreen, ChatState, InputMode}; use tele_tui::config::Config; use tele_tui::tdlib::AuthState; @@ -135,7 +135,8 @@ impl TestAppBuilder { /// Подтверждение удаления pub fn delete_confirmation(mut self, message_id: i64) -> Self { - self.chat_state = Some(ChatState::DeleteConfirmation { message_id: MessageId::new(message_id) }); + self.chat_state = + Some(ChatState::DeleteConfirmation { message_id: MessageId::new(message_id) }); self } @@ -181,9 +182,7 @@ impl TestAppBuilder { /// Режим пересылки сообщения pub fn forward_mode(mut self, message_id: i64) -> Self { - self.chat_state = Some(ChatState::Forward { - message_id: MessageId::new(message_id), - }); + self.chat_state = Some(ChatState::Forward { message_id: MessageId::new(message_id) }); self } @@ -224,17 +223,17 @@ impl TestAppBuilder { pub fn build(self) -> App { // Создаём FakeTdClient с чатами и сообщениями let mut fake_client = FakeTdClient::new(); - + // Добавляем чаты for chat in &self.chats { fake_client = fake_client.with_chat(chat.clone()); } - + // Добавляем сообщения for (chat_id, messages) in self.messages { fake_client = fake_client.with_messages(chat_id, messages); } - + // Устанавливаем текущий чат если нужно if let Some(chat_id) = self.selected_chat_id { *fake_client.current_chat_id.lock().unwrap() = Some(chat_id); @@ -244,7 +243,7 @@ impl TestAppBuilder { if let Some(auth_state) = self.auth_state { fake_client = fake_client.with_auth_state(auth_state); } - + // Создаём App с FakeTdClient let mut app = App::with_client(self.config, fake_client); @@ -254,7 +253,7 @@ impl TestAppBuilder { app.message_input = self.message_input; app.is_searching = self.is_searching; app.search_query = self.search_query; - + // Применяем chat_state если он установлен if let Some(chat_state) = self.chat_state { app.chat_state = chat_state; diff --git a/tests/helpers/fake_tdclient.rs b/tests/helpers/fake_tdclient.rs index 26244c7..3015a46 100644 --- a/tests/helpers/fake_tdclient.rs +++ b/tests/helpers/fake_tdclient.rs @@ -2,22 +2,48 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex}; -use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo}; use tele_tui::tdlib::types::{FolderInfo, ReactionInfo}; +use tele_tui::tdlib::{AuthState, ChatInfo, MessageInfo, NetworkState, ProfileInfo, ReplyInfo}; use tele_tui::types::{ChatId, MessageId, UserId}; use tokio::sync::mpsc; /// Update события от TDLib (упрощённая версия) #[derive(Debug, Clone)] pub enum TdUpdate { - NewMessage { chat_id: ChatId, message: MessageInfo }, - MessageContent { chat_id: ChatId, message_id: MessageId, new_text: String }, - DeleteMessages { chat_id: ChatId, message_ids: Vec }, - ChatAction { chat_id: ChatId, user_id: UserId, action: String }, - MessageInteractionInfo { chat_id: ChatId, message_id: MessageId, reactions: Vec }, - ConnectionState { state: NetworkState }, - ChatReadOutbox { chat_id: ChatId, last_read_outbox_message_id: MessageId }, - ChatDraftMessage { chat_id: ChatId, draft_text: Option }, + NewMessage { + chat_id: ChatId, + message: MessageInfo, + }, + MessageContent { + chat_id: ChatId, + message_id: MessageId, + new_text: String, + }, + DeleteMessages { + chat_id: ChatId, + message_ids: Vec, + }, + ChatAction { + chat_id: ChatId, + user_id: UserId, + action: String, + }, + MessageInteractionInfo { + chat_id: ChatId, + message_id: MessageId, + reactions: Vec, + }, + ConnectionState { + state: NetworkState, + }, + ChatReadOutbox { + chat_id: ChatId, + last_read_outbox_message_id: MessageId, + }, + ChatDraftMessage { + chat_id: ChatId, + draft_text: Option, + }, } /// Упрощённый mock TDLib клиента для тестов @@ -30,14 +56,14 @@ pub struct FakeTdClient { pub profiles: Arc>>, pub drafts: Arc>>, pub available_reactions: Arc>>, - + // Состояние pub network_state: Arc>, pub typing_chat_id: Arc>>, pub current_chat_id: Arc>>, pub current_pinned_message: Arc>>, pub auth_state: Arc>, - + // История действий (для проверки в тестах) pub sent_messages: Arc>>, pub edited_messages: Arc>>, @@ -45,12 +71,12 @@ pub struct FakeTdClient { pub forwarded_messages: Arc>>, pub searched_queries: Arc>>, pub viewed_messages: Arc)>>>, // (chat_id, message_ids) - pub chat_actions: Arc>>, // (chat_id, action) + pub chat_actions: Arc>>, // (chat_id, action) pub pending_view_messages: Arc)>>>, // Очередь для отметки как прочитанные - + // Update channel для симуляции событий pub update_tx: Arc>>>, - + // Скачанные файлы (file_id -> local_path) pub downloaded_files: Arc>>, @@ -142,8 +168,14 @@ impl FakeTdClient { profiles: Arc::new(Mutex::new(HashMap::new())), drafts: Arc::new(Mutex::new(HashMap::new())), available_reactions: Arc::new(Mutex::new(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(), + "😢".to_string(), + "🙏".to_string(), + "👏".to_string(), + "🔥".to_string(), ])), network_state: Arc::new(Mutex::new(NetworkState::Ready)), typing_chat_id: Arc::new(Mutex::new(None)), @@ -164,14 +196,14 @@ impl FakeTdClient { fail_next_operation: Arc::new(Mutex::new(false)), } } - + /// Создать update channel для получения событий pub fn with_update_channel(self) -> (Self, mpsc::UnboundedReceiver) { let (tx, rx) = mpsc::unbounded_channel(); *self.update_tx.lock().unwrap() = Some(tx); (self, rx) } - + /// Включить симуляцию задержек (как в реальном TDLib) pub fn with_delays(mut self) -> Self { self.simulate_delays = true; @@ -179,7 +211,7 @@ impl FakeTdClient { } // ==================== Builder Methods ==================== - + /// Добавить чат pub fn with_chat(self, chat: ChatInfo) -> Self { self.chats.lock().unwrap().push(chat); @@ -205,16 +237,16 @@ impl FakeTdClient { /// Добавить несколько сообщений в чат pub fn with_messages(self, chat_id: i64, messages: Vec) -> Self { - self.messages - .lock() - .unwrap() - .insert(chat_id, messages); + self.messages.lock().unwrap().insert(chat_id, messages); self } /// Добавить папку pub fn with_folder(self, id: i32, name: &str) -> Self { - self.folders.lock().unwrap().push(FolderInfo { id, name: name.to_string() }); + self.folders + .lock() + .unwrap() + .push(FolderInfo { id, name: name.to_string() }); self } @@ -241,10 +273,13 @@ impl FakeTdClient { *self.auth_state.lock().unwrap() = state; self } - + /// Добавить скачанный файл (для mock download_file) pub fn with_downloaded_file(self, file_id: i32, path: &str) -> Self { - self.downloaded_files.lock().unwrap().insert(file_id, path.to_string()); + self.downloaded_files + .lock() + .unwrap() + .insert(file_id, path.to_string()); self } @@ -255,60 +290,76 @@ impl FakeTdClient { } // ==================== Async TDLib Operations ==================== - + /// Загрузить список чатов pub async fn load_chats(&self, limit: usize) -> Result, String> { if self.should_fail() { return Err("Failed to load chats".to_string()); } - + if self.simulate_delays { tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } - - let chats = self.chats.lock().unwrap().iter().take(limit).cloned().collect(); + + let chats = self + .chats + .lock() + .unwrap() + .iter() + .take(limit) + .cloned() + .collect(); Ok(chats) } - + /// Открыть чат pub async fn open_chat(&self, chat_id: ChatId) -> Result<(), String> { if self.should_fail() { return Err("Failed to open chat".to_string()); } - + *self.current_chat_id.lock().unwrap() = Some(chat_id.as_i64()); Ok(()) } - + /// Получить историю чата - pub async fn get_chat_history(&self, chat_id: ChatId, limit: i32) -> Result, String> { + pub async fn get_chat_history( + &self, + chat_id: ChatId, + limit: i32, + ) -> Result, String> { if self.should_fail() { return Err("Failed to load history".to_string()); } - + if self.simulate_delays { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } - - let messages = self.messages + + let messages = self + .messages .lock() .unwrap() .get(&chat_id.as_i64()) .map(|msgs| msgs.iter().take(limit as usize).cloned().collect()) .unwrap_or_default(); - + Ok(messages) } - + /// Загрузить старые сообщения - pub async fn load_older_messages(&self, chat_id: ChatId, from_message_id: MessageId) -> Result, String> { + pub async fn load_older_messages( + &self, + chat_id: ChatId, + from_message_id: MessageId, + ) -> Result, String> { if self.should_fail() { return Err("Failed to load older messages".to_string()); } - + let messages = self.messages.lock().unwrap(); let chat_messages = messages.get(&chat_id.as_i64()).ok_or("Chat not found")?; - + // Найти индекс сообщения и вернуть предыдущие if let Some(idx) = chat_messages.iter().position(|m| m.id() == from_message_id) { let older: Vec<_> = chat_messages.iter().take(idx).cloned().collect(); @@ -329,24 +380,24 @@ impl FakeTdClient { if self.should_fail() { return Err("Failed to send message".to_string()); } - + if self.simulate_delays { tokio::time::sleep(tokio::time::Duration::from_millis(200)).await; } - + let message_id = MessageId::new((self.sent_messages.lock().unwrap().len() as i64) + 1000); - + self.sent_messages.lock().unwrap().push(SentMessage { chat_id: chat_id.as_i64(), text: text.clone(), reply_to, reply_info: reply_info.clone(), }); - + let message = MessageInfo::new( message_id, "You".to_string(), - true, // is_outgoing + true, // is_outgoing text.clone(), vec![], // entities chrono::Utc::now().timestamp() as i32, @@ -356,10 +407,10 @@ impl FakeTdClient { true, // can_be_deleted_only_for_self true, // can_be_deleted_for_all_users reply_info, - None, // forward_from + None, // forward_from vec![], // reactions ); - + // Добавляем в историю self.messages .lock() @@ -367,16 +418,13 @@ impl FakeTdClient { .entry(chat_id.as_i64()) .or_insert_with(Vec::new) .push(message.clone()); - + // Отправляем Update::NewMessage - self.send_update(TdUpdate::NewMessage { - chat_id, - message: message.clone(), - }); - + self.send_update(TdUpdate::NewMessage { chat_id, message: message.clone() }); + Ok(message) } - + /// Редактировать сообщение pub async fn edit_message( &self, @@ -387,41 +435,37 @@ impl FakeTdClient { if self.should_fail() { return Err("Failed to edit message".to_string()); } - + if self.simulate_delays { tokio::time::sleep(tokio::time::Duration::from_millis(150)).await; } - + self.edited_messages.lock().unwrap().push(EditedMessage { chat_id: chat_id.as_i64(), message_id, new_text: new_text.clone(), }); - + // Обновляем сообщение let mut messages = self.messages.lock().unwrap(); if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) { if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) { msg.content.text = new_text.clone(); msg.metadata.edit_date = msg.metadata.date + 60; - + let updated = msg.clone(); drop(messages); // Освобождаем lock перед отправкой update - + // Отправляем Update - self.send_update(TdUpdate::MessageContent { - chat_id, - message_id, - new_text, - }); - + self.send_update(TdUpdate::MessageContent { chat_id, message_id, new_text }); + return Ok(updated); } } - + Err("Message not found".to_string()) } - + /// Удалить сообщения pub async fn delete_messages( &self, @@ -432,33 +476,30 @@ impl FakeTdClient { if self.should_fail() { return Err("Failed to delete messages".to_string()); } - + if self.simulate_delays { tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; } - + self.deleted_messages.lock().unwrap().push(DeletedMessages { chat_id: chat_id.as_i64(), message_ids: message_ids.clone(), revoke, }); - + // Удаляем из истории let mut messages = self.messages.lock().unwrap(); if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) { chat_msgs.retain(|m| !message_ids.contains(&m.id())); } drop(messages); - + // Отправляем Update - self.send_update(TdUpdate::DeleteMessages { - chat_id, - message_ids, - }); - + self.send_update(TdUpdate::DeleteMessages { chat_id, message_ids }); + Ok(()) } - + /// Переслать сообщения pub async fn forward_messages( &self, @@ -469,26 +510,33 @@ impl FakeTdClient { if self.should_fail() { return Err("Failed to forward messages".to_string()); } - + if self.simulate_delays { tokio::time::sleep(tokio::time::Duration::from_millis(150)).await; } - - self.forwarded_messages.lock().unwrap().push(ForwardedMessages { - from_chat_id: from_chat_id.as_i64(), - to_chat_id: to_chat_id.as_i64(), - message_ids, - }); - + + self.forwarded_messages + .lock() + .unwrap() + .push(ForwardedMessages { + from_chat_id: from_chat_id.as_i64(), + to_chat_id: to_chat_id.as_i64(), + message_ids, + }); + Ok(()) } /// Поиск сообщений в чате - pub async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result, String> { + pub async fn search_messages( + &self, + chat_id: ChatId, + query: &str, + ) -> Result, String> { if self.should_fail() { return Err("Failed to search messages".to_string()); } - + let messages = self.messages.lock().unwrap(); let results: Vec<_> = messages .get(&chat_id.as_i64()) @@ -499,43 +547,49 @@ impl FakeTdClient { .collect() }) .unwrap_or_default(); - + self.searched_queries.lock().unwrap().push(SearchQuery { chat_id: chat_id.as_i64(), query: query.to_string(), results_count: results.len(), }); - + Ok(results) } - + /// Установить черновик pub async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<(), String> { if text.is_empty() { self.drafts.lock().unwrap().remove(&chat_id.as_i64()); } else { - self.drafts.lock().unwrap().insert(chat_id.as_i64(), text.clone()); + self.drafts + .lock() + .unwrap() + .insert(chat_id.as_i64(), text.clone()); } - + self.send_update(TdUpdate::ChatDraftMessage { chat_id, draft_text: if text.is_empty() { None } else { Some(text) }, }); - + Ok(()) } - + /// Отправить действие в чате (typing, etc.) pub async fn send_chat_action(&self, chat_id: ChatId, action: String) { - self.chat_actions.lock().unwrap().push((chat_id.as_i64(), action.clone())); - + self.chat_actions + .lock() + .unwrap() + .push((chat_id.as_i64(), action.clone())); + if action == "Typing" { *self.typing_chat_id.lock().unwrap() = Some(chat_id.as_i64()); } else if action == "Cancel" { *self.typing_chat_id.lock().unwrap() = None; } } - + /// Получить доступные реакции для сообщения pub async fn get_message_available_reactions( &self, @@ -545,10 +599,10 @@ impl FakeTdClient { if self.should_fail() { return Err("Failed to get available reactions".to_string()); } - + Ok(self.available_reactions.lock().unwrap().clone()) } - + /// Установить/удалить реакцию pub async fn toggle_reaction( &self, @@ -559,15 +613,18 @@ impl FakeTdClient { if self.should_fail() { return Err("Failed to toggle reaction".to_string()); } - + // Обновляем реакции на сообщении let mut messages = self.messages.lock().unwrap(); if let Some(chat_msgs) = messages.get_mut(&chat_id.as_i64()) { if let Some(msg) = chat_msgs.iter_mut().find(|m| m.id() == message_id) { let reactions = &mut msg.interactions.reactions; - + // Toggle logic - if let Some(pos) = reactions.iter().position(|r| r.emoji == emoji && r.is_chosen) { + if let Some(pos) = reactions + .iter() + .position(|r| r.emoji == emoji && r.is_chosen) + { // Удаляем свою реакцию reactions.remove(pos); } else if let Some(reaction) = reactions.iter_mut().find(|r| r.emoji == emoji) { @@ -582,10 +639,10 @@ impl FakeTdClient { is_chosen: true, }); } - + let updated_reactions = reactions.clone(); drop(messages); - + // Отправляем Update self.send_update(TdUpdate::MessageInteractionInfo { chat_id, @@ -594,10 +651,10 @@ impl FakeTdClient { }); } } - + Ok(()) } - + /// Скачать файл (mock) pub async fn download_file(&self, file_id: i32) -> Result { if self.should_fail() { @@ -617,7 +674,7 @@ impl FakeTdClient { if self.should_fail() { return Err("Failed to get profile info".to_string()); } - + self.profiles .lock() .unwrap() @@ -625,7 +682,7 @@ impl FakeTdClient { .cloned() .ok_or_else(|| "Profile not found".to_string()) } - + /// Отметить сообщения как просмотренные pub async fn view_messages(&self, chat_id: ChatId, message_ids: Vec) { self.viewed_messages @@ -633,25 +690,25 @@ impl FakeTdClient { .unwrap() .push((chat_id.as_i64(), message_ids.iter().map(|id| id.as_i64()).collect())); } - + /// Загрузить чаты папки pub async fn load_folder_chats(&self, _folder_id: i32, _limit: usize) -> Result<(), String> { if self.should_fail() { return Err("Failed to load folder chats".to_string()); } - + Ok(()) } - + // ==================== Helper Methods ==================== - + /// Отправить update в канал (если он установлен) fn send_update(&self, update: TdUpdate) { if let Some(tx) = self.update_tx.lock().unwrap().as_ref() { let _ = tx.send(update); } } - + /// Проверить нужно ли симулировать ошибку fn should_fail(&self) -> bool { let mut fail = self.fail_next_operation.lock().unwrap(); @@ -662,16 +719,16 @@ impl FakeTdClient { false } } - + /// Симулировать ошибку в следующей операции pub fn fail_next(&self) { *self.fail_next_operation.lock().unwrap() = true; } - + /// Симулировать входящее сообщение pub fn simulate_incoming_message(&self, chat_id: ChatId, text: String, sender_name: &str) { let message_id = MessageId::new(9000 + chrono::Utc::now().timestamp()); - + let message = MessageInfo::new( message_id, sender_name.to_string(), @@ -688,7 +745,7 @@ impl FakeTdClient { None, vec![], ); - + // Добавляем в историю self.messages .lock() @@ -696,26 +753,22 @@ impl FakeTdClient { .entry(chat_id.as_i64()) .or_insert_with(Vec::new) .push(message.clone()); - + // Отправляем Update self.send_update(TdUpdate::NewMessage { chat_id, message }); } - + /// Симулировать typing от собеседника pub fn simulate_typing(&self, chat_id: ChatId, user_id: UserId) { - self.send_update(TdUpdate::ChatAction { - chat_id, - user_id, - action: "Typing".to_string(), - }); + self.send_update(TdUpdate::ChatAction { chat_id, user_id, action: "Typing".to_string() }); } - + /// Симулировать изменение состояния сети pub fn simulate_network_change(&self, state: NetworkState) { *self.network_state.lock().unwrap() = state.clone(); self.send_update(TdUpdate::ConnectionState { state }); } - + /// Симулировать прочтение сообщений pub fn simulate_read_outbox(&self, chat_id: ChatId, last_read_message_id: MessageId) { self.send_update(TdUpdate::ChatReadOutbox { @@ -723,9 +776,9 @@ impl FakeTdClient { last_read_outbox_message_id: last_read_message_id, }); } - + // ==================== Getters for Test Assertions ==================== - + /// Получить все чаты pub fn get_chats(&self) -> Vec { self.chats.lock().unwrap().clone() @@ -745,57 +798,57 @@ impl FakeTdClient { .cloned() .unwrap_or_default() } - + /// Получить отправленные сообщения pub fn get_sent_messages(&self) -> Vec { self.sent_messages.lock().unwrap().clone() } - + /// Получить отредактированные сообщения pub fn get_edited_messages(&self) -> Vec { self.edited_messages.lock().unwrap().clone() } - + /// Получить удалённые сообщения pub fn get_deleted_messages(&self) -> Vec { self.deleted_messages.lock().unwrap().clone() } - + /// Получить пересланные сообщения pub fn get_forwarded_messages(&self) -> Vec { self.forwarded_messages.lock().unwrap().clone() } - + /// Получить поисковые запросы pub fn get_search_queries(&self) -> Vec { self.searched_queries.lock().unwrap().clone() } - + /// Получить просмотренные сообщения pub fn get_viewed_messages(&self) -> Vec<(i64, Vec)> { self.viewed_messages.lock().unwrap().clone() } - + /// Получить действия в чатах pub fn get_chat_actions(&self) -> Vec<(i64, String)> { self.chat_actions.lock().unwrap().clone() } - + /// Получить текущее состояние сети pub fn get_network_state(&self) -> NetworkState { self.network_state.lock().unwrap().clone() } - + /// Получить ID текущего открытого чата pub fn get_current_chat_id(&self) -> Option { *self.current_chat_id.lock().unwrap() } - + /// Установить update channel для получения событий pub fn set_update_channel(&self, tx: mpsc::UnboundedSender) { *self.update_tx.lock().unwrap() = Some(tx); } - + /// Очистить всю историю действий pub fn clear_all_history(&self) { self.sent_messages.lock().unwrap().clear(); @@ -835,10 +888,12 @@ mod tests { async fn test_send_message() { let client = FakeTdClient::new(); let chat_id = ChatId::new(123); - - let result = client.send_message(chat_id, "Hello".to_string(), None, None).await; + + let result = client + .send_message(chat_id, "Hello".to_string(), None, None) + .await; assert!(result.is_ok()); - + let sent = client.get_sent_messages(); assert_eq!(sent.len(), 1); assert_eq!(sent[0].text, "Hello"); @@ -849,12 +904,17 @@ mod tests { async fn test_edit_message() { let client = FakeTdClient::new(); let chat_id = ChatId::new(123); - - let msg = client.send_message(chat_id, "Hello".to_string(), None, None).await.unwrap(); + + let msg = client + .send_message(chat_id, "Hello".to_string(), None, None) + .await + .unwrap(); let msg_id = msg.id(); - - let _ = client.edit_message(chat_id, msg_id, "Hello World".to_string()).await; - + + let _ = client + .edit_message(chat_id, msg_id, "Hello World".to_string()) + .await; + let edited = client.get_edited_messages(); assert_eq!(edited.len(), 1); assert_eq!(client.get_messages(123)[0].text(), "Hello World"); @@ -865,25 +925,30 @@ mod tests { async fn test_delete_message() { let client = FakeTdClient::new(); let chat_id = ChatId::new(123); - - let msg = client.send_message(chat_id, "Hello".to_string(), None, None).await.unwrap(); + + let msg = client + .send_message(chat_id, "Hello".to_string(), None, None) + .await + .unwrap(); let msg_id = msg.id(); - + let _ = client.delete_messages(chat_id, vec![msg_id], false).await; - + let deleted = client.get_deleted_messages(); assert_eq!(deleted.len(), 1); assert_eq!(client.get_messages(123).len(), 0); } - + #[tokio::test] async fn test_update_channel() { let (client, mut rx) = FakeTdClient::new().with_update_channel(); let chat_id = ChatId::new(123); - + // Отправляем сообщение - let _ = client.send_message(chat_id, "Test".to_string(), None, None).await; - + let _ = client + .send_message(chat_id, "Test".to_string(), None, None) + .await; + // Проверяем что получили Update if let Some(update) = rx.recv().await { match update { @@ -896,39 +961,43 @@ mod tests { panic!("No update received"); } } - + #[tokio::test] async fn test_simulate_incoming_message() { let (client, mut rx) = FakeTdClient::new().with_update_channel(); let chat_id = ChatId::new(123); - + client.simulate_incoming_message(chat_id, "Hello from Bob".to_string(), "Bob"); - + // Проверяем Update if let Some(TdUpdate::NewMessage { message, .. }) = rx.recv().await { assert_eq!(message.text(), "Hello from Bob"); assert_eq!(message.sender_name(), "Bob"); assert!(!message.is_outgoing()); } - + // Проверяем что сообщение добавилось assert_eq!(client.get_messages(123).len(), 1); } - + #[tokio::test] async fn test_fail_next_operation() { let client = FakeTdClient::new(); let chat_id = ChatId::new(123); - + // Устанавливаем флаг ошибки client.fail_next(); - + // Следующая операция должна упасть - let result = client.send_message(chat_id, "Test".to_string(), None, None).await; + let result = client + .send_message(chat_id, "Test".to_string(), None, None) + .await; assert!(result.is_err()); - + // Но следующая должна пройти - let result2 = client.send_message(chat_id, "Test2".to_string(), None, None).await; + let result2 = client + .send_message(chat_id, "Test2".to_string(), None, None) + .await; assert!(result2.is_ok()); } } diff --git a/tests/helpers/fake_tdclient_impl.rs b/tests/helpers/fake_tdclient_impl.rs index 4a27238..8104bc6 100644 --- a/tests/helpers/fake_tdclient_impl.rs +++ b/tests/helpers/fake_tdclient_impl.rs @@ -4,8 +4,11 @@ use super::fake_tdclient::FakeTdClient; use async_trait::async_trait; use std::path::PathBuf; use tdlib_rs::enums::{ChatAction, Update}; -use tele_tui::tdlib::{AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache, UserOnlineStatus}; use tele_tui::tdlib::TdClientTrait; +use tele_tui::tdlib::{ + AuthState, ChatInfo, FolderInfo, MessageInfo, ProfileInfo, ReplyInfo, UserCache, + UserOnlineStatus, +}; use tele_tui::types::{ChatId, MessageId, UserId}; #[async_trait] @@ -55,11 +58,19 @@ impl TdClientTrait for FakeTdClient { } // ============ Message methods ============ - async fn get_chat_history(&mut self, chat_id: ChatId, limit: i32) -> Result, String> { + async fn get_chat_history( + &mut self, + chat_id: ChatId, + limit: i32, + ) -> Result, String> { FakeTdClient::get_chat_history(self, chat_id, limit).await } - async fn load_older_messages(&mut self, chat_id: ChatId, from_message_id: MessageId) -> Result, String> { + async fn load_older_messages( + &mut self, + chat_id: ChatId, + from_message_id: MessageId, + ) -> Result, String> { FakeTdClient::load_older_messages(self, chat_id, from_message_id).await } @@ -72,7 +83,11 @@ impl TdClientTrait for FakeTdClient { // Not implemented for fake } - async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result, String> { + async fn search_messages( + &self, + chat_id: ChatId, + query: &str, + ) -> Result, String> { FakeTdClient::search_messages(self, chat_id, query).await } @@ -130,7 +145,10 @@ impl TdClientTrait for FakeTdClient { let mut pending = self.pending_view_messages.lock().unwrap(); for (chat_id, message_ids) in pending.drain(..) { let ids: Vec = message_ids.iter().map(|id| id.as_i64()).collect(); - self.viewed_messages.lock().unwrap().push((chat_id.as_i64(), ids)); + self.viewed_messages + .lock() + .unwrap() + .push((chat_id.as_i64(), ids)); } } @@ -189,13 +207,17 @@ impl TdClientTrait for FakeTdClient { static AUTH_STATE_WAIT_PHONE: OnceLock = OnceLock::new(); static AUTH_STATE_WAIT_CODE: OnceLock = OnceLock::new(); static AUTH_STATE_WAIT_PASSWORD: OnceLock = OnceLock::new(); - + let current = self.auth_state.lock().unwrap(); match *current { AuthState::Ready => &AUTH_STATE_READY, - AuthState::WaitPhoneNumber => AUTH_STATE_WAIT_PHONE.get_or_init(|| AuthState::WaitPhoneNumber), + AuthState::WaitPhoneNumber => { + AUTH_STATE_WAIT_PHONE.get_or_init(|| AuthState::WaitPhoneNumber) + } AuthState::WaitCode => AUTH_STATE_WAIT_CODE.get_or_init(|| AuthState::WaitCode), - AuthState::WaitPassword => AUTH_STATE_WAIT_PASSWORD.get_or_init(|| AuthState::WaitPassword), + AuthState::WaitPassword => { + AUTH_STATE_WAIT_PASSWORD.get_or_init(|| AuthState::WaitPassword) + } _ => &AUTH_STATE_READY, } } diff --git a/tests/helpers/test_data.rs b/tests/helpers/test_data.rs index 982043b..82af233 100644 --- a/tests/helpers/test_data.rs +++ b/tests/helpers/test_data.rs @@ -1,7 +1,7 @@ // Test data builders and fixtures -use tele_tui::tdlib::{ChatInfo, MessageInfo, ProfileInfo, ReplyInfo}; use tele_tui::tdlib::types::{ForwardInfo, ReactionInfo}; +use tele_tui::tdlib::{ChatInfo, MessageInfo, ProfileInfo, ReplyInfo}; use tele_tui::types::{ChatId, MessageId}; /// Builder для создания тестового чата @@ -177,9 +177,7 @@ impl TestMessageBuilder { } pub fn forwarded_from(mut self, sender: &str) -> Self { - self.forward_from = Some(ForwardInfo { - sender_name: sender.to_string(), - }); + self.forward_from = Some(ForwardInfo { sender_name: sender.to_string() }); self } diff --git a/tests/input_navigation.rs b/tests/input_navigation.rs index 1829383..d0e9190 100644 --- a/tests/input_navigation.rs +++ b/tests/input_navigation.rs @@ -292,7 +292,9 @@ async fn test_normal_mode_auto_enters_message_selection() { #[tokio::test] async fn test_album_navigation_skips_grouped_messages() { let messages = vec![ - TestMessageBuilder::new("Before album", 1).sender("Alice").build(), + TestMessageBuilder::new("Before album", 1) + .sender("Alice") + .build(), TestMessageBuilder::new("Photo 1", 2) .sender("Alice") .media_album_id(100) @@ -305,7 +307,9 @@ async fn test_album_navigation_skips_grouped_messages() { .sender("Alice") .media_album_id(100) .build(), - TestMessageBuilder::new("After album", 5).sender("Alice").build(), + TestMessageBuilder::new("After album", 5) + .sender("Alice") + .build(), ]; let mut app = TestAppBuilder::new() @@ -347,7 +351,9 @@ async fn test_album_navigation_skips_grouped_messages() { #[tokio::test] async fn test_album_navigation_start_at_album_end() { let messages = vec![ - TestMessageBuilder::new("Regular", 1).sender("Alice").build(), + TestMessageBuilder::new("Regular", 1) + .sender("Alice") + .build(), TestMessageBuilder::new("Album Photo 1", 2) .sender("Alice") .media_album_id(200) diff --git a/tests/modals.rs b/tests/modals.rs index 75eee3c..99421a3 100644 --- a/tests/modals.rs +++ b/tests/modals.rs @@ -3,12 +3,12 @@ mod helpers; use helpers::app_builder::TestAppBuilder; -use tele_tui::tdlib::TdClientTrait; use helpers::snapshot_utils::{buffer_to_string, render_to_buffer}; use helpers::test_data::{ create_test_chat, create_test_profile, TestChatBuilder, TestMessageBuilder, }; use insta::assert_snapshot; +use tele_tui::tdlib::TdClientTrait; #[test] fn snapshot_delete_confirmation_modal() { @@ -35,7 +35,16 @@ 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 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) @@ -57,7 +66,16 @@ 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 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) @@ -160,7 +178,9 @@ fn snapshot_search_in_chat() { .build(); // Устанавливаем результаты поиска - if let tele_tui::app::ChatState::SearchInChat { results, selected_index, .. } = &mut app.chat_state { + if let tele_tui::app::ChatState::SearchInChat { results, selected_index, .. } = + &mut app.chat_state + { *results = vec![msg1, msg2]; *selected_index = 0; } diff --git a/tests/network_typing.rs b/tests/network_typing.rs index 1bf0096..61365b4 100644 --- a/tests/network_typing.rs +++ b/tests/network_typing.rs @@ -97,7 +97,9 @@ async fn test_typing_indicator_on() { // Alice начала печатать в чате 123 // Симулируем через send_chat_action - client.send_chat_action(ChatId::new(123), "Typing".to_string()).await; + client + .send_chat_action(ChatId::new(123), "Typing".to_string()) + .await; assert_eq!(*client.typing_chat_id.lock().unwrap(), Some(123)); @@ -110,11 +112,15 @@ async fn test_typing_indicator_off() { let client = FakeTdClient::new(); // Изначально Alice печатала - client.send_chat_action(ChatId::new(123), "Typing".to_string()).await; + client + .send_chat_action(ChatId::new(123), "Typing".to_string()) + .await; assert_eq!(*client.typing_chat_id.lock().unwrap(), Some(123)); // Alice перестала печатать - client.send_chat_action(ChatId::new(123), "Cancel".to_string()).await; + client + .send_chat_action(ChatId::new(123), "Cancel".to_string()) + .await; assert_eq!(*client.typing_chat_id.lock().unwrap(), None); diff --git a/tests/reactions.rs b/tests/reactions.rs index 391967b..8d1e12c 100644 --- a/tests/reactions.rs +++ b/tests/reactions.rs @@ -12,10 +12,16 @@ async fn test_add_reaction_to_message() { let client = FakeTdClient::new(); // Отправляем сообщение - let msg = client.send_message(ChatId::new(123), "React to this!".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "React to this!".to_string(), None, None) + .await + .unwrap(); // Добавляем реакцию - client.toggle_reaction(ChatId::new(123), msg.id(), "👍".to_string()).await.unwrap(); + client + .toggle_reaction(ChatId::new(123), msg.id(), "👍".to_string()) + .await + .unwrap(); // Проверяем что реакция записалась let messages = client.get_messages(123); @@ -46,7 +52,10 @@ async fn test_toggle_reaction_removes_it() { let msg_id = messages_before[0].id(); // Toggle - удаляем свою реакцию - client.toggle_reaction(ChatId::new(123), msg_id, "👍".to_string()).await.unwrap(); + client + .toggle_reaction(ChatId::new(123), msg_id, "👍".to_string()) + .await + .unwrap(); let messages_after = client.get_messages(123); assert_eq!(messages_after[0].reactions().len(), 0); @@ -57,13 +66,28 @@ async fn test_toggle_reaction_removes_it() { async fn test_multiple_reactions_on_one_message() { let client = FakeTdClient::new(); - let msg = client.send_message(ChatId::new(123), "Many reactions".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Many reactions".to_string(), None, None) + .await + .unwrap(); // Добавляем несколько разных реакций - client.toggle_reaction(ChatId::new(123), msg.id(), "👍".to_string()).await.unwrap(); - client.toggle_reaction(ChatId::new(123), msg.id(), "❤️".to_string()).await.unwrap(); - client.toggle_reaction(ChatId::new(123), msg.id(), "😂".to_string()).await.unwrap(); - client.toggle_reaction(ChatId::new(123), msg.id(), "🔥".to_string()).await.unwrap(); + client + .toggle_reaction(ChatId::new(123), msg.id(), "👍".to_string()) + .await + .unwrap(); + client + .toggle_reaction(ChatId::new(123), msg.id(), "❤️".to_string()) + .await + .unwrap(); + client + .toggle_reaction(ChatId::new(123), msg.id(), "😂".to_string()) + .await + .unwrap(); + client + .toggle_reaction(ChatId::new(123), msg.id(), "🔥".to_string()) + .await + .unwrap(); // Проверяем что все 4 реакции записались let messages = client.get_messages(123); @@ -151,7 +175,10 @@ async fn test_reaction_counter_increases() { let msg_id = messages_before[0].id(); // Мы добавляем свою реакцию - счётчик должен увеличиться - client.toggle_reaction(ChatId::new(123), msg_id, "👍".to_string()).await.unwrap(); + client + .toggle_reaction(ChatId::new(123), msg_id, "👍".to_string()) + .await + .unwrap(); let messages = client.get_messages(123); assert_eq!(messages[0].reactions()[0].count, 2); @@ -177,7 +204,10 @@ async fn test_update_reaction_we_add_ours() { let msg_id = messages_before[0].id(); // Добавляем нашу реакцию - client.toggle_reaction(ChatId::new(123), msg_id, "🔥".to_string()).await.unwrap(); + client + .toggle_reaction(ChatId::new(123), msg_id, "🔥".to_string()) + .await + .unwrap(); let messages = client.get_messages(123); let reaction = &messages[0].reactions()[0]; diff --git a/tests/reply_forward.rs b/tests/reply_forward.rs index ac439b3..c989e2f 100644 --- a/tests/reply_forward.rs +++ b/tests/reply_forward.rs @@ -4,8 +4,8 @@ mod helpers; use helpers::fake_tdclient::FakeTdClient; use helpers::test_data::TestMessageBuilder; -use tele_tui::tdlib::ReplyInfo; use tele_tui::tdlib::types::ForwardInfo; +use tele_tui::tdlib::ReplyInfo; use tele_tui::types::{ChatId, MessageId}; /// Test: Reply создаёт сообщение с reply_to @@ -28,7 +28,15 @@ async fn test_reply_creates_message_with_reply_to() { }; // Отвечаем на него - let reply_msg = client.send_message(ChatId::new(123), "Answer!".to_string(), Some(MessageId::new(100)), Some(reply_info)).await.unwrap(); + let reply_msg = client + .send_message( + ChatId::new(123), + "Answer!".to_string(), + Some(MessageId::new(100)), + Some(reply_info), + ) + .await + .unwrap(); // Проверяем что ответ отправлен с reply_to assert_eq!(client.get_sent_messages().len(), 1); @@ -79,7 +87,10 @@ async fn test_cancel_reply_sends_without_reply_to() { // Пользователь начал reply (r), потом отменил (Esc), затем отправил // Это эмулируется отправкой без reply_to - client.send_message(ChatId::new(123), "Regular message".to_string(), None, None).await.unwrap(); + client + .send_message(ChatId::new(123), "Regular message".to_string(), None, None) + .await + .unwrap(); // Проверяем что отправилось без reply_to assert_eq!(client.get_sent_messages()[0].reply_to, None); @@ -175,7 +186,15 @@ async fn test_reply_to_forwarded_message() { }; // Отвечаем на пересланное сообщение - let reply_msg = client.send_message(ChatId::new(123), "Thanks for sharing!".to_string(), Some(MessageId::new(100)), Some(reply_info)).await.unwrap(); + let reply_msg = client + .send_message( + ChatId::new(123), + "Thanks for sharing!".to_string(), + Some(MessageId::new(100)), + Some(reply_info), + ) + .await + .unwrap(); // Проверяем что reply содержит reply_to assert_eq!(client.get_sent_messages()[0].reply_to, Some(MessageId::new(100))); diff --git a/tests/send_message.rs b/tests/send_message.rs index 4703ac4..9f3c2ae 100644 --- a/tests/send_message.rs +++ b/tests/send_message.rs @@ -14,7 +14,10 @@ async fn test_send_text_message() { let client = client.with_chat(chat); // Отправляем сообщение - let msg = client.send_message(ChatId::new(123), "Hello, Mom!".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "Hello, Mom!".to_string(), None, None) + .await + .unwrap(); // Проверяем что сообщение было отправлено assert_eq!(client.get_sent_messages().len(), 1); @@ -36,13 +39,22 @@ async fn test_send_multiple_messages_updates_list() { let client = FakeTdClient::new(); // Отправляем первое сообщение - let msg1 = client.send_message(ChatId::new(123), "Message 1".to_string(), None, None).await.unwrap(); + let msg1 = client + .send_message(ChatId::new(123), "Message 1".to_string(), None, None) + .await + .unwrap(); // Отправляем второе сообщение - let msg2 = client.send_message(ChatId::new(123), "Message 2".to_string(), None, None).await.unwrap(); + let msg2 = client + .send_message(ChatId::new(123), "Message 2".to_string(), None, None) + .await + .unwrap(); // Отправляем третье сообщение - let msg3 = client.send_message(ChatId::new(123), "Message 3".to_string(), None, None).await.unwrap(); + let msg3 = client + .send_message(ChatId::new(123), "Message 3".to_string(), None, None) + .await + .unwrap(); // Проверяем что все 3 сообщения отслеживаются assert_eq!(client.get_sent_messages().len(), 3); @@ -66,7 +78,10 @@ async fn test_send_empty_message_technical() { let client = FakeTdClient::new(); // FakeTdClient технически может отправить пустое сообщение - let msg = client.send_message(ChatId::new(123), "".to_string(), None, None).await.unwrap(); + let msg = client + .send_message(ChatId::new(123), "".to_string(), None, None) + .await + .unwrap(); // Проверяем что оно отправилось (в реальном App это должно фильтроваться) assert_eq!(client.get_sent_messages().len(), 1); @@ -85,7 +100,10 @@ async fn test_send_message_with_markdown() { let client = FakeTdClient::new(); let text = "**Bold** *italic* `code`"; - client.send_message(ChatId::new(123), text.to_string(), None, None).await.unwrap(); + client + .send_message(ChatId::new(123), text.to_string(), None, None) + .await + .unwrap(); // Проверяем что текст сохранился как есть (парсинг markdown - отдельная логика) let messages = client.get_messages(123); @@ -99,13 +117,22 @@ async fn test_send_messages_to_different_chats() { let client = FakeTdClient::new(); // Отправляем в чат 123 - client.send_message(ChatId::new(123), "Hello Mom".to_string(), None, None).await.unwrap(); + client + .send_message(ChatId::new(123), "Hello Mom".to_string(), None, None) + .await + .unwrap(); // Отправляем в чат 456 - client.send_message(ChatId::new(456), "Hello Boss".to_string(), None, None).await.unwrap(); + client + .send_message(ChatId::new(456), "Hello Boss".to_string(), None, None) + .await + .unwrap(); // Отправляем ещё одно в чат 123 - client.send_message(ChatId::new(123), "How are you?".to_string(), None, None).await.unwrap(); + client + .send_message(ChatId::new(123), "How are you?".to_string(), None, None) + .await + .unwrap(); // Проверяем общее количество отправленных assert_eq!(client.get_sent_messages().len(), 3); @@ -128,7 +155,10 @@ async fn test_receive_incoming_message() { let client = FakeTdClient::new(); // Добавляем существующее сообщение - client.send_message(ChatId::new(123), "My outgoing".to_string(), None, None).await.unwrap(); + client + .send_message(ChatId::new(123), "My outgoing".to_string(), None, None) + .await + .unwrap(); // Симулируем входящее сообщение от собеседника let incoming_msg = TestMessageBuilder::new("Hey there!", 2000) diff --git a/tests/vim_mode.rs b/tests/vim_mode.rs index 559ef08..3c45233 100644 --- a/tests/vim_mode.rs +++ b/tests/vim_mode.rs @@ -12,9 +12,9 @@ mod helpers; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use helpers::app_builder::TestAppBuilder; use helpers::test_data::{create_test_chat, TestMessageBuilder}; -use tele_tui::app::InputMode; use tele_tui::app::methods::compose::ComposeMethods; use tele_tui::app::methods::messages::MessageMethods; +use tele_tui::app::InputMode; use tele_tui::input::handle_main_input; fn key(code: KeyCode) -> KeyEvent { @@ -32,9 +32,7 @@ fn ctrl_key(c: char) -> KeyEvent { /// `i` в Normal mode → переход в Insert mode #[tokio::test] async fn test_i_enters_insert_mode() { - let messages = vec![ - TestMessageBuilder::new("Hello", 1).outgoing().build(), - ]; + let messages = vec![TestMessageBuilder::new("Hello", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -54,9 +52,7 @@ async fn test_i_enters_insert_mode() { /// `ш` (русская i) в Normal mode → переход в Insert mode #[tokio::test] async fn test_russian_i_enters_insert_mode() { - let messages = vec![ - TestMessageBuilder::new("Hello", 1).outgoing().build(), - ]; + let messages = vec![TestMessageBuilder::new("Hello", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -72,9 +68,7 @@ async fn test_russian_i_enters_insert_mode() { /// Esc в Insert mode → Normal mode + MessageSelection #[tokio::test] async fn test_esc_exits_insert_mode() { - let messages = vec![ - TestMessageBuilder::new("Hello", 1).outgoing().build(), - ]; + let messages = vec![TestMessageBuilder::new("Hello", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -127,9 +121,9 @@ async fn test_close_chat_resets_input_mode() { /// Auto-Insert при Reply (`r` в MessageSelection) #[tokio::test] async fn test_reply_auto_enters_insert_mode() { - let messages = vec![ - TestMessageBuilder::new("Hello from friend", 1).sender("Friend").build(), - ]; + let messages = vec![TestMessageBuilder::new("Hello from friend", 1) + .sender("Friend") + .build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -149,9 +143,7 @@ async fn test_reply_auto_enters_insert_mode() { /// Auto-Insert при Edit (Enter в MessageSelection) #[tokio::test] async fn test_edit_auto_enters_insert_mode() { - let messages = vec![ - TestMessageBuilder::new("My message", 1).outgoing().build(), - ]; + let messages = vec![TestMessageBuilder::new("My message", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -248,9 +240,7 @@ async fn test_k_types_in_insert_mode() { /// `d` в Insert mode → набирает "d", НЕ удаляет сообщение #[tokio::test] async fn test_d_types_in_insert_mode() { - let messages = vec![ - TestMessageBuilder::new("Hello", 1).outgoing().build(), - ]; + let messages = vec![TestMessageBuilder::new("Hello", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -268,9 +258,7 @@ async fn test_d_types_in_insert_mode() { /// `r` в Insert mode → набирает "r", НЕ reply #[tokio::test] async fn test_r_types_in_insert_mode() { - let messages = vec![ - TestMessageBuilder::new("Hello", 1).build(), - ]; + let messages = vec![TestMessageBuilder::new("Hello", 1).build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -395,9 +383,7 @@ async fn test_k_navigates_in_normal_mode() { /// `d` в Normal mode → показывает подтверждение удаления #[tokio::test] async fn test_d_deletes_in_normal_mode() { - let messages = vec![ - TestMessageBuilder::new("My message", 1).outgoing().build(), - ]; + let messages = vec![TestMessageBuilder::new("My message", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -488,9 +474,7 @@ async fn test_ctrl_e_moves_to_end_in_insert() { /// Esc из Insert при активном Reply → отменяет reply + Normal + MessageSelection #[tokio::test] async fn test_esc_from_insert_cancels_reply() { - let messages = vec![ - TestMessageBuilder::new("Hello", 1).sender("Friend").build(), - ]; + let messages = vec![TestMessageBuilder::new("Hello", 1).sender("Friend").build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -512,9 +496,7 @@ async fn test_esc_from_insert_cancels_reply() { /// Esc из Insert при активном Editing → отменяет editing + Normal + MessageSelection #[tokio::test] async fn test_esc_from_insert_cancels_editing() { - let messages = vec![ - TestMessageBuilder::new("My message", 1).outgoing().build(), - ]; + let messages = vec![TestMessageBuilder::new("My message", 1).outgoing().build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -564,9 +546,7 @@ async fn test_normal_mode_auto_enters_selection_on_any_key() { /// Полный цикл: Normal → i → набор текста → Esc → Normal #[tokio::test] async fn test_full_mode_cycle() { - let messages = vec![ - TestMessageBuilder::new("Msg", 1).build(), - ]; + let messages = vec![TestMessageBuilder::new("Msg", 1).build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101) @@ -599,9 +579,9 @@ async fn test_full_mode_cycle() { /// Полный цикл: Normal → r (reply) → набор → Enter (отправка) → остаёмся в Insert #[tokio::test] async fn test_reply_send_stays_insert() { - let messages = vec![ - TestMessageBuilder::new("Question?", 1).sender("Friend").build(), - ]; + let messages = vec![TestMessageBuilder::new("Question?", 1) + .sender("Friend") + .build()]; let mut app = TestAppBuilder::new() .with_chats(vec![create_test_chat("Chat", 101)]) .selected_chat(101)