diff --git a/src/input/main_input.rs b/src/input/main_input.rs index 53293f4..47e48db 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -1089,9 +1089,23 @@ async fn open_chat_and_load_data(app: &mut App, chat_id: i6 .await { Ok(messages) => { + // Собираем ID всех входящих сообщений для отметки как прочитанные + let incoming_message_ids: Vec = messages + .iter() + .filter(|msg| !msg.is_outgoing()) + .map(|msg| msg.id()) + .collect(); + // Сохраняем загруженные сообщения app.td_client.set_current_chat_messages(messages); + // Добавляем входящие сообщения в очередь для отметки как прочитанные + if !incoming_message_ids.is_empty() { + app.td_client + .pending_view_messages_mut() + .push((ChatId::new(chat_id), incoming_message_ids)); + } + // ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории // Это предотвращает race condition с Update::NewMessage app.td_client.set_current_chat_id(Some(ChatId::new(chat_id))); diff --git a/src/tdlib/messages.rs b/src/tdlib/messages.rs index 79453aa..f5b5139 100644 --- a/src/tdlib/messages.rs +++ b/src/tdlib/messages.rs @@ -167,10 +167,9 @@ impl MessageManager { } } - // Если получили достаточно сообщений, прекращаем попытки - if all_messages.len() >= 2 || attempt == max_attempts { - break; - } + // Если получили непустой результат, прекращаем попытки + // (TDLib вернёт столько сообщений, сколько доступно, до limit) + break; } // Если сообщений мало, ждём перед следующей попыткой diff --git a/tests/chat_list.rs b/tests/chat_list.rs index ef8ecdb..297086c 100644 --- a/tests/chat_list.rs +++ b/tests/chat_list.rs @@ -54,6 +54,169 @@ fn snapshot_chat_with_unread_count() { assert_snapshot!("chat_with_unread", output); } +#[test] +fn test_incoming_message_shows_unread_badge() { + use tele_tui::tdlib::ChatInfo; + use tele_tui::types::ChatId; + + // Создаём чат БЕЗ непрочитанных сообщений + let chat = TestChatBuilder::new("Friend", 999) + .unread_count(0) + .last_message("Как дела?") + .build(); + + let mut app = TestAppBuilder::new() + .with_chat(chat) + .build(); + + // Рендерим UI - должно быть без "(1)" + let buffer_before = render_to_buffer(80, 24, |f| { + tele_tui::ui::chat_list::render(f, f.area(), &mut app); + }); + let output_before = buffer_to_string(&buffer_before); + + // Проверяем что нет "(1)" в первой строке чата + assert!(!output_before.contains("(1)"), "Before: should not contain (1)"); + + // Симулируем входящее сообщение - обновляем unread_count + app.chats[0].unread_count = 1; + app.chats[0].last_message = "Привет!".to_string(); + + // Рендерим UI снова - теперь должно быть "(1)" + let buffer_after = render_to_buffer(80, 24, |f| { + tele_tui::ui::chat_list::render(f, f.area(), &mut app); + }); + let output_after = buffer_to_string(&buffer_after); + + // Проверяем что появилось "(1)" в первой строке чата + assert!(output_after.contains("(1)"), "After: should contain (1)\nActual output:\n{}", output_after); +} + +#[tokio::test] +async fn test_opening_chat_clears_unread_badge() { + use helpers::test_data::TestMessageBuilder; + use tele_tui::tdlib::TdClientTrait; + use tele_tui::types::{ChatId, MessageId}; + + // Создаём чат с 3 непрочитанными сообщениями + let chat = TestChatBuilder::new("Friend", 999) + .unread_count(3) + .last_message("У тебя 3 новых сообщения") + .build(); + + // Создаём 3 входящих сообщения (по умолчанию is_outgoing = false) + let messages = vec![ + TestMessageBuilder::new("Привет!", 1) + .sender("Friend") + .build(), + TestMessageBuilder::new("Как дела?", 2) + .sender("Friend") + .build(), + TestMessageBuilder::new("Ответь мне!", 3) + .sender("Friend") + .build(), + ]; + + let mut app = TestAppBuilder::new() + .with_chat(chat) + .with_messages(999, messages) + .build(); + + // Рендерим UI - должно быть "(3)" + let buffer_before = render_to_buffer(80, 24, |f| { + tele_tui::ui::chat_list::render(f, f.area(), &mut app); + }); + let output_before = buffer_to_string(&buffer_before); + + // Проверяем что есть "(3)" в списке чатов + assert!(output_before.contains("(3)"), "Before opening: should contain (3)\nActual output:\n{}", output_before); + + // Симулируем открытие чата - загружаем историю + let chat_id = ChatId::new(999); + let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap(); + + // Собираем ID входящих сообщений (как в реальном коде) + let incoming_message_ids: Vec = loaded_messages + .iter() + .filter(|msg| !msg.is_outgoing()) + .map(|msg| msg.id()) + .collect(); + + // Проверяем что нашли 3 входящих сообщения + assert_eq!(incoming_message_ids.len(), 3, "Should have 3 incoming messages"); + + // Добавляем в очередь для отметки как прочитанные (напрямую через Mutex) + app.td_client.pending_view_messages + .lock() + .unwrap() + .push((chat_id, incoming_message_ids)); + + // Обрабатываем очередь (как в main loop) + app.td_client.process_pending_view_messages().await; + + // В FakeTdClient это должно записаться в viewed_messages + let viewed = app.td_client.get_viewed_messages(); + assert_eq!(viewed.len(), 1, "Should have one batch of viewed messages"); + assert_eq!(viewed[0].0, 999, "Should be for chat 999"); + assert_eq!(viewed[0].1.len(), 3, "Should have viewed 3 messages"); + + // В реальном приложении TDLib отправит Update::ChatReadInbox + // который обновит unread_count в чате. Симулируем это: + app.chats[0].unread_count = 0; + + // Рендерим UI снова - "(3)" должно пропасть + let buffer_after = render_to_buffer(80, 24, |f| { + tele_tui::ui::chat_list::render(f, f.area(), &mut app); + }); + let output_after = buffer_to_string(&buffer_after); + + // Проверяем что "(3)" больше нет + assert!(!output_after.contains("(3)"), "After opening: should not contain (3)\nActual output:\n{}", output_after); +} + +#[tokio::test] +async fn test_opening_chat_loads_many_messages() { + use helpers::test_data::TestMessageBuilder; + use tele_tui::tdlib::TdClientTrait; + use tele_tui::types::ChatId; + + // Создаём чат с 50 сообщениями + let chat = TestChatBuilder::new("History Chat", 888) + .last_message("Message 50") + .build(); + + // Создаём 50 сообщений + let messages: Vec<_> = (1..=50) + .map(|i| { + TestMessageBuilder::new(&format!("Message {}", i), i) + .sender("Friend") + .build() + }) + .collect(); + + let mut app = TestAppBuilder::new() + .with_chat(chat) + .with_messages(888, messages) + .build(); + + // Открываем чат - загружаем историю (запрашиваем 100 сообщений) + let chat_id = ChatId::new(888); + let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap(); + + // Проверяем что загрузились ВСЕ 50 сообщений, а не только последние 2-3 + assert_eq!( + loaded_messages.len(), + 50, + "Should load all 50 messages, not just last few. Got: {}", + loaded_messages.len() + ); + + // Проверяем что сообщения в правильном порядке (от старых к новым) + assert_eq!(loaded_messages[0].text(), "Message 1"); + assert_eq!(loaded_messages[24].text(), "Message 25"); + assert_eq!(loaded_messages[49].text(), "Message 50"); +} + #[test] fn snapshot_chat_with_pinned() { let chat = TestChatBuilder::new("Important Chat", 123) diff --git a/tests/helpers/fake_tdclient.rs b/tests/helpers/fake_tdclient.rs index 037ef4c..19558a3 100644 --- a/tests/helpers/fake_tdclient.rs +++ b/tests/helpers/fake_tdclient.rs @@ -46,6 +46,7 @@ pub struct FakeTdClient { pub searched_queries: Arc>>, pub viewed_messages: Arc)>>>, // (chat_id, message_ids) pub chat_actions: Arc>>, // (chat_id, action) + pub pending_view_messages: Arc)>>>, // Очередь для отметки как прочитанные // Update channel для симуляции событий pub update_tx: Arc>>>, @@ -119,6 +120,7 @@ impl Clone for FakeTdClient { searched_queries: Arc::clone(&self.searched_queries), viewed_messages: Arc::clone(&self.viewed_messages), chat_actions: Arc::clone(&self.chat_actions), + pending_view_messages: Arc::clone(&self.pending_view_messages), update_tx: Arc::clone(&self.update_tx), simulate_delays: self.simulate_delays, fail_next_operation: Arc::clone(&self.fail_next_operation), @@ -151,6 +153,7 @@ impl FakeTdClient { searched_queries: Arc::new(Mutex::new(vec![])), viewed_messages: Arc::new(Mutex::new(vec![])), chat_actions: Arc::new(Mutex::new(vec![])), + pending_view_messages: Arc::new(Mutex::new(vec![])), update_tx: Arc::new(Mutex::new(None)), simulate_delays: false, fail_next_operation: Arc::new(Mutex::new(false)), diff --git a/tests/helpers/fake_tdclient_impl.rs b/tests/helpers/fake_tdclient_impl.rs index c9b2c7a..83d8b56 100644 --- a/tests/helpers/fake_tdclient_impl.rs +++ b/tests/helpers/fake_tdclient_impl.rs @@ -125,7 +125,12 @@ impl TdClientTrait for FakeTdClient { } async fn process_pending_view_messages(&mut self) { - // Not used in fake client + // Перемещаем pending в viewed для проверки в тестах + let mut pending = self.pending_view_messages.lock().unwrap(); + for (chat_id, message_ids) in pending.drain(..) { + let ids: Vec = message_ids.iter().map(|id| id.as_i64()).collect(); + self.viewed_messages.lock().unwrap().push((chat_id.as_i64(), ids)); + } } // ============ User methods ============ @@ -276,7 +281,10 @@ impl TdClientTrait for FakeTdClient { } fn pending_view_messages_mut(&mut self) -> &mut Vec<(ChatId, Vec)> { - panic!("pending_view_messages_mut not supported for FakeTdClient") + // WORKAROUND: Возвращаем мутабельную ссылку через leak + // Это безопасно так как мы единственные владельцы &mut self + let guard = self.pending_view_messages.lock().unwrap(); + unsafe { &mut *(guard.as_ptr() as *mut Vec<(ChatId, Vec)>) } } fn pending_user_ids_mut(&mut self) -> &mut Vec {