From 4deb0fbe009152a0b6345d43e2de641e2d0c88c1 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Fri, 30 Jan 2026 15:07:13 +0300 Subject: [PATCH] commit --- src/app/mod.rs | 38 ++- src/config.rs | 22 +- src/input/auth.rs | 25 +- src/input/main_input.rs | 267 +++++++++++++----- src/main.rs | 39 ++- src/tdlib/client.rs | 475 ++++++++++++++++++-------------- src/tdlib/mod.rs | 12 +- src/ui/auth.rs | 4 +- src/ui/chat_list.rs | 57 ++-- src/ui/footer.rs | 9 +- src/ui/loading.rs | 13 +- src/ui/main_screen.rs | 11 +- src/ui/messages.rs | 426 ++++++++++++++++++++-------- src/ui/mod.rs | 21 +- src/ui/profile.rs | 80 ++++-- src/utils.rs | 10 +- tests/chat_list.rs | 24 +- tests/delete_message.rs | 6 +- tests/drafts.rs | 4 +- tests/edit_message.rs | 6 +- tests/footer.rs | 20 +- tests/helpers/app_builder.rs | 22 +- tests/helpers/fake_tdclient.rs | 32 +-- tests/helpers/snapshot_utils.rs | 12 +- tests/helpers/test_data.rs | 9 +- tests/input_field.rs | 4 +- tests/messages.rs | 35 +-- tests/modals.rs | 37 +-- tests/navigation.rs | 4 +- tests/screens.rs | 12 +- tests/search.rs | 8 +- tests/send_message.rs | 2 +- 32 files changed, 1049 insertions(+), 697 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index fa48e81..9ac1993 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2,9 +2,9 @@ mod state; pub use state::AppScreen; -use ratatui::widgets::ListState; use crate::tdlib::client::ChatInfo; use crate::tdlib::TdClient; +use ratatui::widgets::ListState; pub struct App { pub config: crate::config::Config, @@ -87,7 +87,6 @@ pub struct App { pub selected_reaction_index: usize, } - impl App { pub fn new(config: crate::config::Config) -> App { let mut state = ListState::default(); @@ -226,13 +225,14 @@ impl App { self.selected_message_index = Some( self.selected_message_index .map(|i| (i + 1).min(total - 1)) - .unwrap_or(0) + .unwrap_or(0), ); } /// Выбрать следующее сообщение (вниз по списку = уменьшить индекс) pub fn select_next_message(&mut self) { - self.selected_message_index = self.selected_message_index + self.selected_message_index = self + .selected_message_index .map(|i| if i > 0 { Some(i - 1) } else { None }) .flatten(); } @@ -312,7 +312,8 @@ impl App { pub fn get_filtered_chats(&self) -> Vec<&ChatInfo> { let folder_filtered: Vec<&ChatInfo> = match self.selected_folder_id { None => self.chats.iter().collect(), // All - показываем все - Some(folder_id) => self.chats + Some(folder_id) => self + .chats .iter() .filter(|c| c.folder_ids.contains(&folder_id)) .collect(), @@ -410,7 +411,10 @@ impl App { /// Получить сообщение, на которое отвечаем pub fn get_replying_to_message(&self) -> Option<&crate::tdlib::client::MessageInfo> { self.replying_to_message_id.and_then(|id| { - self.td_client.current_chat_messages.iter().find(|m| m.id == id) + self.td_client + .current_chat_messages + .iter() + .find(|m| m.id == id) }) } @@ -441,7 +445,10 @@ impl App { /// Получить сообщение для пересылки pub fn get_forwarding_message(&self) -> Option<&crate::tdlib::client::MessageInfo> { self.forwarding_message_id.and_then(|id| { - self.td_client.current_chat_messages.iter().find(|m| m.id == id) + self.td_client + .current_chat_messages + .iter() + .find(|m| m.id == id) }) } @@ -470,7 +477,9 @@ impl App { /// Выбрать предыдущий pinned (вверх = более старый) pub fn select_previous_pinned(&mut self) { - if !self.pinned_messages.is_empty() && self.selected_pinned_index < self.pinned_messages.len() - 1 { + if !self.pinned_messages.is_empty() + && self.selected_pinned_index < self.pinned_messages.len() - 1 + { self.selected_pinned_index += 1; } } @@ -530,8 +539,8 @@ impl App { /// Выбрать следующий результат (вниз) pub fn select_next_search_result(&mut self) { - if !self.message_search_results.is_empty() - && self.selected_search_result_index < self.message_search_results.len() - 1 + if !self.message_search_results.is_empty() + && self.selected_search_result_index < self.message_search_results.len() - 1 { self.selected_search_result_index += 1; } @@ -539,7 +548,8 @@ impl App { /// Получить текущий выбранный результат pub fn get_selected_search_result(&self) -> Option<&crate::tdlib::client::MessageInfo> { - self.message_search_results.get(self.selected_search_result_index) + self.message_search_results + .get(self.selected_search_result_index) } /// Получить ID выбранного результата для перехода @@ -629,7 +639,11 @@ impl App { self.is_reaction_picker_mode } - pub fn enter_reaction_picker_mode(&mut self, message_id: i64, available_reactions: Vec) { + pub fn enter_reaction_picker_mode( + &mut self, + message_id: i64, + available_reactions: Vec, + ) { self.is_reaction_picker_mode = true; self.selected_message_for_reaction = Some(message_id); self.available_reactions = available_reactions; diff --git a/src/config.rs b/src/config.rs index 118d266..12d1ae1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -67,9 +67,7 @@ fn default_reaction_other_color() -> String { impl Default for GeneralConfig { fn default() -> Self { - Self { - timezone: default_timezone(), - } + Self { timezone: default_timezone() } } } @@ -132,15 +130,13 @@ impl Config { } match fs::read_to_string(&config_path) { - Ok(content) => { - match toml::from_str::(&content) { - Ok(config) => config, - Err(e) => { - eprintln!("Warning: Could not parse config file: {}", e); - Self::default() - } + Ok(content) => match toml::from_str::(&content) { + Ok(config) => config, + Err(e) => { + eprintln!("Warning: Could not parse config file: {}", e); + Self::default() } - } + }, Err(e) => { eprintln!("Warning: Could not read config file: {}", e); Self::default() @@ -150,8 +146,8 @@ impl Config { /// Сохранить конфигурацию в файл pub fn save(&self) -> Result<(), String> { - let config_dir = Self::config_dir() - .ok_or_else(|| "Could not determine config directory".to_string())?; + let config_dir = + Self::config_dir().ok_or_else(|| "Could not determine config directory".to_string())?; // Создаём директорию если её нет fs::create_dir_all(&config_dir) diff --git a/src/input/auth.rs b/src/input/auth.rs index d385a06..4704f06 100644 --- a/src/input/auth.rs +++ b/src/input/auth.rs @@ -1,8 +1,8 @@ +use crate::app::App; +use crate::tdlib::client::AuthState; use crossterm::event::KeyCode; use std::time::Duration; use tokio::time::timeout; -use crate::app::App; -use crate::tdlib::client::AuthState; pub async fn handle(app: &mut App, key_code: KeyCode) { match &app.td_client.auth_state { @@ -18,7 +18,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) { KeyCode::Enter => { if !app.phone_input.is_empty() { app.status_message = Some("Отправка номера...".to_string()); - match timeout(Duration::from_secs(10), app.td_client.send_phone_number(app.phone_input.clone())).await { + match timeout( + Duration::from_secs(10), + app.td_client.send_phone_number(app.phone_input.clone()), + ) + .await + { Ok(Ok(_)) => { app.error_message = None; app.status_message = None; @@ -48,7 +53,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) { KeyCode::Enter => { if !app.code_input.is_empty() { app.status_message = Some("Проверка кода...".to_string()); - match timeout(Duration::from_secs(10), app.td_client.send_code(app.code_input.clone())).await { + match timeout( + Duration::from_secs(10), + app.td_client.send_code(app.code_input.clone()), + ) + .await + { Ok(Ok(_)) => { app.error_message = None; app.status_message = None; @@ -78,7 +88,12 @@ pub async fn handle(app: &mut App, key_code: KeyCode) { KeyCode::Enter => { if !app.password_input.is_empty() { app.status_message = Some("Проверка пароля...".to_string()); - match timeout(Duration::from_secs(10), app.td_client.send_password(app.password_input.clone())).await { + match timeout( + Duration::from_secs(10), + app.td_client.send_password(app.password_input.clone()), + ) + .await + { Ok(Ok(_)) => { app.error_message = None; app.status_message = None; diff --git a/src/input/main_input.rs b/src/input/main_input.rs index 3dbe3c5..f54bf9c 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -1,8 +1,8 @@ +use crate::app::App; +use crate::tdlib::ChatAction; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::time::{Duration, Instant}; use tokio::time::timeout; -use crate::app::App; -use crate::tdlib::ChatAction; pub async fn handle(app: &mut App, key: KeyEvent) { let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL); @@ -27,7 +27,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if app.selected_chat_id.is_some() && !app.is_pinned_mode() { if let Some(chat_id) = app.get_selected_chat_id() { app.status_message = Some("Загрузка закреплённых...".to_string()); - match timeout(Duration::from_secs(5), app.td_client.get_pinned_messages(chat_id)).await { + match timeout( + Duration::from_secs(5), + app.td_client.get_pinned_messages(chat_id), + ) + .await + { Ok(Ok(messages)) => { if messages.is_empty() { app.status_message = Some("Нет закреплённых сообщений".to_string()); @@ -51,7 +56,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) { } KeyCode::Char('f') if has_ctrl => { // Ctrl+F - поиск по сообщениям в открытом чате - if app.selected_chat_id.is_some() && !app.is_pinned_mode() && !app.is_message_search_mode() { + if app.selected_chat_id.is_some() + && !app.is_pinned_mode() + && !app.is_message_search_mode() + { app.enter_message_search_mode(); } return; @@ -125,13 +133,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if profile.username.is_some() { if action_index == current_idx { if let Some(username) = &profile.username { - let url = format!("https://t.me/{}", username.trim_start_matches('@')); + let url = format!( + "https://t.me/{}", + username.trim_start_matches('@') + ); match open::that(&url) { Ok(_) => { app.status_message = Some(format!("Открыто: {}", url)); } Err(e) => { - app.error_message = Some(format!("Ошибка открытия браузера: {}", e)); + app.error_message = + Some(format!("Ошибка открытия браузера: {}", e)); } } } @@ -142,7 +154,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Действие: Скопировать ID if action_index == current_idx { - app.status_message = Some(format!("ID скопирован: {}", profile.chat_id)); + app.status_message = + Some(format!("ID скопирован: {}", profile.chat_id)); return; } current_idx += 1; @@ -174,10 +187,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) { KeyCode::Enter => { // Перейти к выбранному сообщению if let Some(msg_id) = app.get_selected_search_result_id() { - let msg_index = app.td_client.current_chat_messages + let msg_index = app + .td_client + .current_chat_messages .iter() .position(|m| m.id == msg_id); - + if let Some(idx) = msg_index { let total = app.td_client.current_chat_messages.len(); app.message_scroll_offset = total.saturating_sub(idx + 5); @@ -192,8 +207,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if !app.message_search_query.is_empty() { if let Ok(Ok(results)) = timeout( Duration::from_secs(3), - app.td_client.search_messages(chat_id, &app.message_search_query) - ).await { + app.td_client + .search_messages(chat_id, &app.message_search_query), + ) + .await + { app.set_search_results(results); } } else { @@ -207,8 +225,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(chat_id) = app.get_selected_chat_id() { if let Ok(Ok(results)) = timeout( Duration::from_secs(3), - app.td_client.search_messages(chat_id, &app.message_search_query) - ).await { + app.td_client + .search_messages(chat_id, &app.message_search_query), + ) + .await + { app.set_search_results(results); } } @@ -234,10 +255,12 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Перейти к сообщению в истории if let Some(msg_id) = app.get_selected_pinned_id() { // Ищем индекс сообщения в текущей истории - let msg_index = app.td_client.current_chat_messages + let msg_index = app + .td_client + .current_chat_messages .iter() .position(|m| m.id == msg_id); - + if let Some(idx) = msg_index { // Вычисляем scroll offset чтобы показать сообщение let total = app.td_client.current_chat_messages.len(); @@ -284,13 +307,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(chat_id) = app.selected_chat_id { app.status_message = Some("Отправка реакции...".to_string()); app.needs_redraw = true; - + match timeout( Duration::from_secs(5), - app.td_client.toggle_reaction(chat_id, message_id, emoji.clone()) - ).await { + app.td_client + .toggle_reaction(chat_id, message_id, emoji.clone()), + ) + .await + { Ok(Ok(_)) => { - app.status_message = Some(format!("Реакция {} добавлена", emoji)); + app.status_message = + Some(format!("Реакция {} добавлена", emoji)); app.exit_reaction_picker_mode(); app.needs_redraw = true; } @@ -300,7 +327,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) { app.needs_redraw = true; } Err(_) => { - app.error_message = Some("Таймаут отправки реакции".to_string()); + app.error_message = + Some("Таймаут отправки реакции".to_string()); app.status_message = None; app.needs_redraw = true; } @@ -326,7 +354,9 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(msg_id) = app.confirm_delete_message_id { if let Some(chat_id) = app.get_selected_chat_id() { // Находим сообщение для проверки can_be_deleted_for_all_users - let can_delete_for_all = app.td_client.current_chat_messages + let can_delete_for_all = app + .td_client + .current_chat_messages .iter() .find(|m| m.id == msg_id) .map(|m| m.can_be_deleted_for_all_users) @@ -334,11 +364,19 @@ pub async fn handle(app: &mut App, key: KeyEvent) { match timeout( Duration::from_secs(5), - app.td_client.delete_messages(chat_id, vec![msg_id], can_delete_for_all) - ).await { + app.td_client.delete_messages( + chat_id, + vec![msg_id], + can_delete_for_all, + ), + ) + .await + { Ok(Ok(_)) => { // Удаляем из локального списка - app.td_client.current_chat_messages.retain(|m| m.id != msg_id); + app.td_client + .current_chat_messages + .retain(|m| m.id != msg_id); app.selected_message_index = None; } Ok(Err(e)) => { @@ -377,10 +415,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(from_chat_id) = app.get_selected_chat_id() { match timeout( Duration::from_secs(5), - app.td_client.forward_messages(to_chat_id, from_chat_id, vec![msg_id]) - ).await { + app.td_client.forward_messages( + to_chat_id, + from_chat_id, + vec![msg_id], + ), + ) + .await + { Ok(Ok(_)) => { - app.status_message = Some("Сообщение переслано".to_string()); + app.status_message = + Some("Сообщение переслано".to_string()); } Ok(Err(e)) => { app.error_message = Some(e); @@ -418,12 +463,25 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(chat_id) = app.get_selected_chat_id() { app.status_message = Some("Загрузка сообщений...".to_string()); app.message_scroll_offset = 0; - match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await { + match timeout( + Duration::from_secs(10), + app.td_client.get_chat_history(chat_id, 100), + ) + .await + { Ok(Ok(_)) => { // Загружаем недостающие reply info - let _ = timeout(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()).await; + let _ = timeout( + Duration::from_secs(5), + app.td_client.fetch_missing_reply_info(), + ) + .await; // Загружаем последнее закреплённое сообщение - let _ = timeout(Duration::from_secs(2), app.td_client.load_current_pinned_message(chat_id)).await; + let _ = timeout( + Duration::from_secs(2), + app.td_client.load_current_pinned_message(chat_id), + ) + .await; // Загружаем черновик app.load_draft(); app.status_message = None; @@ -460,8 +518,6 @@ pub async fn handle(app: &mut App, key: KeyEvent) { return; } - - // Enter - открыть чат, отправить сообщение или редактировать if key.code == KeyCode::Enter { if app.selected_chat_id.is_some() { @@ -488,10 +544,20 @@ pub async fn handle(app: &mut App, key: KeyEvent) { app.cursor_position = 0; app.editing_message_id = None; - match timeout(Duration::from_secs(5), app.td_client.edit_message(chat_id, msg_id, text)).await { + match timeout( + Duration::from_secs(5), + app.td_client.edit_message(chat_id, msg_id, text), + ) + .await + { Ok(Ok(edited_msg)) => { // Обновляем сообщение в списке - if let Some(msg) = app.td_client.current_chat_messages.iter_mut().find(|m| m.id == msg_id) { + if let Some(msg) = app + .td_client + .current_chat_messages + .iter_mut() + .find(|m| m.id == msg_id) + { msg.content = edited_msg.content; msg.entities = edited_msg.entities; msg.edit_date = edited_msg.edit_date; @@ -521,9 +587,17 @@ pub async fn handle(app: &mut App, key: KeyEvent) { app.last_typing_sent = None; // Отменяем typing status - app.td_client.send_chat_action(chat_id, ChatAction::Cancel).await; + app.td_client + .send_chat_action(chat_id, ChatAction::Cancel) + .await; - match timeout(Duration::from_secs(5), app.td_client.send_message(chat_id, text, reply_to_id, reply_info)).await { + match timeout( + Duration::from_secs(5), + app.td_client + .send_message(chat_id, text, reply_to_id, reply_info), + ) + .await + { Ok(Ok(sent_msg)) => { // Добавляем отправленное сообщение в список (с лимитом) app.td_client.push_message(sent_msg); @@ -549,12 +623,25 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(chat_id) = app.get_selected_chat_id() { app.status_message = Some("Загрузка сообщений...".to_string()); app.message_scroll_offset = 0; - match timeout(Duration::from_secs(10), app.td_client.get_chat_history(chat_id, 100)).await { + match timeout( + Duration::from_secs(10), + app.td_client.get_chat_history(chat_id, 100), + ) + .await + { Ok(Ok(_)) => { // Загружаем недостающие reply info - let _ = timeout(Duration::from_secs(5), app.td_client.fetch_missing_reply_info()).await; + let _ = timeout( + Duration::from_secs(5), + app.td_client.fetch_missing_reply_info(), + ) + .await; // Загружаем последнее закреплённое сообщение - let _ = timeout(Duration::from_secs(2), app.td_client.load_current_pinned_message(chat_id)).await; + let _ = timeout( + Duration::from_secs(2), + app.td_client.load_current_pinned_message(chat_id), + ) + .await; // Загружаем черновик app.load_draft(); app.status_message = None; @@ -593,7 +680,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) { let _ = app.td_client.set_draft_message(chat_id, draft_text).await; } else if app.message_input.is_empty() { // Очищаем черновик если инпут пустой - 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(); @@ -616,7 +706,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) { KeyCode::Char('d') | KeyCode::Char('в') | KeyCode::Delete => { // Показать модалку подтверждения удаления if let Some(msg) = app.get_selected_message() { - let can_delete = msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users; + let can_delete = + msg.can_be_deleted_only_for_self || msg.can_be_deleted_for_all_users; if can_delete { app.confirm_delete_message_id = Some(msg.id); } @@ -649,18 +740,22 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if let Some(msg) = app.get_selected_message() { let chat_id = app.selected_chat_id.unwrap(); let message_id = msg.id; - + app.status_message = Some("Загрузка реакций...".to_string()); app.needs_redraw = true; - + // Запрашиваем доступные реакции match timeout( Duration::from_secs(5), - app.td_client.get_message_available_reactions(chat_id, message_id) - ).await { + app.td_client + .get_message_available_reactions(chat_id, message_id), + ) + .await + { Ok(Ok(reactions)) => { if reactions.is_empty() { - app.error_message = Some("Реакции недоступны для этого сообщения".to_string()); + app.error_message = + Some("Реакции недоступны для этого сообщения".to_string()); app.status_message = None; app.needs_redraw = true; } else { @@ -691,7 +786,8 @@ pub async fn handle(app: &mut App, key: KeyEvent) { if key.code == KeyCode::Char('u') && has_ctrl { if let Some(chat_id) = app.selected_chat_id { app.status_message = Some("Загрузка профиля...".to_string()); - match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await { + match timeout(Duration::from_secs(5), app.td_client.get_profile_info(chat_id)).await + { Ok(Ok(profile)) => { app.profile_info = Some(profile); app.enter_profile_mode(); @@ -756,12 +852,15 @@ pub async fn handle(app: &mut App, key: KeyEvent) { app.cursor_position += 1; // Отправляем typing status с throttling (не чаще 1 раза в 5 сек) - let should_send_typing = app.last_typing_sent + let should_send_typing = app + .last_typing_sent .map(|t| t.elapsed().as_secs() >= 5) .unwrap_or(true); if should_send_typing { if let Some(chat_id) = app.get_selected_chat_id() { - app.td_client.send_chat_action(chat_id, ChatAction::Typing).await; + app.td_client + .send_chat_action(chat_id, ChatAction::Typing) + .await; app.last_typing_sent = Some(Instant::now()); } } @@ -804,18 +903,29 @@ pub async fn handle(app: &mut App, key: KeyEvent) { // Проверяем, нужно ли подгрузить старые сообщения if !app.td_client.current_chat_messages.is_empty() { - let oldest_msg_id = app.td_client.current_chat_messages.first().map(|m| m.id).unwrap_or(0); + let oldest_msg_id = app + .td_client + .current_chat_messages + .first() + .map(|m| m.id) + .unwrap_or(0); if let Some(chat_id) = app.get_selected_chat_id() { // Подгружаем больше сообщений если скролл близко к верху - if app.message_scroll_offset > app.td_client.current_chat_messages.len().saturating_sub(10) { + if app.message_scroll_offset + > app.td_client.current_chat_messages.len().saturating_sub(10) + { if let Ok(Ok(older)) = timeout( Duration::from_secs(3), - app.td_client.load_older_messages(chat_id, oldest_msg_id, 20) - ).await { + app.td_client + .load_older_messages(chat_id, oldest_msg_id, 20), + ) + .await + { if !older.is_empty() { // Добавляем старые сообщения в начало let mut new_messages = older; - new_messages.extend(app.td_client.current_chat_messages.drain(..)); + new_messages + .extend(app.td_client.current_chat_messages.drain(..)); app.td_client.current_chat_messages = new_messages; } } @@ -848,7 +958,11 @@ pub async fn handle(app: &mut App, key: KeyEvent) { app.selected_folder_id = Some(folder_id); // Загружаем чаты папки app.status_message = Some("Загрузка чатов папки...".to_string()); - let _ = timeout(Duration::from_secs(5), app.td_client.load_folder_chats(folder_id, 50)).await; + let _ = timeout( + Duration::from_secs(5), + app.td_client.load_folder_chats(folder_id, 50), + ) + .await; app.status_message = None; } } @@ -862,73 +976,76 @@ pub async fn handle(app: &mut App, key: KeyEvent) { /// Подсчёт количества доступных действий в профиле fn get_available_actions_count(profile: &crate::tdlib::ProfileInfo) -> usize { let mut count = 0; - + if profile.username.is_some() { count += 1; // Открыть в браузере } - + count += 1; // Скопировать ID - + if profile.is_group { count += 1; // Покинуть группу } - + count } /// Копирует текст в системный буфер обмена fn copy_to_clipboard(text: &str) -> Result<(), String> { use arboard::Clipboard; - - let mut clipboard = Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?; - clipboard.set_text(text).map_err(|e| format!("Не удалось скопировать: {}", e))?; - + + let mut clipboard = + Clipboard::new().map_err(|e| format!("Не удалось инициализировать буфер обмена: {}", e))?; + clipboard + .set_text(text) + .map_err(|e| format!("Не удалось скопировать: {}", e))?; + Ok(()) } /// Форматирует сообщение для копирования с контекстом fn format_message_for_clipboard(msg: &crate::tdlib::client::MessageInfo) -> String { let mut result = String::new(); - + // Добавляем forward контекст если есть if let Some(forward) = &msg.forward_from { result.push_str(&format!("↪ Переслано от {}\n", forward.sender_name)); } - + // Добавляем reply контекст если есть if let Some(reply) = &msg.reply_to { result.push_str(&format!("┌ {}: {}\n", reply.sender_name, reply.text)); } - + // Добавляем основной текст с markdown форматированием result.push_str(&convert_entities_to_markdown(&msg.content, &msg.entities)); - + result } /// Конвертирует текст с entities в markdown fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEntity]) -> String { use tdlib_rs::enums::TextEntityType; - + if entities.is_empty() { return text.to_string(); } - + // Создаём вектор символов для работы с unicode let chars: Vec = text.chars().collect(); let mut result = String::new(); let mut i = 0; - + while i < chars.len() { // Ищем entity, который начинается в текущей позиции let mut entity_found = false; - + for entity in entities { if entity.offset as usize == i { entity_found = true; let end = (entity.offset + entity.length) as usize; let entity_text: String = chars[i..end.min(chars.len())].iter().collect(); - + // Применяем форматирование в зависимости от типа let formatted = match &entity.r#type { TextEntityType::Bold => format!("**{}**", entity_text), @@ -948,18 +1065,18 @@ fn convert_entities_to_markdown(text: &str, entities: &[tdlib_rs::types::TextEnt TextEntityType::Spoiler => format!("||{}||", entity_text), _ => entity_text, }; - + result.push_str(&formatted); i = end; break; } } - + if !entity_found { result.push(chars[i]); i += 1; } } - + result } diff --git a/src/main.rs b/src/main.rs index 3f595e1..88d3e67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,11 +46,7 @@ async fn main() -> Result<(), io::Error> { // Restore terminal disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; + execute!(terminal.backend_mut(), LeaveAlternateScreen, DisableMouseCapture)?; terminal.show_cursor()?; if let Err(err) = res { @@ -91,20 +87,20 @@ async fn run_app( tokio::spawn(async move { let _ = tdlib_rs::functions::set_tdlib_parameters( - false, // use_test_dc - "tdlib_data".to_string(), // 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 + "tdlib_data".to_string(), // 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 - "Desktop".to_string(), // device_model - "".to_string(), // system_version - env!("CARGO_PKG_VERSION").to_string(), // application_version + "en".to_string(), // system_language_code + "Desktop".to_string(), // device_model + "".to_string(), // system_version + env!("CARGO_PKG_VERSION").to_string(), // application_version client_id, ) .await; @@ -156,7 +152,9 @@ async fn run_app( match event::read()? { Event::Key(key) => { // Global quit command - if key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL) { + if key.code == KeyCode::Char('c') + && key.modifiers.contains(KeyModifiers::CONTROL) + { // Graceful shutdown should_stop.store(true, Ordering::Relaxed); @@ -164,10 +162,7 @@ async fn run_app( let _ = tdlib_rs::functions::close(app.td_client.client_id()).await; // Ждём завершения polling задачи (с таймаутом) - let _ = tokio::time::timeout( - Duration::from_secs(2), - polling_handle - ).await; + let _ = tokio::time::timeout(Duration::from_secs(2), polling_handle).await; return Ok(()); } diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs index 2acbce0..08ea32f 100644 --- a/src/tdlib/client.rs +++ b/src/tdlib/client.rs @@ -1,7 +1,10 @@ -use std::env; use std::collections::HashMap; +use std::env; use std::time::Instant; -use tdlib_rs::enums::{AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, MessageSender, SearchMessagesFilter, Update, User, UserStatus}; +use tdlib_rs::enums::{ + AuthorizationState, ChatAction, ChatList, ChatType, ConnectionState, MessageContent, + MessageSender, SearchMessagesFilter, Update, User, UserStatus, +}; use tdlib_rs::types::TextEntity; /// Максимальный размер кэшей пользователей @@ -311,7 +314,11 @@ impl TdClient { /// Если сообщение с таким id уже есть — заменяет его (сохраняя reply_to) pub fn push_message(&mut self, msg: MessageInfo) { // Проверяем, есть ли уже сообщение с таким id - if let Some(idx) = self.current_chat_messages.iter().position(|m| m.id == msg.id) { + if let Some(idx) = self + .current_chat_messages + .iter() + .position(|m| m.id == msg.id) + { // Если новое сообщение имеет reply_to, или старое не имеет — заменяем if msg.reply_to.is_some() || self.current_chat_messages[idx].reply_to.is_none() { self.current_chat_messages[idx] = msg; @@ -350,7 +357,8 @@ impl TdClient { /// Например: "Вася печатает..." pub fn get_typing_text(&self) -> Option { self.typing_status.as_ref().map(|(user_id, action, _)| { - let name = self.user_names + let name = self + .user_names .peek(user_id) .cloned() .unwrap_or_else(|| "Кто-то".to_string()); @@ -361,20 +369,20 @@ impl TdClient { /// Инициализация TDLib с параметрами pub async fn init(&mut self) -> Result<(), String> { let result = functions::set_tdlib_parameters( - false, // use_test_dc - "tdlib_data".to_string(), // 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 - self.api_id, // api_id - self.api_hash.clone(), // api_hash - "en".to_string(), // system_language_code - "Desktop".to_string(), // device_model - "".to_string(), // system_version - env!("CARGO_PKG_VERSION").to_string(), // application_version + false, // use_test_dc + "tdlib_data".to_string(), // 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 + self.api_id, // api_id + self.api_hash.clone(), // api_hash + "en".to_string(), // system_language_code + "Desktop".to_string(), // device_model + "".to_string(), // system_version + env!("CARGO_PKG_VERSION").to_string(), // application_version self.client_id, ) .await; @@ -457,7 +465,9 @@ impl TdClient { if update.position.order == 0 { // Чат больше не в Main (перемещён в архив и т.д.) self.chats.retain(|c| c.id != update.chat_id); - } else if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { + } else if let Some(chat) = + self.chats.iter_mut().find(|c| c.id == update.chat_id) + { // Обновляем позицию существующего чата chat.order = update.position.order; chat.is_pinned = update.position.is_pinned; @@ -493,7 +503,10 @@ impl TdClient { let is_incoming = !msg_info.is_outgoing; // Проверяем, есть ли уже сообщение с таким id - let existing_idx = self.current_chat_messages.iter().position(|m| m.id == msg_info.id); + let existing_idx = self + .current_chat_messages + .iter() + .position(|m| m.id == msg_info.id); match existing_idx { Some(idx) => { @@ -505,8 +518,10 @@ impl TdClient { // но сохраняем reply_to (добавленный при отправке) let existing = &mut self.current_chat_messages[idx]; existing.can_be_edited = msg_info.can_be_edited; - existing.can_be_deleted_only_for_self = msg_info.can_be_deleted_only_for_self; - existing.can_be_deleted_for_all_users = msg_info.can_be_deleted_for_all_users; + existing.can_be_deleted_only_for_self = + msg_info.can_be_deleted_only_for_self; + existing.can_be_deleted_for_all_users = + msg_info.can_be_deleted_for_all_users; existing.is_read = msg_info.is_read; } } @@ -529,9 +544,8 @@ impl TdClient { if user.first_name.is_empty() && user.last_name.is_empty() { // Удаляем чаты с этим пользователем из списка let user_id = user.id; - self.chats.retain(|c| { - self.chat_user_ids.get(&c.id) != Some(&user_id) - }); + self.chats + .retain(|c| self.chat_user_ids.get(&c.id) != Some(&user_id)); return; } @@ -550,7 +564,8 @@ impl TdClient { // Обновляем username в чатах, связанных с этим пользователем for (&chat_id, &user_id) in &self.chat_user_ids.clone() { if user_id == user.id { - if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) { + if let Some(chat) = self.chats.iter_mut().find(|c| c.id == chat_id) + { chat.username = Some(format!("@{}", username)); } } @@ -564,10 +579,7 @@ impl TdClient { self.folders = update .chat_folders .into_iter() - .map(|f| FolderInfo { - id: f.id, - name: f.title, - }) + .map(|f| FolderInfo { id: f.id, name: f.title }) .collect(); self.main_chat_list_position = update.main_chat_list_position; } @@ -607,14 +619,26 @@ impl TdClient { let action_text = match update.action { ChatAction::Typing => Some("печатает...".to_string()), ChatAction::RecordingVideo => Some("записывает видео...".to_string()), - ChatAction::UploadingVideo(_) => Some("отправляет видео...".to_string()), - ChatAction::RecordingVoiceNote => Some("записывает голосовое...".to_string()), - ChatAction::UploadingVoiceNote(_) => Some("отправляет голосовое...".to_string()), + ChatAction::UploadingVideo(_) => { + Some("отправляет видео...".to_string()) + } + ChatAction::RecordingVoiceNote => { + Some("записывает голосовое...".to_string()) + } + ChatAction::UploadingVoiceNote(_) => { + Some("отправляет голосовое...".to_string()) + } ChatAction::UploadingPhoto(_) => Some("отправляет фото...".to_string()), - ChatAction::UploadingDocument(_) => Some("отправляет файл...".to_string()), + ChatAction::UploadingDocument(_) => { + Some("отправляет файл...".to_string()) + } ChatAction::ChoosingSticker => Some("выбирает стикер...".to_string()), - ChatAction::RecordingVideoNote => Some("записывает видеосообщение...".to_string()), - ChatAction::UploadingVideoNote(_) => Some("отправляет видеосообщение...".to_string()), + ChatAction::RecordingVideoNote => { + Some("записывает видеосообщение...".to_string()) + } + ChatAction::UploadingVideoNote(_) => { + Some("отправляет видеосообщение...".to_string()) + } ChatAction::Cancel => None, // Отмена — сбрасываем статус _ => None, }; @@ -633,7 +657,9 @@ impl TdClient { if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { chat.draft_text = update.draft_message.as_ref().and_then(|draft| { // Извлекаем текст из InputMessageText - if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) = &draft.input_message_text { + if let tdlib_rs::enums::InputMessageContent::InputMessageText(text_msg) = + &draft.input_message_text + { Some(text_msg.text.text.clone()) } else { None @@ -644,7 +670,11 @@ impl TdClient { Update::MessageInteractionInfo(update) => { // Обновляем реакции в текущем открытом чате if Some(update.chat_id) == self.current_chat_id { - if let Some(msg) = self.current_chat_messages.iter_mut().find(|m| m.id == update.message_id) { + if let Some(msg) = self + .current_chat_messages + .iter_mut() + .find(|m| m.id == update.message_id) + { // Извлекаем реакции из interaction_info msg.reactions = update .interaction_info @@ -656,8 +686,12 @@ impl TdClient { .iter() .filter_map(|reaction| { let emoji = match &reaction.r#type { - tdlib_rs::enums::ReactionType::Emoji(e) => e.emoji.clone(), - tdlib_rs::enums::ReactionType::CustomEmoji(_) => return None, + tdlib_rs::enums::ReactionType::Emoji(e) => { + e.emoji.clone() + } + tdlib_rs::enums::ReactionType::CustomEmoji(_) => { + return None + } }; Some(ReactionInfo { @@ -697,9 +731,10 @@ impl TdClient { } // Ищем позицию в Main списке (если есть) - let main_position = td_chat.positions.iter().find(|pos| { - matches!(pos.list, ChatList::Main) - }); + let main_position = td_chat + .positions + .iter() + .find(|pos| matches!(pos.list, ChatList::Main)); // Получаем order и is_pinned из позиции, или используем значения по умолчанию let (order, is_pinned) = main_position @@ -716,7 +751,9 @@ impl TdClient { let username = match &td_chat.r#type { ChatType::Private(private) => { // Ограничиваем размер chat_user_ids - if self.chat_user_ids.len() >= MAX_CHAT_USER_IDS && !self.chat_user_ids.contains_key(&td_chat.id) { + if self.chat_user_ids.len() >= MAX_CHAT_USER_IDS + && !self.chat_user_ids.contains_key(&td_chat.id) + { // Удаляем случайную запись (первую найденную) if let Some(&key) = self.chat_user_ids.keys().next() { self.chat_user_ids.remove(&key); @@ -724,7 +761,9 @@ impl TdClient { } self.chat_user_ids.insert(td_chat.id, private.user_id); // Проверяем, есть ли уже username в кэше (peek не обновляет LRU) - self.user_usernames.peek(&private.user_id).map(|u| format!("@{}", u)) + self.user_usernames + .peek(&private.user_id) + .map(|u| format!("@{}", u)) } _ => None, }; @@ -784,7 +823,13 @@ impl TdClient { // Ограничиваем количество чатов if self.chats.len() > MAX_CHATS { // Удаляем чат с наименьшим order (наименее активный) - if let Some(min_idx) = self.chats.iter().enumerate().min_by_key(|(_, c)| c.order).map(|(i, _)| i) { + if let Some(min_idx) = self + .chats + .iter() + .enumerate() + .min_by_key(|(_, c)| c.order) + .map(|(i, _)| i) + { self.chats.remove(min_idx); } } @@ -891,11 +936,7 @@ impl TdClient { .unwrap_or_default() }; - Some(ReplyInfo { - message_id: reply.message_id, - sender_name, - text, - }) + Some(ReplyInfo { message_id: reply.message_id, sender_name, text }) } _ => None, } @@ -905,10 +946,7 @@ impl TdClient { fn extract_forward_info(&self, message: &TdMessage) -> Option { message.forward_info.as_ref().map(|info| { let sender_name = self.get_origin_sender_name(&info.origin); - ForwardInfo { - sender_name, - date: info.date, - } + ForwardInfo { sender_name, date: info.date } }) } @@ -944,24 +982,24 @@ impl TdClient { fn get_origin_sender_name(&self, origin: &tdlib_rs::enums::MessageOrigin) -> String { use tdlib_rs::enums::MessageOrigin; match origin { - MessageOrigin::User(u) => { - self.user_names.peek(&u.sender_user_id) - .cloned() - .unwrap_or_else(|| format!("User_{}", u.sender_user_id)) - } - MessageOrigin::Chat(c) => { - self.chats.iter() - .find(|chat| chat.id == c.sender_chat_id) - .map(|chat| chat.title.clone()) - .unwrap_or_else(|| "Чат".to_string()) - } + MessageOrigin::User(u) => self + .user_names + .peek(&u.sender_user_id) + .cloned() + .unwrap_or_else(|| format!("User_{}", u.sender_user_id)), + MessageOrigin::Chat(c) => self + .chats + .iter() + .find(|chat| chat.id == c.sender_chat_id) + .map(|chat| chat.title.clone()) + .unwrap_or_else(|| "Чат".to_string()), MessageOrigin::HiddenUser(h) => h.sender_name.clone(), - MessageOrigin::Channel(c) => { - self.chats.iter() - .find(|chat| chat.id == c.chat_id) - .map(|chat| chat.title.clone()) - .unwrap_or_else(|| "Канал".to_string()) - } + MessageOrigin::Channel(c) => self + .chats + .iter() + .find(|chat| chat.id == c.chat_id) + .map(|chat| chat.title.clone()) + .unwrap_or_else(|| "Канал".to_string()), } } @@ -1032,19 +1070,17 @@ impl TdClient { functions::get_message(chat_id, msg_id, self.client_id).await { let sender_name = match &msg.sender_id { - tdlib_rs::enums::MessageSender::User(user) => { - self.user_names - .get(&user.user_id) - .cloned() - .unwrap_or_else(|| format!("User_{}", user.user_id)) - } - tdlib_rs::enums::MessageSender::Chat(chat) => { - self.chats - .iter() - .find(|c| c.id == chat.chat_id) - .map(|c| c.title.clone()) - .unwrap_or_else(|| "Чат".to_string()) - } + tdlib_rs::enums::MessageSender::User(user) => self + .user_names + .get(&user.user_id) + .cloned() + .unwrap_or_else(|| format!("User_{}", user.user_id)), + tdlib_rs::enums::MessageSender::Chat(chat) => self + .chats + .iter() + .find(|c| c.id == chat.chat_id) + .map(|c| c.title.clone()) + .unwrap_or_else(|| "Чат".to_string()), }; let (content, _) = extract_message_text_static(&msg); reply_cache.insert(msg_id, (sender_name, content)); @@ -1068,12 +1104,7 @@ impl TdClient { /// Отправка номера телефона pub async fn send_phone_number(&mut self, phone: String) -> Result<(), String> { - let result = functions::set_authentication_phone_number( - phone, - None, - self.client_id, - ) - .await; + let result = functions::set_authentication_phone_number(phone, None, self.client_id).await; match result { Ok(_) => Ok(()), @@ -1103,12 +1134,7 @@ impl TdClient { /// Загрузка списка чатов pub async fn load_chats(&mut self, limit: i32) -> Result<(), String> { - let result = functions::load_chats( - Some(ChatList::Main), - limit, - self.client_id, - ) - .await; + let result = functions::load_chats(Some(ChatList::Main), limit, self.client_id).await; match result { Ok(_) => Ok(()), @@ -1118,16 +1144,10 @@ impl TdClient { /// Загрузка чатов для конкретной папки pub async fn load_folder_chats(&mut self, folder_id: i32, limit: i32) -> Result<(), String> { - let chat_list = ChatList::Folder(tdlib_rs::types::ChatListFolder { - chat_folder_id: folder_id, - }); + let chat_list = + ChatList::Folder(tdlib_rs::types::ChatListFolder { chat_folder_id: folder_id }); - let result = functions::load_chats( - Some(chat_list), - limit, - self.client_id, - ) - .await; + let result = functions::load_chats(Some(chat_list), limit, self.client_id).await; match result { Ok(_) => Ok(()), @@ -1155,9 +1175,9 @@ impl TdClient { let result = functions::get_chat_history( chat_id, from_message_id, - 0, // offset + 0, // offset limit, - false, // only_local - загружаем с сервера! + false, // only_local - загружаем с сервера! self.client_id, ) .await; @@ -1209,8 +1229,8 @@ impl TdClient { let _ = functions::view_messages( chat_id, message_ids, - None, // source - true, // force_read + None, // source + true, // force_read self.client_id, ) .await; @@ -1223,14 +1243,14 @@ impl TdClient { pub async fn get_pinned_messages(&mut self, chat_id: i64) -> Result, String> { let result = functions::search_chat_messages( chat_id, - "".to_string(), // query - None, // sender_id - 0, // from_message_id - 0, // offset - 100, // limit - Some(SearchMessagesFilter::Pinned), // filter - 0, // message_thread_id - 0, // saved_messages_topic_id + "".to_string(), // query + None, // sender_id + 0, // from_message_id + 0, // offset + 100, // limit + Some(SearchMessagesFilter::Pinned), // filter + 0, // message_thread_id + 0, // saved_messages_topic_id self.client_id, ) .await; @@ -1279,7 +1299,11 @@ impl TdClient { } /// Поиск сообщений в чате по тексту - pub async fn search_messages(&mut self, chat_id: i64, query: &str) -> Result, String> { + pub async fn search_messages( + &mut self, + chat_id: i64, + query: &str, + ) -> Result, String> { if query.trim().is_empty() { return Ok(Vec::new()); } @@ -1287,13 +1311,13 @@ impl TdClient { let result = functions::search_chat_messages( chat_id, query.to_string(), - None, // sender_id - 0, // from_message_id - 0, // offset - 50, // limit - None, // filter (no filter = search by text) - 0, // message_thread_id - 0, // saved_messages_topic_id + None, // sender_id + 0, // from_message_id + 0, // offset + 50, // limit + None, // filter (no filter = search by text) + 0, // message_thread_id + 0, // saved_messages_topic_id self.client_id, ) .await; @@ -1359,8 +1383,12 @@ impl TdClient { profile.online_status = Some(match user.status { tdlib_rs::enums::UserStatus::Online(_) => "Онлайн".to_string(), tdlib_rs::enums::UserStatus::Recently(_) => "Был(а) недавно".to_string(), - tdlib_rs::enums::UserStatus::LastWeek(_) => "Был(а) на этой неделе".to_string(), - tdlib_rs::enums::UserStatus::LastMonth(_) => "Был(а) в этом месяце".to_string(), + tdlib_rs::enums::UserStatus::LastWeek(_) => { + "Был(а) на этой неделе".to_string() + } + tdlib_rs::enums::UserStatus::LastMonth(_) => { + "Был(а) в этом месяце".to_string() + } tdlib_rs::enums::UserStatus::Offline(offline) => { crate::utils::format_was_online(offline.was_online) } @@ -1369,8 +1397,10 @@ impl TdClient { } // Bio (getUserFullInfo) - let full_info_result = functions::get_user_full_info(private_chat.user_id, self.client_id).await; - if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = full_info_result { + let full_info_result = + functions::get_user_full_info(private_chat.user_id, self.client_id).await; + if let Ok(tdlib_rs::enums::UserFullInfo::UserFullInfo(full_info)) = full_info_result + { if let Some(bio_obj) = full_info.bio { profile.bio = Some(bio_obj.text); } @@ -1381,14 +1411,21 @@ impl TdClient { profile.is_group = true; // Получаем информацию о группе - let group_result = functions::get_basic_group(basic_group.basic_group_id, self.client_id).await; + let group_result = + functions::get_basic_group(basic_group.basic_group_id, self.client_id).await; if let Ok(tdlib_rs::enums::BasicGroup::BasicGroup(group)) = group_result { profile.member_count = Some(group.member_count); } // Полная информация о группе - let full_info_result = functions::get_basic_group_full_info(basic_group.basic_group_id, self.client_id).await; - if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) = full_info_result { + let full_info_result = functions::get_basic_group_full_info( + basic_group.basic_group_id, + self.client_id, + ) + .await; + if let Ok(tdlib_rs::enums::BasicGroupFullInfo::BasicGroupFullInfo(full_info)) = + full_info_result + { if !full_info.description.is_empty() { profile.description = Some(full_info.description); } @@ -1399,9 +1436,14 @@ impl TdClient { } ChatType::Supergroup(supergroup) => { // Получаем информацию о супергруппе - let sg_result = functions::get_supergroup(supergroup.supergroup_id, self.client_id).await; + let sg_result = + functions::get_supergroup(supergroup.supergroup_id, self.client_id).await; if let Ok(tdlib_rs::enums::Supergroup::Supergroup(sg)) = sg_result { - profile.chat_type = if sg.is_channel { "Канал".to_string() } else { "Супергруппа".to_string() }; + profile.chat_type = if sg.is_channel { + "Канал".to_string() + } else { + "Супергруппа".to_string() + }; profile.is_group = !sg.is_channel; profile.member_count = Some(sg.member_count); @@ -1414,8 +1456,12 @@ impl TdClient { } // Полная информация о супергруппе - let full_info_result = functions::get_supergroup_full_info(supergroup.supergroup_id, self.client_id).await; - if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) = full_info_result { + let full_info_result = + functions::get_supergroup_full_info(supergroup.supergroup_id, self.client_id) + .await; + if let Ok(tdlib_rs::enums::SupergroupFullInfo::SupergroupFullInfo(full_info)) = + full_info_result + { if !full_info.description.is_empty() { profile.description = Some(full_info.description); } @@ -1451,9 +1497,9 @@ impl TdClient { let result = functions::get_chat_history( chat_id, from_message_id, - 0, // offset + 0, // offset limit, - false, // only_local + false, // only_local self.client_id, ) .await; @@ -1497,11 +1543,9 @@ impl TdClient { /// Получение моего user_id pub async fn get_me(&self) -> Result { match functions::get_me(self.client_id).await { - Ok(user) => { - match user { - User::User(u) => Ok(u.id), - } - } + Ok(user) => match user { + User::User(u) => Ok(u.id), + }, Err(e) => Err(format!("Ошибка получения профиля: {:?}", e)), } } @@ -1513,30 +1557,37 @@ impl TdClient { 0, // message_thread_id Some(action), self.client_id, - ).await; + ) + .await; } /// Отправка текстового сообщения с поддержкой Markdown и reply - pub async fn send_message(&self, chat_id: i64, text: String, reply_to_message_id: Option, reply_info: Option) -> Result { - use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown, InputMessageReplyToMessage}; - use tdlib_rs::enums::{InputMessageContent, TextParseMode, InputMessageReplyTo}; + pub async fn send_message( + &self, + chat_id: i64, + text: String, + reply_to_message_id: Option, + reply_info: Option, + ) -> Result { + use tdlib_rs::enums::{InputMessageContent, InputMessageReplyTo, TextParseMode}; + use tdlib_rs::types::{ + FormattedText, InputMessageReplyToMessage, InputMessageText, TextParseModeMarkdown, + }; // Парсим markdown в тексте let formatted_text = match functions::parse_text_entities( text.clone(), TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }), self.client_id, - ).await { - Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => FormattedText { - text: ft.text, - entities: ft.entities, - }, + ) + .await + { + Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => { + FormattedText { text: ft.text, entities: ft.entities } + } Err(_) => { // Если парсинг не удался, отправляем как plain text - FormattedText { - text: text.clone(), - entities: vec![], - } + FormattedText { text: text.clone(), entities: vec![] } } }; @@ -1558,9 +1609,9 @@ impl TdClient { let result = functions::send_message( chat_id, - 0, // message_thread_id + 0, // message_thread_id reply_to, - None, // options + None, // options content, self.client_id, ) @@ -1592,7 +1643,6 @@ impl TdClient { } } - /// Получить доступные реакции для сообщения pub async fn get_message_available_reactions( &mut self, @@ -1653,9 +1703,9 @@ impl TdClient { message_id: i64, emoji: String, ) -> Result<(), String> { + use tdlib_rs::enums::ReactionType; use tdlib_rs::functions; use tdlib_rs::types::ReactionTypeEmoji; - use tdlib_rs::enums::ReactionType; let reaction_type = ReactionType::Emoji(ReactionTypeEmoji { emoji }); @@ -1678,17 +1728,18 @@ impl TdClient { /// Редактирование текстового сообщения с поддержкой Markdown /// Устанавливает черновик для чата через TDLib API pub async fn set_draft_message(&self, chat_id: i64, text: String) -> Result<(), String> { - use tdlib_rs::types::{FormattedText, InputMessageText, DraftMessage}; use tdlib_rs::enums::InputMessageContent; + use tdlib_rs::types::{DraftMessage, FormattedText, InputMessageText}; if text.is_empty() { // Очищаем черновик let result = functions::set_chat_draft_message( chat_id, - 0, // message_thread_id + 0, // message_thread_id None, // draft_message (None = очистить) self.client_id, - ).await; + ) + .await; match result { Ok(_) => Ok(()), @@ -1696,10 +1747,7 @@ impl TdClient { } } else { // Создаём черновик - let formatted_text = FormattedText { - text: text.clone(), - entities: vec![], - }; + let formatted_text = FormattedText { text: text.clone(), entities: vec![] }; let input_message = InputMessageContent::InputMessageText(InputMessageText { text: formatted_text, @@ -1718,7 +1766,8 @@ impl TdClient { 0, // message_thread_id Some(draft), self.client_id, - ).await; + ) + .await; match result { Ok(_) => Ok(()), @@ -1727,26 +1776,29 @@ impl TdClient { } } - pub async fn edit_message(&self, chat_id: i64, message_id: i64, text: String) -> Result { - use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown}; + pub async fn edit_message( + &self, + chat_id: i64, + message_id: i64, + text: String, + ) -> Result { use tdlib_rs::enums::{InputMessageContent, TextParseMode}; + use tdlib_rs::types::{FormattedText, InputMessageText, TextParseModeMarkdown}; // Парсим markdown в тексте let formatted_text = match functions::parse_text_entities( text.clone(), TextParseMode::Markdown(TextParseModeMarkdown { version: 2 }), self.client_id, - ).await { - Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => FormattedText { - text: ft.text, - entities: ft.entities, - }, + ) + .await + { + Ok(tdlib_rs::enums::FormattedText::FormattedText(ft)) => { + FormattedText { text: ft.text, entities: ft.entities } + } Err(_) => { // Если парсинг не удался, отправляем как plain text - FormattedText { - text: text.clone(), - entities: vec![], - } + FormattedText { text: text.clone(), entities: vec![] } } }; @@ -1756,13 +1808,8 @@ impl TdClient { clear_draft: true, }); - let result = functions::edit_message_text( - chat_id, - message_id, - content, - self.client_id, - ) - .await; + let result = + functions::edit_message_text(chat_id, message_id, content, self.client_id).await; match result { Ok(tdlib_rs::enums::Message::Message(msg)) => { @@ -1790,14 +1837,13 @@ impl TdClient { /// Удаление сообщений /// revoke = true удаляет для всех, false только для себя - pub async fn delete_messages(&self, chat_id: i64, message_ids: Vec, revoke: bool) -> Result<(), String> { - let result = functions::delete_messages( - chat_id, - message_ids, - revoke, - self.client_id, - ) - .await; + pub async fn delete_messages( + &self, + chat_id: i64, + message_ids: Vec, + revoke: bool, + ) -> Result<(), String> { + let result = functions::delete_messages(chat_id, message_ids, revoke, self.client_id).await; match result { Ok(_) => Ok(()), @@ -1806,13 +1852,18 @@ impl TdClient { } /// Пересылка сообщений - pub async fn forward_messages(&self, to_chat_id: i64, from_chat_id: i64, message_ids: Vec) -> Result<(), String> { + pub async fn forward_messages( + &self, + to_chat_id: i64, + from_chat_id: i64, + message_ids: Vec, + ) -> Result<(), String> { let result = functions::forward_messages( to_chat_id, 0, // message_thread_id from_chat_id, message_ids, - None, // options + None, // options false, // send_copy false, // remove_caption self.client_id, @@ -1832,8 +1883,8 @@ impl TdClient { let _ = functions::view_messages( chat_id, message_ids, - None, // source - true, // force_read + None, // source + true, // force_read self.client_id, ) .await; @@ -1847,7 +1898,8 @@ impl TdClient { const BATCH_SIZE: usize = 5; // Убираем дубликаты и уже загруженные - self.pending_user_ids.retain(|id| !self.user_names.contains_key(id)); + self.pending_user_ids + .retain(|id| !self.user_names.contains_key(id)); self.pending_user_ids.dedup(); // Берём последние BATCH_SIZE элементов @@ -1885,16 +1937,17 @@ impl TdClient { /// Статическая функция для извлечения текста и entities сообщения (без &self) fn extract_message_text_static(message: &TdMessage) -> (String, Vec) { 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()), MessageContent::MessagePhoto(photo) => { if photo.caption.text.is_empty() { ("[Фото]".to_string(), vec![]) } else { // Добавляем смещение для "[Фото] " к entities let prefix_len = "[Фото] ".chars().count() as i32; - let adjusted_entities: Vec = photo.caption.entities.iter() + let adjusted_entities: Vec = photo + .caption + .entities + .iter() .map(|e| TextEntity { offset: e.offset + prefix_len, length: e.length, @@ -1909,7 +1962,10 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec) ("[Видео]".to_string(), vec![]) } else { let prefix_len = "[Видео] ".chars().count() as i32; - let adjusted_entities: Vec = video.caption.entities.iter() + let adjusted_entities: Vec = video + .caption + .entities + .iter() .map(|e| TextEntity { offset: e.offset + prefix_len, length: e.length, @@ -1932,7 +1988,10 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec) ("[GIF]".to_string(), vec![]) } else { let prefix_len = "[GIF] ".chars().count() as i32; - let adjusted_entities: Vec = anim.caption.entities.iter() + let adjusted_entities: Vec = anim + .caption + .entities + .iter() .map(|e| TextEntity { offset: e.offset + prefix_len, length: e.length, @@ -1942,9 +2001,7 @@ fn extract_message_text_static(message: &TdMessage) -> (String, Vec) (format!("[GIF] {}", anim.caption.text), adjusted_entities) } } - MessageContent::MessageAudio(audio) => { - (format!("[Аудио: {}]", audio.audio.title), vec![]) - } + MessageContent::MessageAudio(audio) => (format!("[Аудио: {}]", audio.audio.title), vec![]), MessageContent::MessageCall(_) => ("[Звонок]".to_string(), vec![]), MessageContent::MessagePoll(poll) => { (format!("[Опрос: {}]", poll.poll.question.text), vec![]) diff --git a/src/tdlib/mod.rs b/src/tdlib/mod.rs index f2037b0..3a408f5 100644 --- a/src/tdlib/mod.rs +++ b/src/tdlib/mod.rs @@ -1,13 +1,13 @@ pub mod client; -pub use client::TdClient; -pub use client::UserOnlineStatus; +pub use client::ChatInfo; +pub use client::FolderInfo; +pub use client::ForwardInfo; +pub use client::MessageInfo; pub use client::NetworkState; pub use client::ProfileInfo; -pub use client::ChatInfo; -pub use client::MessageInfo; pub use client::ReactionInfo; pub use client::ReplyInfo; -pub use client::ForwardInfo; -pub use client::FolderInfo; +pub use client::TdClient; +pub use client::UserOnlineStatus; pub use tdlib_rs::enums::ChatAction; diff --git a/src/ui/auth.rs b/src/ui/auth.rs index a6fadb4..88b4da0 100644 --- a/src/ui/auth.rs +++ b/src/ui/auth.rs @@ -1,3 +1,5 @@ +use crate::app::App; +use crate::tdlib::client::AuthState; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, @@ -5,8 +7,6 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, Frame, }; -use crate::app::App; -use crate::tdlib::client::AuthState; pub fn render(f: &mut Frame, app: &App) { let area = f.area(); diff --git a/src/ui/chat_list.rs b/src/ui/chat_list.rs index 999a9e8..5bc0eca 100644 --- a/src/ui/chat_list.rs +++ b/src/ui/chat_list.rs @@ -1,11 +1,11 @@ +use crate::app::App; +use crate::tdlib::UserOnlineStatus; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, widgets::{Block, Borders, List, ListItem, Paragraph}, Frame, }; -use crate::app::App; -use crate::tdlib::UserOnlineStatus; pub fn render(f: &mut Frame, area: Rect, app: &mut App) { let chat_chunks = Layout::default() @@ -54,7 +54,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { let prefix = if is_selected { "▌" } else { " " }; - let username_text = chat.username.as_ref() + let username_text = chat + .username + .as_ref() .map(|u| format!(" {}", u)) .unwrap_or_default(); @@ -78,7 +80,18 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { String::new() }; - let content = format!("{}{}{}{}{}{}{}{}{}", prefix, status_icon, pin_icon, mute_icon, chat.title, username_text, mention_badge, draft_badge, unread_badge); + let content = format!( + "{}{}{}{}{}{}{}{}{}", + prefix, + status_icon, + pin_icon, + mute_icon, + chat.title, + username_text, + mention_badge, + draft_badge, + unread_badge + ); // Цвет: онлайн — зелёные, остальные — белые let style = match app.td_client.get_user_status_by_chat_id(chat.id) { @@ -100,13 +113,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { Block::default().borders(Borders::ALL) }; - let chats_list = List::new(items) - .block(block) - .highlight_style( - Style::default() - .add_modifier(Modifier::ITALIC) - .fg(Color::Yellow), - ); + let chats_list = List::new(items).block(block).highlight_style( + Style::default() + .add_modifier(Modifier::ITALIC) + .fg(Color::Yellow), + ); f.render_stateful_widget(chats_list, chat_chunks[1], &mut app.chat_list_state); @@ -119,8 +130,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { let formatted = format_was_online(*was_online); (formatted, Color::Gray) } - Some(UserOnlineStatus::LastWeek) => ("был(а) на этой неделе".to_string(), Color::DarkGray), - Some(UserOnlineStatus::LastMonth) => ("был(а) в этом месяце".to_string(), Color::DarkGray), + Some(UserOnlineStatus::LastWeek) => { + ("был(а) на этой неделе".to_string(), Color::DarkGray) + } + Some(UserOnlineStatus::LastMonth) => { + ("был(а) в этом месяце".to_string(), Color::DarkGray) + } Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".to_string(), Color::DarkGray), None => ("".to_string(), Color::DarkGray), // Для групп/каналов } @@ -131,14 +146,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { if let Some(chat) = filtered.get(i) { match app.td_client.get_user_status_by_chat_id(chat.id) { Some(UserOnlineStatus::Online) => ("● онлайн".to_string(), Color::Green), - Some(UserOnlineStatus::Recently) => ("был(а) недавно".to_string(), Color::Yellow), + Some(UserOnlineStatus::Recently) => { + ("был(а) недавно".to_string(), Color::Yellow) + } Some(UserOnlineStatus::Offline(was_online)) => { let formatted = format_was_online(*was_online); (formatted, Color::Gray) } - Some(UserOnlineStatus::LastWeek) => ("был(а) на этой неделе".to_string(), Color::DarkGray), - Some(UserOnlineStatus::LastMonth) => ("был(а) в этом месяце".to_string(), Color::DarkGray), - Some(UserOnlineStatus::LongTimeAgo) => ("был(а) давно".to_string(), Color::DarkGray), + Some(UserOnlineStatus::LastWeek) => { + ("был(а) на этой неделе".to_string(), Color::DarkGray) + } + Some(UserOnlineStatus::LastMonth) => { + ("был(а) в этом месяце".to_string(), Color::DarkGray) + } + Some(UserOnlineStatus::LongTimeAgo) => { + ("был(а) давно".to_string(), Color::DarkGray) + } None => ("".to_string(), Color::DarkGray), } } else { diff --git a/src/ui/footer.rs b/src/ui/footer.rs index 95a5a6a..4154254 100644 --- a/src/ui/footer.rs +++ b/src/ui/footer.rs @@ -1,11 +1,11 @@ +use crate::app::App; +use crate::tdlib::NetworkState; use ratatui::{ layout::Rect, style::{Color, Style}, widgets::Paragraph, Frame, }; -use crate::app::App; -use crate::tdlib::NetworkState; pub fn render(f: &mut Frame, area: Rect, app: &App) { // Индикатор состояния сети @@ -26,7 +26,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } else if app.selected_chat_id.is_some() { format!(" {}↑/↓: Scroll | Ctrl+U: Profile | Enter: Send | Esc: Close | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator) } else { - format!(" {}↑/↓: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ", network_indicator) + format!( + " {}↑/↓: Navigate | Enter: Open | Ctrl+S: Search | Ctrl+R: Refresh | Ctrl+C: Quit ", + network_indicator + ) }; let style = if matches!(app.td_client.network_state, NetworkState::WaitingForNetwork) { diff --git a/src/ui/loading.rs b/src/ui/loading.rs index bb1a64b..a4b8d4a 100644 --- a/src/ui/loading.rs +++ b/src/ui/loading.rs @@ -1,10 +1,10 @@ +use crate::app::App; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout}, style::{Color, Modifier, Style}, widgets::{Block, Borders, Paragraph}, Frame, }; -use crate::app::App; pub fn render(f: &mut Frame, app: &App) { let area = f.area(); @@ -18,10 +18,7 @@ pub fn render(f: &mut Frame, app: &App) { ]) .split(area); - let message = app - .status_message - .as_deref() - .unwrap_or("Загрузка..."); + let message = app.status_message.as_deref().unwrap_or("Загрузка..."); let loading = Paragraph::new(message) .style( @@ -30,11 +27,7 @@ pub fn render(f: &mut Frame, app: &App) { .add_modifier(Modifier::BOLD), ) .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::ALL) - .title(" TTUI "), - ); + .block(Block::default().borders(Borders::ALL).title(" TTUI ")); f.render_widget(loading, chunks[1]); } diff --git a/src/ui/main_screen.rs b/src/ui/main_screen.rs index 39106e9..582efc6 100644 --- a/src/ui/main_screen.rs +++ b/src/ui/main_screen.rs @@ -1,3 +1,5 @@ +use super::{chat_list, footer, messages}; +use crate::app::App; use ratatui::{ layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -5,8 +7,6 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, Frame, }; -use crate::app::App; -use super::{chat_list, messages, footer}; /// Порог ширины для компактного режима (одна панель) const COMPACT_WIDTH: u16 = 80; @@ -81,11 +81,8 @@ fn render_folders(f: &mut Frame, area: Rect, app: &App) { } let folders_line = Line::from(spans); - let folders_widget = Paragraph::new(folders_line).block( - Block::default() - .title(" TTUI ") - .borders(Borders::ALL), - ); + let folders_widget = + Paragraph::new(folders_line).block(Block::default().title(" TTUI ").borders(Borders::ALL)); f.render_widget(folders_widget, area); } diff --git a/src/ui/messages.rs b/src/ui/messages.rs index cf165e0..7317727 100644 --- a/src/ui/messages.rs +++ b/src/ui/messages.rs @@ -1,3 +1,5 @@ +use crate::app::App; +use crate::utils::{format_date, format_timestamp_with_tz, get_day}; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -5,8 +7,6 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, Frame, }; -use crate::app::App; -use crate::utils::{format_timestamp_with_tz, format_date, get_day}; use tdlib_rs::enums::TextEntityType; use tdlib_rs::types::TextEntity; @@ -58,9 +58,16 @@ impl CharStyle { } /// Преобразует текст с entities в вектор стилизованных Span (owned) -fn format_text_with_entities(text: &str, entities: &[TextEntity], base_color: Color) -> Vec> { +fn format_text_with_entities( + text: &str, + entities: &[TextEntity], + base_color: Color, +) -> Vec> { if entities.is_empty() { - return vec![Span::styled(text.to_string(), Style::default().fg(base_color))]; + return vec![Span::styled( + text.to_string(), + Style::default().fg(base_color), + )]; } // Создаём массив стилей для каждого символа @@ -82,9 +89,13 @@ fn format_text_with_entities(text: &str, entities: &[TextEntity], base_color: Co char_styles[i].code = true } TextEntityType::Spoiler => char_styles[i].spoiler = true, - TextEntityType::Url | TextEntityType::TextUrl(_) | TextEntityType::EmailAddress + TextEntityType::Url + | TextEntityType::TextUrl(_) + | TextEntityType::EmailAddress | TextEntityType::PhoneNumber => char_styles[i].url = true, - TextEntityType::Mention | TextEntityType::MentionName(_) => char_styles[i].mention = true, + TextEntityType::Mention | TextEntityType::MentionName(_) => { + char_styles[i].mention = true + } _ => {} } } @@ -144,7 +155,12 @@ fn styles_equal(a: &CharStyle, b: &CharStyle) -> bool { } /// Рендерит текст инпута с блочным курсором -fn render_input_with_cursor(prefix: &str, text: &str, cursor_pos: usize, color: Color) -> Line<'static> { +fn render_input_with_cursor( + prefix: &str, + text: &str, + cursor_pos: usize, + color: Color, +) -> Line<'static> { let chars: Vec = text.chars().collect(); let mut spans: Vec = vec![Span::raw(prefix.to_string())]; @@ -186,10 +202,7 @@ struct WrappedLine { /// Возвращает строки с информацией о позициях для корректного применения entities fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec { if max_width == 0 { - return vec![WrappedLine { - text: text.to_string(), - start_offset: 0, - }]; + return vec![WrappedLine { text: text.to_string(), start_offset: 0 }]; } let mut result = Vec::new(); @@ -263,10 +276,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec { } if result.is_empty() { - result.push(WrappedLine { - text: String::new(), - start_offset: 0, - }); + result.push(WrappedLine { text: String::new(), start_offset: 0 }); } result @@ -368,24 +378,28 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { }; // Chat header с typing status - let typing_action = app.td_client.typing_status.as_ref().map(|(_, action, _)| action.clone()); + let typing_action = app + .td_client + .typing_status + .as_ref() + .map(|(_, action, _)| action.clone()); let header_line = if let Some(action) = typing_action { // Показываем typing status: "👤 Имя @username печатает..." - let mut spans = vec![ - Span::styled( - format!("👤 {}", chat.title), - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), - ), - ]; + let mut spans = vec![Span::styled( + format!("👤 {}", chat.title), + Style::default() + .fg(Color::Cyan) + .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), - Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::ITALIC), )); Line::from(spans) } else { @@ -396,17 +410,22 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { }; Line::from(Span::styled( header_text, - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), )) }; - let header = Paragraph::new(header_line) - .block(Block::default().borders(Borders::ALL)); + let header = Paragraph::new(header_line).block(Block::default().borders(Borders::ALL)); f.render_widget(header, message_chunks[0]); // Pinned bar (если есть закреплённое сообщение) if let Some(pinned_msg) = &app.td_client.current_pinned_message { let pinned_preview: String = pinned_msg.content.chars().take(40).collect(); - let ellipsis = if pinned_msg.content.chars().count() > 40 { "..." } else { "" }; + let ellipsis = if pinned_msg.content.chars().count() > 40 { + "..." + } else { + "" + }; let pinned_datetime = crate::utils::format_datetime(pinned_msg.date); let pinned_text = format!("📌 {} {}{}", pinned_datetime, pinned_preview, ellipsis); let pinned_hint = "Ctrl+P"; @@ -421,8 +440,8 @@ pub fn render(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, message_chunks[1]); } @@ -484,9 +503,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } let sender_style = if msg.is_outgoing { - Style::default().fg(Color::Green).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD) } else { - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD) }; if msg.is_outgoing { @@ -540,16 +563,21 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { ])); } else { // Forward слева для входящих - lines.push(Line::from(vec![ - Span::styled(forward_line, Style::default().fg(Color::Magenta)), - ])); + lines.push(Line::from(vec![Span::styled( + forward_line, + Style::default().fg(Color::Magenta), + )])); } } // Отображаем reply если есть if let Some(reply) = &msg.reply_to { let reply_text: String = reply.text.chars().take(40).collect(); - let ellipsis = if reply.text.chars().count() > 40 { "..." } else { "" }; + let ellipsis = if reply.text.chars().count() > 40 { + "..." + } else { + "" + }; let reply_line = format!("┌ {}: {}{}", reply.sender_name, reply_text, ellipsis); let reply_len = reply_line.chars().count(); @@ -562,9 +590,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { ])); } else { // Reply слева для входящих - 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), + )])); } } @@ -593,11 +622,8 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { ); // Форматируем текст с entities - let formatted_spans = format_text_with_entities( - &wrapped.text, - &line_entities, - msg_color, - ); + let formatted_spans = + format_text_with_entities(&wrapped.text, &line_entities, msg_color); if is_last_line { // Последняя строка — добавляем time_mark @@ -605,17 +631,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { let padding = content_width.saturating_sub(full_len + 1); let mut line_spans = vec![Span::raw(" ".repeat(padding))]; if is_selected { - line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))); + line_spans.push(Span::styled( + selection_marker, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )); } 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); let mut line_spans = vec![Span::raw(" ".repeat(padding))]; if i == 0 && is_selected { - line_spans.push(Span::styled(selection_marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))); + line_spans.push(Span::styled( + selection_marker, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + )); } line_spans.extend(formatted_spans); lines.push(Line::from(line_spans)); @@ -643,19 +682,24 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { ); // Форматируем текст с entities - let formatted_spans = format_text_with_entities( - &wrapped.text, - &line_entities, - msg_color, - ); + let formatted_spans = + 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))); + line_spans.push(Span::styled( + selection_marker, + 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)); @@ -694,9 +738,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { }; let style = if reaction.is_chosen { - Style::default().fg(app.config.parse_color(&app.config.colors.reaction_chosen)) + Style::default() + .fg(app.config.parse_color(&app.config.colors.reaction_chosen)) } else { - Style::default().fg(app.config.parse_color(&app.config.colors.reaction_other)) + Style::default() + .fg(app.config.parse_color(&app.config.colors.reaction_other)) }; reaction_spans.push(Span::styled(reaction_text, style)); @@ -723,10 +769,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } if lines.is_empty() { - lines.push(Line::from(Span::styled( - "Нет сообщений", - Style::default().fg(Color::Gray), - ))); + lines.push(Line::from(Span::styled("Нет сообщений", Style::default().fg(Color::Gray)))); } // Вычисляем скролл с учётом пользовательского offset @@ -769,10 +812,15 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { // Input box с wrap для длинного текста и блочным курсором let (input_line, input_title) = if app.is_forwarding() { // Режим пересылки - показываем превью сообщения - let forward_preview = app.get_forwarding_message() + let forward_preview = app + .get_forwarding_message() .map(|m| { let text_preview: String = m.content.chars().take(40).collect(); - let ellipsis = if m.content.chars().count() > 40 { "..." } else { "" }; + let ellipsis = if m.content.chars().count() > 40 { + "..." + } else { + "" + }; format!("↪ {}{}", text_preview, ellipsis) }) .unwrap_or_else(|| "↪ ...".to_string()); @@ -782,8 +830,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } else if app.is_selecting_message() { // Режим выбора сообщения - подсказка зависит от возможностей let selected_msg = app.get_selected_message(); - let can_edit = selected_msg.map(|m| m.can_be_edited && m.is_outgoing).unwrap_or(false); - let can_delete = selected_msg.map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users).unwrap_or(false); + let can_edit = selected_msg + .map(|m| m.can_be_edited && m.is_outgoing) + .unwrap_or(false); + let can_delete = selected_msg + .map(|m| m.can_be_deleted_only_for_self || m.can_be_deleted_for_all_users) + .unwrap_or(false); let hint = match (can_edit, can_delete) { (true, true) => "↑↓ · Enter ред. · r ответ · f перслть · y копир. · d удал. · Esc", @@ -791,7 +843,10 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { (false, true) => "↑↓ · r ответ · f переслать · y копир. · d удалить · Esc", (false, false) => "↑↓ · r ответить · f переслать · y копировать · Esc", }; - (Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), " Выбор сообщения ") + ( + Line::from(Span::styled(hint, Style::default().fg(Color::Cyan))), + " Выбор сообщения ", + ) } else if app.is_editing() { // Режим редактирования if app.message_input.is_empty() { @@ -804,16 +859,30 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { (line, " Редактирование (Esc отмена) ") } else { // Текст с курсором - let line = render_input_with_cursor("✏ ", &app.message_input, app.cursor_position, Color::Magenta); + let line = render_input_with_cursor( + "✏ ", + &app.message_input, + app.cursor_position, + Color::Magenta, + ); (line, " Редактирование (Esc отмена) ") } } else if app.is_replying() { // Режим ответа на сообщение - let reply_preview = app.get_replying_to_message() + let reply_preview = app + .get_replying_to_message() .map(|m| { - let sender = if m.is_outgoing { "Вы" } else { &m.sender_name }; + let sender = if m.is_outgoing { + "Вы" + } else { + &m.sender_name + }; let text_preview: String = m.content.chars().take(30).collect(); - let ellipsis = if m.content.chars().count() > 30 { "..." } else { "" }; + let ellipsis = if m.content.chars().count() > 30 { + "..." + } else { + "" + }; format!("{}: {}{}", sender, text_preview, ellipsis) }) .unwrap_or_else(|| "...".to_string()); @@ -829,7 +898,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { } else { let short_preview: String = reply_preview.chars().take(15).collect(); let prefix = format!("↪ {} > ", short_preview); - let line = render_input_with_cursor(&prefix, &app.message_input, app.cursor_position, Color::Yellow); + let line = render_input_with_cursor( + &prefix, + &app.message_input, + app.cursor_position, + Color::Yellow, + ); (line, " Ответ (Esc отмена) ") } } else { @@ -844,7 +918,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { (line, "") } else { // Текст с курсором - let line = render_input_with_cursor("> ", &app.message_input, app.cursor_position, Color::Yellow); + let line = render_input_with_cursor( + "> ", + &app.message_input, + app.cursor_position, + Color::Yellow, + ); (line, "") } }; @@ -860,7 +939,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { Block::default() .borders(Borders::ALL) .title(input_title) - .title_style(Style::default().fg(title_color).add_modifier(Modifier::BOLD)) + .title_style( + Style::default() + .fg(title_color) + .add_modifier(Modifier::BOLD), + ) }; let input = Paragraph::new(input_line) @@ -882,7 +965,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) { // Модалка выбора реакции if app.is_reaction_picker_mode() { - render_reaction_picker_modal(f, area, &app.available_reactions, app.selected_reaction_index); + render_reaction_picker_modal( + f, + area, + &app.available_reactions, + app.selected_reaction_index, + ); } } @@ -899,8 +987,12 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { // Search input let total = app.message_search_results.len(); - let current = if total > 0 { app.selected_search_result_index + 1 } else { 0 }; - + let current = if total > 0 { + app.selected_search_result_index + 1 + } else { + 0 + }; + let input_line = if app.message_search_query.is_empty() { Line::from(vec![ Span::styled("🔍 ", Style::default().fg(Color::Yellow)), @@ -915,15 +1007,18 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { Span::styled(format!(" ({}/{})", current, total), Style::default().fg(Color::Gray)), ]) }; - - let search_input = Paragraph::new(input_line) - .block( - Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)) - .title(" Поиск по сообщениям ") - .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) - ); + + let search_input = Paragraph::new(input_line).block( + Block::default() + .borders(Borders::ALL) + .border_style(Style::default().fg(Color::Yellow)) + .title(" Поиск по сообщениям ") + .title_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), + ); f.render_widget(search_input, chunks[0]); // Search results @@ -948,14 +1043,29 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { // Маркер выбора, имя и дата let marker = if is_selected { "▶ " } else { " " }; - let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan }; - let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() }; + let sender_color = if msg.is_outgoing { + Color::Green + } else { + Color::Cyan + }; + let sender_name = if msg.is_outgoing { + "Вы".to_string() + } else { + msg.sender_name.clone() + }; lines.push(Line::from(vec![ - Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::styled( + marker, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), Span::styled( format!("{} ", sender_name), - Style::default().fg(sender_color).add_modifier(Modifier::BOLD), + Style::default() + .fg(sender_color) + .add_modifier(Modifier::BOLD), ), Span::styled( format!("({})", crate::utils::format_datetime(msg.date)), @@ -964,7 +1074,11 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { ])); // Текст сообщения (с переносом) - let msg_color = if is_selected { Color::Yellow } else { Color::White }; + let msg_color = if is_selected { + Color::Yellow + } else { + Color::White + }; let max_width = content_width.saturating_sub(4); let wrapped = wrap_text_with_offsets(&msg.content, max_width); let wrapped_count = wrapped.len(); @@ -998,20 +1112,35 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)) + .border_style(Style::default().fg(Color::Yellow)), ) .scroll((scroll_offset, 0)); f.render_widget(results_widget, chunks[1]); // Help bar let help_line = Line::from(vec![ - Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::styled( + " ↑↓ ", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), Span::raw("навигация"), Span::raw(" "), - Span::styled(" n/N ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::styled( + " n/N ", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), Span::raw("след./пред."), Span::raw(" "), - Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::styled( + " Enter ", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ), Span::raw("перейти"), Span::raw(" "), Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), @@ -1021,7 +1150,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Yellow)) + .border_style(Style::default().fg(Color::Yellow)), ) .alignment(Alignment::Center); f.render_widget(help, chunks[2]); @@ -1046,9 +1175,13 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Magenta)) + .border_style(Style::default().fg(Color::Magenta)), ) - .style(Style::default().fg(Color::Magenta).add_modifier(Modifier::BOLD)); + .style( + Style::default() + .fg(Color::Magenta) + .add_modifier(Modifier::BOLD), + ); f.render_widget(header, chunks[0]); // Pinned messages list @@ -1057,7 +1190,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { for (idx, msg) in app.pinned_messages.iter().enumerate() { let is_selected = idx == app.selected_pinned_index; - + // Пустая строка между сообщениями if idx > 0 { lines.push(Line::from("")); @@ -1065,14 +1198,29 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { // Маркер выбора и имя отправителя let marker = if is_selected { "▶ " } else { " " }; - let sender_color = if msg.is_outgoing { Color::Green } else { Color::Cyan }; - let sender_name = if msg.is_outgoing { "Вы".to_string() } else { msg.sender_name.clone() }; - + let sender_color = if msg.is_outgoing { + Color::Green + } else { + Color::Cyan + }; + let sender_name = if msg.is_outgoing { + "Вы".to_string() + } else { + msg.sender_name.clone() + }; + lines.push(Line::from(vec![ - Span::styled(marker, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::styled( + marker, + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), Span::styled( format!("{} ", sender_name), - Style::default().fg(sender_color).add_modifier(Modifier::BOLD), + Style::default() + .fg(sender_color) + .add_modifier(Modifier::BOLD), ), Span::styled( format!("({})", crate::utils::format_datetime(msg.date)), @@ -1081,12 +1229,17 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { ])); // Текст сообщения (с переносом) - let msg_color = if is_selected { Color::Yellow } else { Color::White }; + let msg_color = if is_selected { + Color::Yellow + } else { + Color::White + }; let max_width = content_width.saturating_sub(4); let wrapped = wrap_text_with_offsets(&msg.content, max_width); let wrapped_count = wrapped.len(); - - for wrapped_line in wrapped.into_iter().take(3) { // Максимум 3 строки на сообщение + + for wrapped_line in wrapped.into_iter().take(3) { + // Максимум 3 строки на сообщение lines.push(Line::from(vec![ Span::raw(" "), // Отступ Span::styled(wrapped_line.text, Style::default().fg(msg_color)), @@ -1121,17 +1274,27 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Magenta)) + .border_style(Style::default().fg(Color::Magenta)), ) .scroll((scroll_offset, 0)); f.render_widget(messages_widget, chunks[1]); // Help bar let help_line = Line::from(vec![ - Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::styled( + " ↑↓ ", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), Span::raw("навигация"), Span::raw(" "), - Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::styled( + " Enter ", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ), Span::raw("перейти"), Span::raw(" "), Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), @@ -1141,7 +1304,7 @@ fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Magenta)) + .border_style(Style::default().fg(Color::Magenta)), ) .alignment(Alignment::Center); f.render_widget(help, chunks[2]); @@ -1169,11 +1332,18 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) { Line::from(""), Line::from(Span::styled( "Удалить сообщение?", - Style::default().fg(Color::White).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::White) + .add_modifier(Modifier::BOLD), )), Line::from(""), Line::from(vec![ - Span::styled(" [y/Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::styled( + " [y/Enter] ", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ), Span::raw("Да"), Span::raw(" "), Span::styled(" [n/Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), @@ -1194,9 +1364,13 @@ fn render_delete_confirm_modal(f: &mut Frame, area: Rect) { f.render_widget(modal, modal_area); } - /// Рендерит модалку выбора реакции -fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions: &[String], selected_index: usize) { +fn render_reaction_picker_modal( + f: &mut Frame, + area: Rect, + available_reactions: &[String], + selected_index: usize, +) { use ratatui::widgets::Clear; // Размеры модалки (зависят от количества реакций) @@ -1248,9 +1422,19 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions: // Добавляем пустую строку и подсказку text_lines.push(Line::from("")); text_lines.push(Line::from(vec![ - Span::styled(" [←/→/↑/↓] ", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)), + Span::styled( + " [←/→/↑/↓] ", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), Span::raw("Выбор "), - Span::styled(" [Enter] ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::styled( + " [Enter] ", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ), Span::raw("Добавить "), Span::styled(" [Esc] ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::raw("Отмена"), @@ -1262,7 +1446,11 @@ fn render_reaction_picker_modal(f: &mut Frame, area: Rect, available_reactions: .borders(Borders::ALL) .border_style(Style::default().fg(Color::Yellow)) .title(" Выбери реакцию ") - .title_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + .title_style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), ) .alignment(Alignment::Left); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index fe5d707..f52a619 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,16 +1,16 @@ -mod loading; mod auth; -mod main_screen; pub mod chat_list; -pub mod messages; pub mod footer; +mod loading; +mod main_screen; +pub mod messages; pub mod profile; -use ratatui::Frame; +use crate::app::{App, AppScreen}; use ratatui::layout::Alignment; use ratatui::style::{Color, Modifier, Style}; use ratatui::widgets::Paragraph; -use crate::app::{App, AppScreen}; +use ratatui::Frame; /// Минимальная высота терминала const MIN_HEIGHT: u16 = 10; @@ -34,12 +34,13 @@ pub fn render(f: &mut Frame, app: &mut App) { } fn render_size_warning(f: &mut Frame, width: u16, height: u16) { - let message = format!( - "{}x{}\nМинимум: {}x{}", - width, height, MIN_WIDTH, MIN_HEIGHT - ); + let message = format!("{}x{}\nМинимум: {}x{}", width, height, MIN_WIDTH, MIN_HEIGHT); let warning = Paragraph::new(message) - .style(Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)) + .style( + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ) .alignment(Alignment::Center); f.render_widget(warning, f.area()); } diff --git a/src/ui/profile.rs b/src/ui/profile.rs index e4af9b3..225f0ac 100644 --- a/src/ui/profile.rs +++ b/src/ui/profile.rs @@ -1,3 +1,5 @@ +use crate::app::App; +use crate::tdlib::client::ProfileInfo; use ratatui::{ layout::{Alignment, Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, @@ -5,8 +7,6 @@ use ratatui::{ widgets::{Block, Borders, Paragraph}, Frame, }; -use crate::app::App; -use crate::tdlib::client::ProfileInfo; /// Рендерит режим просмотра профиля pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { @@ -20,9 +20,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(3), // Header - Constraint::Min(0), // Profile info - Constraint::Length(3), // Actions help + Constraint::Length(3), // Header + Constraint::Min(0), // Profile info + Constraint::Length(3), // Actions help ]) .split(area); @@ -32,9 +32,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)) + .border_style(Style::default().fg(Color::Cyan)), ) - .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)); + .style( + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ); f.render_widget(header, chunks[0]); // Profile info @@ -83,9 +87,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { // Bio (только для личных чатов) if let Some(bio) = &profile.bio { - lines.push(Line::from(vec![ - Span::styled("О себе: ", Style::default().fg(Color::Gray)), - ])); + lines.push(Line::from(vec![Span::styled("О себе: ", Style::default().fg(Color::Gray))])); // Разбиваем bio на строки если длинное let bio_lines: Vec<&str> = bio.lines().collect(); for bio_line in bio_lines { @@ -105,9 +107,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { // Description (для групп/каналов) if let Some(desc) = &profile.description { - lines.push(Line::from(vec![ - Span::styled("Описание: ", Style::default().fg(Color::Gray)), - ])); + lines.push(Line::from(vec![Span::styled("Описание: ", Style::default().fg(Color::Gray))])); let desc_lines: Vec<&str> = desc.lines().collect(); for desc_line in desc_lines { lines.push(Line::from(Span::styled(desc_line, Style::default().fg(Color::White)))); @@ -119,7 +119,12 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { if let Some(link) = &profile.invite_link { lines.push(Line::from(vec![ Span::styled("Ссылка: ", Style::default().fg(Color::Gray)), - Span::styled(link, Style::default().fg(Color::Blue).add_modifier(Modifier::UNDERLINED)), + Span::styled( + link, + Style::default() + .fg(Color::Blue) + .add_modifier(Modifier::UNDERLINED), + ), ])); lines.push(Line::from("")); } @@ -131,7 +136,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { // Действия lines.push(Line::from(Span::styled( "Действия:", - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), ))); lines.push(Line::from("")); @@ -140,7 +147,9 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { let is_selected = idx == app.selected_profile_action; let marker = if is_selected { "▶ " } else { " " }; let style = if is_selected { - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD) + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::White) }; @@ -154,17 +163,27 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)) + .border_style(Style::default().fg(Color::Cyan)), ) .scroll((0, 0)); f.render_widget(info_widget, chunks[1]); // Help bar let help_line = Line::from(vec![ - Span::styled(" ↑↓ ", Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), + Span::styled( + " ↑↓ ", + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), + ), Span::raw("навигация"), Span::raw(" "), - Span::styled(" Enter ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::styled( + " Enter ", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ), Span::raw("выбрать"), Span::raw(" "), Span::styled(" Esc ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), @@ -174,7 +193,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { .block( Block::default() .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)) + .border_style(Style::default().fg(Color::Cyan)), ) .alignment(Alignment::Center); f.render_widget(help, chunks[2]); @@ -183,17 +202,17 @@ pub fn render(f: &mut Frame, area: Rect, app: &App, profile: &ProfileInfo) { /// Получить список доступных действий fn get_available_actions(profile: &ProfileInfo) -> Vec<&'static str> { let mut actions = vec![]; - + if profile.username.is_some() { actions.push("Открыть в браузере"); } - + actions.push("Скопировать ID"); - + if profile.is_group { actions.push("Покинуть группу"); } - + actions } @@ -212,12 +231,19 @@ fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) { Line::from(""), Line::from(Span::styled( text, - Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD), )), Line::from(""), Line::from(""), Line::from(vec![ - Span::styled("y/н/Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), + Span::styled( + "y/н/Enter", + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::BOLD), + ), Span::raw(" — да "), Span::styled("n/т/Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), Span::raw(" — нет"), @@ -230,7 +256,7 @@ fn render_leave_confirmation_modal(f: &mut Frame, area: Rect, step: u8) { .borders(Borders::ALL) .border_style(Style::default().fg(Color::Red)) .title(" ⚠ ВНИМАНИЕ ") - .title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)) + .title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), ) .alignment(Alignment::Center); diff --git a/src/utils.rs b/src/utils.rs index 076fc50..42816fb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -105,21 +105,21 @@ pub fn get_day(timestamp: i32) -> i64 { /// Форматирование timestamp в полную дату и время (DD.MM.YYYY HH:MM) pub fn format_datetime(timestamp: i32) -> String { let secs = timestamp as i64; - + // Время let hours = ((secs % 86400) / 3600) as u32; let minutes = ((secs % 3600) / 60) as u32; let local_hours = (hours + 3) % 24; // MSK - + // Дата let days_since_epoch = secs / 86400; let year = 1970 + (days_since_epoch / 365) as i32; let day_of_year = days_since_epoch % 365; - + let months = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; let mut month = 1; let mut day = day_of_year as i32; - + for (i, &m) in months.iter().enumerate() { if day < m { month = i + 1; @@ -127,7 +127,7 @@ pub fn format_datetime(timestamp: i32) -> String { } day -= m; } - + format!("{:02}.{:02}.{} {:02}:{:02}", day + 1, month, year, local_hours, minutes) } diff --git a/tests/chat_list.rs b/tests/chat_list.rs index de7193f..3ee3f41 100644 --- a/tests/chat_list.rs +++ b/tests/chat_list.rs @@ -2,9 +2,9 @@ mod helpers; -use helpers::test_data::{TestChatBuilder, create_test_chat}; use helpers::app_builder::TestAppBuilder; -use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; +use helpers::snapshot_utils::{buffer_to_string, render_to_buffer}; +use helpers::test_data::{create_test_chat, TestChatBuilder}; use insta::assert_snapshot; #[test] @@ -44,9 +44,7 @@ fn snapshot_chat_with_unread_count() { .last_message("Привет, как дела?") .build(); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::chat_list::render(f, f.area(), &mut app); @@ -63,9 +61,7 @@ fn snapshot_chat_with_pinned() { .last_message("Pinned message") .build(); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::chat_list::render(f, f.area(), &mut app); @@ -83,9 +79,7 @@ fn snapshot_chat_with_muted() { .last_message("Too many messages") .build(); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::chat_list::render(f, f.area(), &mut app); @@ -103,9 +97,7 @@ fn snapshot_chat_with_mentions() { .last_message("@me check this out") .build(); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::chat_list::render(f, f.area(), &mut app); @@ -139,9 +131,7 @@ fn snapshot_chat_long_title() { .last_message("Test message") .build(); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::chat_list::render(f, f.area(), &mut app); diff --git a/tests/delete_message.rs b/tests/delete_message.rs index c12b45e..b1bfaa9 100644 --- a/tests/delete_message.rs +++ b/tests/delete_message.rs @@ -61,9 +61,7 @@ fn test_can_only_delete_own_messages_for_all() { let mut client = FakeTdClient::new(); // Наше исходящее сообщение (можно удалить для всех) - let outgoing_msg = TestMessageBuilder::new("My message", 1) - .outgoing() - .build(); + let outgoing_msg = TestMessageBuilder::new("My message", 1).outgoing().build(); client = client.with_message(123, outgoing_msg); @@ -76,7 +74,7 @@ fn test_can_only_delete_own_messages_for_all() { // Проверяем флаги удаления let messages = client.get_messages(123); - assert_eq!(messages[0].can_be_deleted_for_all_users, true); // Наше + assert_eq!(messages[0].can_be_deleted_for_all_users, true); // Наше assert_eq!(messages[1].can_be_deleted_for_all_users, false); // Чужое // Оба можно удалить для себя diff --git a/tests/drafts.rs b/tests/drafts.rs index a8d926e..8c60504 100644 --- a/tests/drafts.rs +++ b/tests/drafts.rs @@ -12,9 +12,7 @@ struct DraftManager { impl DraftManager { fn new() -> Self { - Self { - drafts: HashMap::new(), - } + Self { drafts: HashMap::new() } } /// Сохранить черновик для чата diff --git a/tests/edit_message.rs b/tests/edit_message.rs index ce228a7..a8918b0 100644 --- a/tests/edit_message.rs +++ b/tests/edit_message.rs @@ -55,9 +55,7 @@ fn test_can_only_edit_own_messages() { let mut client = FakeTdClient::new(); // Наше исходящее сообщение (можно редактировать) - let outgoing_msg = TestMessageBuilder::new("My message", 1) - .outgoing() - .build(); + let outgoing_msg = TestMessageBuilder::new("My message", 1).outgoing().build(); client = client.with_message(123, outgoing_msg); @@ -70,7 +68,7 @@ fn test_can_only_edit_own_messages() { // Проверяем флаги let messages = client.get_messages(123); - assert_eq!(messages[0].can_be_edited, true); // Наше сообщение + assert_eq!(messages[0].can_be_edited, true); // Наше сообщение assert_eq!(messages[1].can_be_edited, false); // Чужое сообщение } diff --git a/tests/footer.rs b/tests/footer.rs index b0dd7e9..8602b55 100644 --- a/tests/footer.rs +++ b/tests/footer.rs @@ -2,9 +2,9 @@ mod helpers; -use helpers::test_data::create_test_chat; use helpers::app_builder::TestAppBuilder; -use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; +use helpers::snapshot_utils::{buffer_to_string, render_to_buffer}; +use helpers::test_data::create_test_chat; use insta::assert_snapshot; use tele_tui::tdlib::NetworkState; @@ -12,9 +12,7 @@ use tele_tui::tdlib::NetworkState; fn snapshot_footer_chat_list() { let chat = create_test_chat("Mom", 123); - let app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let app = TestAppBuilder::new().with_chat(chat).build(); let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::footer::render(f, f.area(), &app); @@ -45,9 +43,7 @@ fn snapshot_footer_open_chat() { fn snapshot_footer_network_waiting() { let chat = create_test_chat("Mom", 123); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); // Set network state to WaitingForNetwork app.td_client.network_state = NetworkState::WaitingForNetwork; @@ -64,9 +60,7 @@ fn snapshot_footer_network_waiting() { fn snapshot_footer_network_connecting_proxy() { let chat = create_test_chat("Mom", 123); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); // Set network state to ConnectingToProxy app.td_client.network_state = NetworkState::ConnectingToProxy; @@ -83,9 +77,7 @@ fn snapshot_footer_network_connecting_proxy() { fn snapshot_footer_network_connecting() { let chat = create_test_chat("Mom", 123); - let mut app = TestAppBuilder::new() - .with_chat(chat) - .build(); + let mut app = TestAppBuilder::new().with_chat(chat).build(); // Set network state to Connecting app.td_client.network_state = NetworkState::Connecting; diff --git a/tests/helpers/app_builder.rs b/tests/helpers/app_builder.rs index 4c2207c..828c6dd 100644 --- a/tests/helpers/app_builder.rs +++ b/tests/helpers/app_builder.rs @@ -1,11 +1,11 @@ // Test App builder -use tele_tui::app::{App, AppScreen}; -use tele_tui::config::Config; -use tele_tui::tdlib::{ChatInfo, MessageInfo}; -use tele_tui::tdlib::client::AuthState; use ratatui::widgets::ListState; use std::collections::HashMap; +use tele_tui::app::{App, AppScreen}; +use tele_tui::config::Config; +use tele_tui::tdlib::client::AuthState; +use tele_tui::tdlib::{ChatInfo, MessageInfo}; /// Builder для создания тестового App /// @@ -149,13 +149,19 @@ impl TestAppBuilder { /// Добавить сообщение для чата pub fn with_message(mut self, chat_id: i64, message: MessageInfo) -> Self { - self.messages.entry(chat_id).or_insert_with(Vec::new).push(message); + self.messages + .entry(chat_id) + .or_insert_with(Vec::new) + .push(message); self } /// Добавить несколько сообщений для чата pub fn with_messages(mut self, chat_id: i64, messages: Vec) -> Self { - self.messages.entry(chat_id).or_insert_with(Vec::new).extend(messages); + self.messages + .entry(chat_id) + .or_insert_with(Vec::new) + .extend(messages); self } @@ -329,9 +335,7 @@ mod tests { #[test] fn test_builder_search_mode() { - let app = TestAppBuilder::new() - .searching("test query") - .build(); + let app = TestAppBuilder::new().searching("test query").build(); assert!(app.is_searching); assert_eq!(app.search_query, "test query"); diff --git a/tests/helpers/fake_tdclient.rs b/tests/helpers/fake_tdclient.rs index 0f706ae..a0575a4 100644 --- a/tests/helpers/fake_tdclient.rs +++ b/tests/helpers/fake_tdclient.rs @@ -1,7 +1,7 @@ // Fake TDLib client for testing use std::collections::HashMap; -use tele_tui::tdlib::{ChatInfo, MessageInfo, FolderInfo, NetworkState}; +use tele_tui::tdlib::{ChatInfo, FolderInfo, MessageInfo, NetworkState}; /// Упрощённый mock TDLib клиента для тестов #[derive(Clone)] @@ -42,12 +42,7 @@ impl FakeTdClient { Self { chats: vec![], messages: HashMap::new(), - folders: vec![ - FolderInfo { - id: 0, - name: "All".to_string(), - }, - ], + folders: vec![FolderInfo { id: 0, name: "All".to_string() }], user_names: HashMap::new(), network_state: NetworkState::Ready, typing_chat_id: None, @@ -90,10 +85,7 @@ impl FakeTdClient { /// Добавить папку pub fn with_folder(mut self, id: i32, name: &str) -> Self { - self.folders.push(FolderInfo { - id, - name: name.to_string(), - }); + self.folders.push(FolderInfo { id, name: name.to_string() }); self } @@ -116,10 +108,7 @@ impl FakeTdClient { /// Получить сообщения для чата pub fn get_messages(&self, chat_id: i64) -> Vec { - self.messages - .get(&chat_id) - .cloned() - .unwrap_or_default() + self.messages.get(&chat_id).cloned().unwrap_or_default() } /// Получить папки @@ -131,11 +120,8 @@ impl FakeTdClient { pub fn send_message(&mut self, chat_id: i64, text: String, reply_to: Option) -> i64 { let message_id = (self.sent_messages.len() as i64) + 1000; - self.sent_messages.push(SentMessage { - chat_id, - text: text.clone(), - reply_to, - }); + self.sent_messages + .push(SentMessage { chat_id, text: text.clone(), reply_to }); // Добавляем сообщение в список сообщений чата let message = MessageInfo { @@ -165,10 +151,8 @@ impl FakeTdClient { /// Редактировать сообщение (мок) pub fn edit_message(&mut self, chat_id: i64, message_id: i64, new_text: String) { - self.edited_messages.push(EditedMessage { - message_id, - new_text: new_text.clone(), - }); + self.edited_messages + .push(EditedMessage { message_id, new_text: new_text.clone() }); // Обновляем сообщение в списке if let Some(messages) = self.messages.get_mut(&chat_id) { diff --git a/tests/helpers/snapshot_utils.rs b/tests/helpers/snapshot_utils.rs index b9d38b9..29cda24 100644 --- a/tests/helpers/snapshot_utils.rs +++ b/tests/helpers/snapshot_utils.rs @@ -1,9 +1,9 @@ // Snapshot testing utilities use ratatui::backend::TestBackend; -use ratatui::Terminal; use ratatui::buffer::Buffer; use ratatui::layout::Rect; +use ratatui::Terminal; /// Конвертирует Buffer в читаемую строку для snapshot тестов pub fn buffer_to_string(buffer: &Buffer) -> String { @@ -33,9 +33,7 @@ where let backend = TestBackend::new(width, height); let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(render_fn) - .unwrap(); + terminal.draw(render_fn).unwrap(); terminal.backend().buffer().clone() } @@ -44,7 +42,7 @@ where #[macro_export] macro_rules! assert_ui_snapshot { ($name:expr, $width:expr, $height:expr, $render_fn:expr) => {{ - use $crate::helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; + use $crate::helpers::snapshot_utils::{buffer_to_string, render_to_buffer}; let buffer = render_to_buffer($width, $height, $render_fn); let output = buffer_to_string(&buffer); insta::assert_snapshot!($name, output); @@ -59,9 +57,7 @@ mod tests { #[test] fn test_buffer_to_string_simple() { let buffer = render_to_buffer(10, 3, |f| { - let block = Block::default() - .borders(Borders::ALL) - .title("Hi"); + let block = Block::default().borders(Borders::ALL).title("Hi"); f.render_widget(block, f.area()); }); diff --git a/tests/helpers/test_data.rs b/tests/helpers/test_data.rs index 29a9963..a4379a2 100644 --- a/tests/helpers/test_data.rs +++ b/tests/helpers/test_data.rs @@ -1,6 +1,6 @@ // Test data builders and fixtures -use tele_tui::tdlib::{ChatInfo, MessageInfo, ReactionInfo, ReplyInfo, ForwardInfo, ProfileInfo}; +use tele_tui::tdlib::{ChatInfo, ForwardInfo, MessageInfo, ProfileInfo, ReactionInfo, ReplyInfo}; /// Builder для создания тестового чата pub struct TestChatBuilder { @@ -181,11 +181,8 @@ impl TestMessageBuilder { } pub fn reaction(mut self, emoji: &str, count: i32, chosen: bool) -> Self { - self.reactions.push(ReactionInfo { - emoji: emoji.to_string(), - count, - is_chosen: chosen, - }); + self.reactions + .push(ReactionInfo { emoji: emoji.to_string(), count, is_chosen: chosen }); self } diff --git a/tests/input_field.rs b/tests/input_field.rs index 446f24c..fce8fe1 100644 --- a/tests/input_field.rs +++ b/tests/input_field.rs @@ -2,9 +2,9 @@ mod helpers; -use helpers::test_data::{TestMessageBuilder, create_test_chat}; use helpers::app_builder::TestAppBuilder; -use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; +use helpers::snapshot_utils::{buffer_to_string, render_to_buffer}; +use helpers::test_data::{create_test_chat, TestMessageBuilder}; use insta::assert_snapshot; #[test] diff --git a/tests/messages.rs b/tests/messages.rs index 8e1d1ae..f21befb 100644 --- a/tests/messages.rs +++ b/tests/messages.rs @@ -2,9 +2,9 @@ mod helpers; -use helpers::test_data::{TestChatBuilder, TestMessageBuilder, create_test_chat}; use helpers::app_builder::TestAppBuilder; -use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; +use helpers::snapshot_utils::{buffer_to_string, render_to_buffer}; +use helpers::test_data::{create_test_chat, TestChatBuilder, TestMessageBuilder}; use insta::assert_snapshot; #[test] @@ -48,9 +48,7 @@ fn snapshot_single_incoming_message() { #[test] fn snapshot_single_outgoing_message() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("Hi mom!", 1) - .outgoing() - .build(); + let message = TestMessageBuilder::new("Hi mom!", 1).outgoing().build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -122,9 +120,7 @@ fn snapshot_sender_grouping() { #[test] fn snapshot_outgoing_sent() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("Just sent", 1) - .outgoing() - .build(); + let message = TestMessageBuilder::new("Just sent", 1).outgoing().build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -173,9 +169,7 @@ fn snapshot_outgoing_read() { #[test] fn snapshot_edited_message() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("Edited text", 1) - .edited() - .build(); + let message = TestMessageBuilder::new("Edited text", 1).edited().build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -195,8 +189,7 @@ fn snapshot_edited_message() { fn snapshot_long_message_wrap() { let chat = create_test_chat("Mom", 123); let long_text = "This is a very long message that should wrap across multiple lines when rendered in the terminal UI. Let's make it even longer to ensure we test the wrapping behavior properly."; - let message = TestMessageBuilder::new(long_text, 1) - .build(); + let message = TestMessageBuilder::new(long_text, 1).build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -215,8 +208,7 @@ fn snapshot_long_message_wrap() { #[test] fn snapshot_markdown_bold_italic_code() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("**bold** *italic* `code`", 1) - .build(); + let message = TestMessageBuilder::new("**bold** *italic* `code`", 1).build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -235,8 +227,8 @@ fn snapshot_markdown_bold_italic_code() { #[test] fn snapshot_markdown_link_mention() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("Check [this](https://example.com) and @username", 1) - .build(); + let message = + TestMessageBuilder::new("Check [this](https://example.com) and @username", 1).build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -255,8 +247,7 @@ fn snapshot_markdown_link_mention() { #[test] fn snapshot_markdown_spoiler() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("Spoiler: ||hidden text||", 1) - .build(); + let message = TestMessageBuilder::new("Spoiler: ||hidden text||", 1).build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -275,8 +266,7 @@ fn snapshot_markdown_spoiler() { #[test] fn snapshot_media_placeholder() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("[Фото]", 1) - .build(); + let message = TestMessageBuilder::new("[Фото]", 1).build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -380,8 +370,7 @@ fn snapshot_multiple_reactions() { #[test] fn snapshot_selected_message() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("Selected message", 1) - .build(); + let message = TestMessageBuilder::new("Selected message", 1).build(); let mut app = TestAppBuilder::new() .with_chat(chat) diff --git a/tests/modals.rs b/tests/modals.rs index a7da16c..e19b533 100644 --- a/tests/modals.rs +++ b/tests/modals.rs @@ -2,17 +2,17 @@ mod helpers; -use helpers::test_data::{TestChatBuilder, TestMessageBuilder, create_test_chat, create_test_profile}; use helpers::app_builder::TestAppBuilder; -use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; +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; #[test] fn snapshot_delete_confirmation_modal() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("Delete me", 1) - .outgoing() - .build(); + let message = TestMessageBuilder::new("Delete me", 1).outgoing().build(); let app = TestAppBuilder::new() .with_chat(chat) @@ -32,8 +32,7 @@ fn snapshot_delete_confirmation_modal() { #[test] fn snapshot_emoji_picker_default() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("React to this", 1) - .build(); + let message = TestMessageBuilder::new("React to this", 1).build(); let app = TestAppBuilder::new() .with_chat(chat) @@ -53,8 +52,7 @@ fn snapshot_emoji_picker_default() { #[test] fn snapshot_emoji_picker_with_selection() { let chat = create_test_chat("Mom", 123); - let message = TestMessageBuilder::new("React to this", 1) - .build(); + let message = TestMessageBuilder::new("React to this", 1).build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -97,8 +95,7 @@ fn snapshot_profile_personal_chat() { #[test] fn snapshot_profile_group_chat() { - let chat = TestChatBuilder::new("Work Group", 456) - .build(); + let chat = TestChatBuilder::new("Work Group", 456).build(); let mut profile = create_test_profile("Work Group", 456); profile.is_group = true; @@ -125,10 +122,8 @@ fn snapshot_profile_group_chat() { #[test] fn snapshot_pinned_message() { let chat = create_test_chat("Mom", 123); - let message1 = TestMessageBuilder::new("Regular message", 1) - .build(); - let pinned_msg = TestMessageBuilder::new("Important pinned message!", 2) - .build(); + let message1 = TestMessageBuilder::new("Regular message", 1).build(); + let pinned_msg = TestMessageBuilder::new("Important pinned message!", 2).build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -150,12 +145,9 @@ fn snapshot_pinned_message() { #[test] fn snapshot_search_in_chat() { let chat = create_test_chat("Mom", 123); - let msg1 = TestMessageBuilder::new("Hello world", 1) - .build(); - let msg2 = TestMessageBuilder::new("World is beautiful", 2) - .build(); - let msg3 = TestMessageBuilder::new("Beautiful day", 3) - .build(); + let msg1 = TestMessageBuilder::new("Hello world", 1).build(); + let msg2 = TestMessageBuilder::new("World is beautiful", 2).build(); + let msg3 = TestMessageBuilder::new("Beautiful day", 3).build(); let mut app = TestAppBuilder::new() .with_chat(chat) @@ -182,8 +174,7 @@ fn snapshot_forward_mode() { let chat2 = create_test_chat("Dad", 456); let chat3 = create_test_chat("Work Group", 789); - let message = TestMessageBuilder::new("Forward this message", 1) - .build(); + let message = TestMessageBuilder::new("Forward this message", 1).build(); let mut app = TestAppBuilder::new() .with_chats(vec![chat1.clone(), chat2, chat3]) diff --git a/tests/navigation.rs b/tests/navigation.rs index e090c50..51cae48 100644 --- a/tests/navigation.rs +++ b/tests/navigation.rs @@ -128,9 +128,7 @@ fn test_switch_folders() { let mut client = FakeTdClient::new(); // Добавляем папки (FakeTdClient уже создаёт "All" с id=0) - client = client - .with_folder(1, "Personal") - .with_folder(2, "Work"); + client = client.with_folder(1, "Personal").with_folder(2, "Work"); let folders = client.get_folders(); diff --git a/tests/screens.rs b/tests/screens.rs index 1e8cd64..72c5bed 100644 --- a/tests/screens.rs +++ b/tests/screens.rs @@ -3,7 +3,7 @@ mod helpers; use helpers::app_builder::TestAppBuilder; -use helpers::snapshot_utils::{render_to_buffer, buffer_to_string}; +use helpers::snapshot_utils::{buffer_to_string, render_to_buffer}; use helpers::test_data::create_test_chat; use insta::assert_snapshot; use tele_tui::app::AppScreen; @@ -11,9 +11,7 @@ use tele_tui::tdlib::client::AuthState; #[test] fn snapshot_loading_screen_default() { - let mut app = TestAppBuilder::new() - .screen(AppScreen::Loading) - .build(); + let mut app = TestAppBuilder::new().screen(AppScreen::Loading).build(); let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::render(f, &mut app); @@ -88,9 +86,7 @@ fn snapshot_auth_screen_password() { #[test] fn snapshot_main_screen_empty() { - let mut app = TestAppBuilder::new() - .screen(AppScreen::Main) - .build(); + let mut app = TestAppBuilder::new().screen(AppScreen::Main).build(); let buffer = render_to_buffer(80, 24, |f| { tele_tui::ui::render(f, &mut app); @@ -103,7 +99,7 @@ fn snapshot_main_screen_empty() { #[test] fn snapshot_main_screen_terminal_too_small() { let chat = create_test_chat("Mom", 123); - + let mut app = TestAppBuilder::new() .screen(AppScreen::Main) .with_chat(chat) diff --git a/tests/search.rs b/tests/search.rs index 14f2a8f..ed4db0e 100644 --- a/tests/search.rs +++ b/tests/search.rs @@ -34,13 +34,9 @@ fn test_search_chats_by_title() { fn test_search_chats_by_username() { let mut client = FakeTdClient::new(); - let chat1 = TestChatBuilder::new("Alice", 123) - .username("alice") - .build(); + let chat1 = TestChatBuilder::new("Alice", 123).username("alice").build(); - let chat2 = TestChatBuilder::new("Bob", 456) - .username("bobby") - .build(); + let chat2 = TestChatBuilder::new("Bob", 456).username("bobby").build(); let chat3 = TestChatBuilder::new("Charlie", 789).build(); // Без username diff --git a/tests/send_message.rs b/tests/send_message.rs index 68687d2..2ddbc8c 100644 --- a/tests/send_message.rs +++ b/tests/send_message.rs @@ -139,7 +139,7 @@ fn test_receive_incoming_message() { // Проверяем что в списке 2 сообщения let messages = client.get_messages(123); assert_eq!(messages.len(), 2); - assert_eq!(messages[0].is_outgoing, true); // Наше сообщение + assert_eq!(messages[0].is_outgoing, true); // Наше сообщение assert_eq!(messages[1].is_outgoing, false); // Входящее assert_eq!(messages[1].content, "Hey there!"); assert_eq!(messages[1].sender_name, "Alice");