diff --git a/CONTEXT.md b/CONTEXT.md index 9e668ab..b48a090 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -14,16 +14,20 @@ #### Функциональность - Загрузка списка чатов (до 50 штук) - **Фильтрация чатов**: показываются только чаты из ChatList::Main (без архива) +- **Фильтрация удалённых аккаунтов**: "Deleted Account" не отображаются в списке - Отображение названия чата, счётчика непрочитанных и **@username** - **Иконка 📌** для закреплённых чатов - Загрузка истории сообщений при открытии чата (множественные попытки) -- **Группировка сообщений по дате** (разделители "Сегодня", "Вчера", дата) +- **Группировка сообщений по дате** (разделители "Сегодня", "Вчера", дата) — по центру - **Группировка сообщений по отправителю** (заголовок с именем) -- **Отображение времени сообщений** в формате [HH:MM] -- **Галочки прочтения** (✓ отправлено, ✓✓ прочитано) +- **Выравнивание сообщений**: исходящие справа (зелёные), входящие слева +- **Отображение времени и галочек**: `текст (HH:MM ✓✓)` для исходящих, `(HH:MM) текст` для входящих +- **Галочки прочтения** (✓ отправлено, ✓✓ прочитано) — обновляются в реальном времени +- **Отметка сообщений как прочитанных**: при открытии чата счётчик непрочитанных сбрасывается - **Отправка текстовых сообщений** - **Новые сообщения в реальном времени** при открытом чате - **Поиск по чатам** (Ctrl+S): фильтрация по названию и @username +- **Кеширование имён пользователей**: имена загружаются асинхронно и обновляются в UI #### Управление - `↑/↓` стрелки — навигация по списку чатов @@ -32,7 +36,7 @@ - `Ctrl+S` — поиск по чатам (фильтрация по названию и username) - `Ctrl+R` — обновить список чатов - `Ctrl+C` — выход -- `Cmd+↑/Cmd+↓` — скролл сообщений в открытом чате (с подгрузкой старых) +- `↑/↓` в открытом чате — скролл сообщений (с подгрузкой старых) - `1-9` — переключение папок (в списке чатов) - Ввод текста в поле сообщения @@ -50,7 +54,7 @@ src/ │ ├── auth.rs # Экран авторизации │ ├── main_screen.rs # Главный экран │ ├── chat_list.rs # Список чатов (с pin и username) -│ ├── messages.rs # Область сообщений (группировка по дате/отправителю) +│ ├── messages.rs # Область сообщений (выравнивание, группировка) │ └── footer.rs # Подвал с командами ├── input/ │ ├── mod.rs # Роутинг ввода @@ -59,7 +63,7 @@ src/ ├── utils.rs # Утилиты (disable_tdlib_logs, format_timestamp, format_date, get_day) └── tdlib/ ├── mod.rs # Модуль экспорта - └── client.rs # TdClient: авторизация, чаты, сообщения, кеш usernames + └── client.rs # TdClient: авторизация, чаты, сообщения, кеш имён ``` ### Ключевые решения @@ -70,11 +74,13 @@ src/ 3. **Синхронизация чатов**: Чаты загружаются асинхронно через updates. Main loop периодически синхронизирует `app.chats` с `td_client.chats`. -4. **Кеширование usernames**: При получении `Update::User` сохраняем username в HashMap. При получении приватного чата связываем chat_id с user_id. +4. **Кеширование имён**: При получении `Update::User` сохраняем имя (first_name + last_name) и username в HashMap. Имена подгружаются асинхронно через очередь `pending_user_ids`. -5. **Группировка сообщений**: Сообщения группируются по дате (разделители) и по отправителю (заголовки). Время отображается рядом с каждым сообщением. +5. **Группировка сообщений**: Сообщения группируются по дате (разделители по центру) и по отправителю (заголовки). Исходящие выравниваются вправо, входящие влево. -6. **Новые сообщения**: `current_chat_id` отслеживает открытый чат. При получении `NewMessage` для этого чата сообщение добавляется сразу. +6. **Отметка прочтения**: При открытии чата вызывается `view_messages` для всех сообщений. Новые входящие сообщения автоматически отмечаются как прочитанные. `Update::ChatReadOutbox` обновляет статус галочек. + +7. **Фильтрация удалённых аккаунтов**: Чаты с названием "Deleted Account" или пустым именем пользователя автоматически удаляются из списка. ### Зависимости (Cargo.toml) @@ -100,7 +106,6 @@ API_HASH=your_api_hash - [ ] Папки телеграма (сейчас только "All") - [ ] Отображение онлайн-статуса пользователя - [ ] Markdown форматирование в сообщениях -- [ ] Отметка сообщений как прочитанные - [ ] Медиа-сообщения (фото, видео, голосовые) ## Известные проблемы diff --git a/Cargo.lock b/Cargo.lock index 8db8dc6..9e70428 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -153,8 +153,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -1873,6 +1875,7 @@ checksum = "87cbdfae498e57fb48d380fff8eb5c9c98d4497c998f6de0d30d5d6b12f5358b" name = "tele-tui" version = "0.1.0" dependencies = [ + "chrono", "crossterm", "dotenvy", "ratatui", diff --git a/Cargo.toml b/Cargo.toml index eb989ca..2a2f65f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ tokio = { version = "1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" dotenvy = "0.15" +chrono = "0.4" [build-dependencies] tdlib-rs = { version = "1.1", features = ["download-tdlib"] } diff --git a/src/app/mod.rs b/src/app/mod.rs index c4cec42..1963b6a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -22,8 +22,8 @@ pub struct App { pub current_messages: Vec, pub message_input: String, pub message_scroll_offset: usize, - pub folders: Vec, - pub selected_folder: usize, + /// None = All (основной список), Some(id) = папка с id + pub selected_folder_id: Option, pub is_loading: bool, // Search state pub is_searching: bool, @@ -49,8 +49,7 @@ impl App { current_messages: Vec::new(), message_input: String::new(), message_scroll_offset: 0, - folders: vec!["All".to_string()], - selected_folder: 0, + selected_folder_id: None, // None = All is_loading: true, is_searching: false, search_query: String::new(), @@ -58,12 +57,13 @@ impl App { } pub fn next_chat(&mut self) { - if self.chats.is_empty() { + let filtered = self.get_filtered_chats(); + if filtered.is_empty() { return; } let i = match self.chat_list_state.selected() { Some(i) => { - if i >= self.chats.len() - 1 { + if i >= filtered.len() - 1 { 0 } else { i + 1 @@ -75,13 +75,14 @@ impl App { } pub fn previous_chat(&mut self) { - if self.chats.is_empty() { + let filtered = self.get_filtered_chats(); + if filtered.is_empty() { return; } let i = match self.chat_list_state.selected() { Some(i) => { if i == 0 { - self.chats.len() - 1 + filtered.len() - 1 } else { i - 1 } @@ -92,8 +93,9 @@ impl App { } pub fn select_current_chat(&mut self) { + let filtered = self.get_filtered_chats(); if let Some(i) = self.chat_list_state.selected() { - if let Some(chat) = self.chats.get(i) { + if let Some(chat) = filtered.get(i) { self.selected_chat_id = Some(chat.id); } } @@ -134,12 +136,20 @@ 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 + .iter() + .filter(|c| c.folder_ids.contains(&folder_id)) + .collect(), + }; + if self.search_query.is_empty() { - self.chats.iter().collect() + folder_filtered } else { let query = self.search_query.to_lowercase(); - self.chats - .iter() + folder_filtered + .into_iter() .filter(|c| { // Поиск по названию чата c.title.to_lowercase().contains(&query) || diff --git a/src/input/main_input.rs b/src/input/main_input.rs index 2fa76b0..4812e86 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -4,7 +4,6 @@ use tokio::time::timeout; use crate::app::App; pub async fn handle(app: &mut App, key: KeyEvent) { - let has_super = key.modifiers.contains(KeyModifiers::SUPER); let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL); // Глобальные команды (работают всегда) @@ -74,56 +73,6 @@ pub async fn handle(app: &mut App, key: KeyEvent) { return; } - // Cmd+j/k - навигация (работает и в списке чатов, и для скролла сообщений) - if has_super { - match key.code { - // Cmd+Down - вниз (следующий чат ИЛИ скролл вниз) - KeyCode::Down => { - if app.selected_chat_id.is_some() { - // В открытом чате - скролл вниз (к новым сообщениям) - if app.message_scroll_offset > 0 { - app.message_scroll_offset = app.message_scroll_offset.saturating_sub(3); - } - } else { - // В списке чатов - следующий чат - app.next_chat(); - } - } - // Cmd+Up - вверх (предыдущий чат ИЛИ скролл вверх) - KeyCode::Up => { - if app.selected_chat_id.is_some() { - // В открытом чате - скролл вверх (к старым сообщениям) - app.message_scroll_offset += 3; - - // Проверяем, нужно ли подгрузить старые сообщения - if !app.current_messages.is_empty() { - let oldest_msg_id = app.current_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.current_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 { - if !older.is_empty() { - // Добавляем старые сообщения в начало - let mut new_messages = older; - new_messages.extend(app.current_messages.drain(..)); - app.current_messages = new_messages; - } - } - } - } - } - } else { - // В списке чатов - предыдущий чат - app.previous_chat(); - } - } - _ => {} - } - return; - } // Enter - открыть чат или отправить сообщение @@ -188,7 +137,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) { return; } - // Ввод текста в режиме открытого чата + // Режим открытого чата if app.selected_chat_id.is_some() { match key.code { KeyCode::Backspace => { @@ -197,6 +146,38 @@ pub async fn handle(app: &mut App, key: KeyEvent) { KeyCode::Char(c) => { app.message_input.push(c); } + // Стрелки - скролл сообщений + KeyCode::Down => { + // Скролл вниз (к новым сообщениям) + if app.message_scroll_offset > 0 { + app.message_scroll_offset = app.message_scroll_offset.saturating_sub(3); + } + } + KeyCode::Up => { + // Скролл вверх (к старым сообщениям) + app.message_scroll_offset += 3; + + // Проверяем, нужно ли подгрузить старые сообщения + if !app.current_messages.is_empty() { + let oldest_msg_id = app.current_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.current_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 { + if !older.is_empty() { + // Добавляем старые сообщения в начало + let mut new_messages = older; + new_messages.extend(app.current_messages.drain(..)); + app.current_messages = new_messages; + } + } + } + } + } + } _ => {} } } else { @@ -208,12 +189,24 @@ pub async fn handle(app: &mut App, key: KeyEvent) { KeyCode::Up => { app.previous_chat(); } - // Цифры - переключение папок + // Цифры 1-9 - переключение папок KeyCode::Char(c) if c >= '1' && c <= '9' => { - let folder_idx = (c as usize) - ('1' as usize); - if folder_idx < app.folders.len() { - app.selected_folder = folder_idx; + let folder_num = (c as usize) - ('1' as usize); // 0-based + if folder_num == 0 { + // 1 = All + app.selected_folder_id = None; + } else { + // 2, 3, 4... = папки из TDLib + if let Some(folder) = app.td_client.folders.get(folder_num - 1) { + let folder_id = folder.id; + 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; + app.status_message = None; + } } + app.chat_list_state.select(Some(0)); } _ => {} } diff --git a/src/tdlib/client.rs b/src/tdlib/client.rs index 04db9eb..6d12fb8 100644 --- a/src/tdlib/client.rs +++ b/src/tdlib/client.rs @@ -1,6 +1,6 @@ use std::env; use std::collections::HashMap; -use tdlib_rs::enums::{AuthorizationState, ChatList, ChatType, MessageContent, Update, User}; +use tdlib_rs::enums::{AuthorizationState, ChatList, ChatType, MessageContent, Update, User, UserStatus}; use tdlib_rs::functions; use tdlib_rs::types::{Chat as TdChat, Message as TdMessage}; @@ -29,6 +29,8 @@ pub struct ChatInfo { pub order: i64, /// ID последнего прочитанного исходящего сообщения (для галочек) pub last_read_outbox_message_id: i64, + /// ID папок, в которых находится чат + pub folder_ids: Vec, } #[derive(Debug, Clone)] @@ -41,6 +43,29 @@ pub struct MessageInfo { pub is_read: bool, } +#[derive(Debug, Clone)] +pub struct FolderInfo { + pub id: i32, + pub name: String, +} + +/// Онлайн-статус пользователя +#[derive(Debug, Clone, PartialEq)] +pub enum UserOnlineStatus { + /// Онлайн + Online, + /// Был недавно (менее часа назад) + Recently, + /// Был на этой неделе + LastWeek, + /// Был в этом месяце + LastMonth, + /// Давно не был + LongTimeAgo, + /// Оффлайн с указанием времени (unix timestamp) + Offline(i32), +} + pub struct TdClient { pub auth_state: AuthState, pub api_id: i32, @@ -60,6 +85,12 @@ pub struct TdClient { pub pending_view_messages: Vec<(i64, Vec)>, /// Очередь user_id для загрузки имён pub pending_user_ids: Vec, + /// Папки чатов + pub folders: Vec, + /// Позиция основного списка среди папок + pub main_chat_list_position: i32, + /// Онлайн-статусы пользователей: user_id -> status + user_statuses: HashMap, } #[allow(dead_code)] @@ -86,6 +117,9 @@ impl TdClient { chat_user_ids: HashMap::new(), pending_view_messages: Vec::new(), pending_user_ids: Vec::new(), + folders: Vec::new(), + main_chat_list_position: 0, + user_statuses: HashMap::new(), } } @@ -97,6 +131,13 @@ impl TdClient { self.client_id } + /// Получение онлайн-статуса пользователя по chat_id (для приватных чатов) + pub fn get_user_status_by_chat_id(&self, chat_id: i64) -> Option<&UserOnlineStatus> { + self.chat_user_ids + .get(&chat_id) + .and_then(|user_id| self.user_statuses.get(user_id)) + } + /// Инициализация TDLib с параметрами pub async fn init(&mut self) -> Result<(), String> { let result = functions::set_tdlib_parameters( @@ -193,9 +234,22 @@ impl TdClient { // Пересортируем по order self.chats.sort_by(|a, b| b.order.cmp(&a.order)); } - ChatList::Archive | ChatList::Folder(_) => { - // Если чат добавляется в архив или папку, ничего не делаем - // (он уже должен быть удалён из Main) + ChatList::Folder(folder) => { + // Обновляем folder_ids для чата + if let Some(chat) = self.chats.iter_mut().find(|c| c.id == update.chat_id) { + if update.position.order == 0 { + // Чат удалён из папки + chat.folder_ids.retain(|&id| id != folder.chat_folder_id); + } else { + // Чат добавлен в папку + if !chat.folder_ids.contains(&folder.chat_folder_id) { + chat.folder_ids.push(folder.chat_folder_id); + } + } + } + } + ChatList::Archive => { + // Архив пока не обрабатываем } } } @@ -220,6 +274,16 @@ impl TdClient { // Сохраняем имя и username пользователя let user = update.user; + // Пропускаем удалённые аккаунты (пустое имя) + 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) + }); + return; + } + // Сохраняем display name (first_name + last_name) let display_name = if user.last_name.is_empty() { user.first_name.clone() @@ -243,6 +307,30 @@ impl TdClient { } } } + Update::ChatFolders(update) => { + // Обновляем список папок + self.folders = update + .chat_folders + .into_iter() + .map(|f| FolderInfo { + id: f.id, + name: f.title, + }) + .collect(); + self.main_chat_list_position = update.main_chat_list_position; + } + Update::UserStatus(update) => { + // Обновляем онлайн-статус пользователя + let status = match update.status { + UserStatus::Online(_) => UserOnlineStatus::Online, + UserStatus::Offline(offline) => UserOnlineStatus::Offline(offline.was_online), + UserStatus::Recently(_) => UserOnlineStatus::Recently, + UserStatus::LastWeek(_) => UserOnlineStatus::LastWeek, + UserStatus::LastMonth(_) => UserOnlineStatus::LastMonth, + UserStatus::Empty => UserOnlineStatus::LongTimeAgo, + }; + self.user_statuses.insert(update.user_id, status); + } _ => {} } } @@ -260,6 +348,13 @@ impl TdClient { } fn add_or_update_chat(&mut self, td_chat: &TdChat) { + // Пропускаем удалённые аккаунты + if td_chat.title == "Deleted Account" || td_chat.title.is_empty() { + // Удаляем из списка если уже был добавлен + self.chats.retain(|c| c.id != td_chat.id); + return; + } + // Ищем позицию в Main списке (если есть) let main_position = td_chat.positions.iter().find(|pos| { matches!(pos.list, ChatList::Main) @@ -286,6 +381,19 @@ impl TdClient { _ => None, }; + // Извлекаем ID папок из позиций + let folder_ids: Vec = td_chat + .positions + .iter() + .filter_map(|pos| { + if let ChatList::Folder(folder) = &pos.list { + Some(folder.chat_folder_id) + } else { + None + } + }) + .collect(); + let chat_info = ChatInfo { id: td_chat.id, title: td_chat.title.clone(), @@ -296,6 +404,7 @@ impl TdClient { is_pinned, order, last_read_outbox_message_id: td_chat.last_read_outbox_message_id, + folder_ids, }; if let Some(existing) = self.chats.iter_mut().find(|c| c.id == td_chat.id) { @@ -304,6 +413,7 @@ impl TdClient { existing.last_message_date = chat_info.last_message_date; existing.unread_count = chat_info.unread_count; existing.last_read_outbox_message_id = chat_info.last_read_outbox_message_id; + existing.folder_ids = chat_info.folder_ids; // Обновляем username если он появился if chat_info.username.is_some() { existing.username = chat_info.username; @@ -417,6 +527,25 @@ 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 result = functions::load_chats( + Some(chat_list), + limit, + self.client_id, + ) + .await; + + match result { + Ok(_) => Ok(()), + Err(e) => Err(format!("Ошибка загрузки чатов папки: {:?}", e)), + } + } + /// Загрузка истории сообщений чата pub async fn get_chat_history( &mut self, diff --git a/src/tdlib/mod.rs b/src/tdlib/mod.rs index 116fb61..c39a584 100644 --- a/src/tdlib/mod.rs +++ b/src/tdlib/mod.rs @@ -1,3 +1,4 @@ pub mod client; pub use client::TdClient; +pub use client::UserOnlineStatus; diff --git a/src/ui/chat_list.rs b/src/ui/chat_list.rs index 58d460c..647e422 100644 --- a/src/ui/chat_list.rs +++ b/src/ui/chat_list.rs @@ -5,6 +5,7 @@ use ratatui::{ 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() @@ -43,7 +44,14 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { .map(|chat| { let is_selected = app.selected_chat_id == Some(chat.id); let pin_icon = if chat.is_pinned { "📌 " } else { "" }; - let prefix = if is_selected { "▌ " } else { " " }; + + // Онлайн-статус (зелёная точка для онлайн) + let status_icon = match app.td_client.get_user_status_by_chat_id(chat.id) { + Some(UserOnlineStatus::Online) => "● ", + _ => " ", + }; + + let prefix = if is_selected { "▌" } else { " " }; let username_text = chat.username.as_ref() .map(|u| format!(" {}", u)) @@ -55,8 +63,13 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { String::new() }; - let content = format!("{}{}{}{}{}", prefix, pin_icon, chat.title, username_text, unread_badge); - let style = Style::default().fg(Color::White); + let content = format!("{}{}{}{}{}{}", prefix, status_icon, pin_icon, chat.title, username_text, unread_badge); + + // Цвет зависит от онлайн-статуса + let style = match app.td_client.get_user_status_by_chat_id(chat.id) { + Some(UserOnlineStatus::Online) => Style::default().fg(Color::Green), + _ => Style::default().fg(Color::White), + }; ListItem::new(content).style(style) }) @@ -72,9 +85,75 @@ pub fn render(f: &mut Frame, area: Rect, app: &mut App) { f.render_stateful_widget(chats_list, chat_chunks[1], &mut app.chat_list_state); - // User status - let status = Paragraph::new("[User: Online]") + // User status - показываем статус выбранного чата + let (status_text, status_color) = if let Some(chat_id) = app.selected_chat_id { + 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::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), + None => ("".to_string(), Color::DarkGray), // Для групп/каналов + } + } else { + // Показываем статус выделенного в списке чата + let filtered = app.get_filtered_chats(); + if let Some(i) = app.chat_list_state.selected() { + 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::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), + None => ("".to_string(), Color::DarkGray), + } + } else { + ("".to_string(), Color::DarkGray) + } + } else { + ("".to_string(), Color::DarkGray) + } + }; + + let status = Paragraph::new(status_text) .block(Block::default().borders(Borders::ALL)) - .style(Style::default().fg(Color::Green)); + .style(Style::default().fg(status_color)); f.render_widget(status, chat_chunks[2]); } + +/// Форматирование времени "был(а) в ..." +fn format_was_online(timestamp: i32) -> String { + use std::time::{SystemTime, UNIX_EPOCH}; + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() as i32; + + let diff = now - timestamp; + + if diff < 60 { + "был(а) только что".to_string() + } else if diff < 3600 { + let mins = diff / 60; + format!("был(а) {} мин. назад", mins) + } else if diff < 86400 { + let hours = diff / 3600; + format!("был(а) {} ч. назад", hours) + } else { + // Показываем дату + let datetime = chrono::DateTime::from_timestamp(timestamp as i64, 0) + .map(|dt| dt.format("%d.%m %H:%M").to_string()) + .unwrap_or_else(|| "давно".to_string()); + format!("был(а) {}", datetime) + } +} diff --git a/src/ui/main_screen.rs b/src/ui/main_screen.rs index 85bb007..a6ca668 100644 --- a/src/ui/main_screen.rs +++ b/src/ui/main_screen.rs @@ -36,19 +36,29 @@ pub fn render(f: &mut Frame, app: &mut App) { fn render_folders(f: &mut Frame, area: Rect, app: &App) { let mut spans = vec![]; - for (i, folder) in app.folders.iter().enumerate() { - let style = if i == app.selected_folder { + // "All" всегда первая (клавиша 1) + let all_style = if app.selected_folder_id.is_none() { + Style::default() + .fg(Color::Yellow) + .add_modifier(Modifier::BOLD) + } else { + Style::default().fg(Color::White) + }; + spans.push(Span::styled(" 1:All ", all_style)); + + // Папки из TDLib (клавиши 2, 3, 4...) + for (i, folder) in app.td_client.folders.iter().enumerate() { + spans.push(Span::raw("│")); + + let style = if app.selected_folder_id == Some(folder.id) { Style::default() - .fg(Color::Cyan) + .fg(Color::Yellow) .add_modifier(Modifier::BOLD) } else { Style::default().fg(Color::White) }; - spans.push(Span::styled(format!(" {}:{} ", i + 1, folder), style)); - if i < app.folders.len() - 1 { - spans.push(Span::raw("│")); - } + spans.push(Span::styled(format!(" {}:{} ", i + 2, folder.name), style)); } let folders_line = Line::from(spans);