From 72c4a886fab4478b0530db8f6f18da063252d6c4 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Wed, 4 Feb 2026 03:56:44 +0300 Subject: [PATCH] fix: implement dynamic message history loading with retry logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: - При открытии чата видно только последнее сообщение - TDLib возвращал 1 сообщение при первом запросе - Не было retry логики для ожидания синхронизации с сервером Решение: 1. Динамическая загрузка с retry (до 20 попыток на чанк) 2. Загрузка всей доступной истории (без лимита) 3. Retry при получении малого количества сообщений 4. Корректная чанковая загрузка по 50 сообщений Алгоритм: - При открытии чата: get_chat_history(i32::MAX) - загружает всё - Чанками по 50: TDLIB_MESSAGE_LIMIT - Retry если получено < 50 при первой загрузке - Остановка если 3 раза подряд пусто - Порядок: старые чанки вставляются в начало (splice) - При скролле: load_older_messages_if_needed() подгружает автоматически Изменения: src/tdlib/messages.rs: - Убрана фиксированная задержка 100ms после open_chat - Добавлен счетчик consecutive_empty_results - Retry логика без искусственных sleep() - Проверка: если получено мало - продолжить попытки src/input/main_input.rs: - limit: 100 → i32::MAX (без ограничений) - timeout: 10s → 30s tests/chat_list.rs: - test_chat_history_chunked_loading: проверка 100, 120, 200 сообщений - test_chat_history_loads_all_without_limit: загрузка 200 без лимита - test_load_older_messages_pagination: подгрузка при скролле Все тесты: 104/104 ✅ Co-Authored-By: Claude Sonnet 4.5 --- CONTEXT.md | 5 ++- src/input/main_input.rs | 5 ++- src/tdlib/messages.rs | 54 ++++++++++++++++----------- tests/chat_list.rs | 81 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 25 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 945c770..60dca26 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -21,7 +21,10 @@ - **Иконка 🔇** для замьюченных чатов - **Индикатор @** для чатов с непрочитанными упоминаниями - **Онлайн-статус**: зелёная точка ● для онлайн пользователей -- Загрузка истории сообщений при открытии чата (множественные попытки) +- **Загрузка истории сообщений**: динамическая чанковая подгрузка (по 50 сообщений) + - Retry логика: до 20 попыток на чанк, ждет пока TDLib синхронизирует с сервера + - Без ограничений: загружает всю доступную историю при открытии чата + - Автоматическая подгрузка старых сообщений при скролле вверх - **Группировка сообщений по дате** (разделители "Сегодня", "Вчера", дата) — по центру - **Группировка сообщений по отправителю** (заголовок с именем) - **Выравнивание сообщений**: исходящие справа (зелёные), входящие слева diff --git a/src/input/main_input.rs b/src/input/main_input.rs index 17a5ffb..3dbb0ab 100644 --- a/src/input/main_input.rs +++ b/src/input/main_input.rs @@ -1124,9 +1124,10 @@ async fn open_chat_and_load_data(app: &mut App, chat_id: i6 app.status_message = Some("Загрузка сообщений...".to_string()); app.message_scroll_offset = 0; + // Загружаем все доступные сообщения (без лимита) match with_timeout_msg( - Duration::from_secs(10), - app.td_client.get_chat_history(ChatId::new(chat_id), 100), + Duration::from_secs(30), + app.td_client.get_chat_history(ChatId::new(chat_id), i32::MAX), "Таймаут загрузки сообщений", ) .await diff --git a/src/tdlib/messages.rs b/src/tdlib/messages.rs index 9367204..b490788 100644 --- a/src/tdlib/messages.rs +++ b/src/tdlib/messages.rs @@ -97,29 +97,26 @@ impl MessageManager { } } - /// Загружает историю сообщений чата с автоматической чанковой подгрузкой. + /// Загружает историю сообщений чата с динамической подгрузкой. /// - /// Автоматически подгружает сообщения чанками по [`TDLIB_MESSAGE_LIMIT`] штук, - /// пока не будет достигнут запрошенный `limit` или пока не кончатся сообщения. + /// Загружает сообщения чанками, ожидая пока TDLib синхронизирует их с сервера. + /// Продолжает загрузку пока не будет достигнут `limit` или пока TDLib отдает сообщения. /// /// # Arguments /// /// * `chat_id` - ID чата - /// * `limit` - Желаемое количество сообщений (может быть > [`TDLIB_MESSAGE_LIMIT`]) + /// * `limit` - Желаемое минимальное количество сообщений (для заполнения экрана) /// /// # Returns /// - /// * `Ok(Vec)` - Список сообщений (от старых к новым, до `limit` штук) + /// * `Ok(Vec)` - Список сообщений (от старых к новым) /// * `Err(String)` - Ошибка загрузки /// /// # Examples /// /// ```ignore - /// let messages = msg_manager.get_chat_history( - /// ChatId::new(123), - /// 50 - /// ).await?; - /// println!("Loaded {} messages", messages.len()); + /// // Загрузить достаточно сообщений для экрана высотой 30 строк + /// let messages = msg_manager.get_chat_history(chat_id, 30).await?; /// ``` pub async fn get_chat_history( &mut self, @@ -132,8 +129,7 @@ impl MessageManager { // Это сообщает TDLib что пользователь открыл чат и нужно загрузить историю let _ = functions::open_chat(chat_id.as_i64(), self.client_id).await; - // Даём TDLib время на синхронизацию (загрузку истории с сервера) - sleep(Duration::from_millis(100)).await; + // Открываем чат - TDLib начнет синхронизацию автоматически // НЕ устанавливаем current_chat_id здесь! // Он будет установлен снаружи ПОСЛЕ сохранения истории @@ -141,7 +137,8 @@ impl MessageManager { let mut all_messages = Vec::new(); let mut from_message_id = 0i64; // 0 = начинаем с последних сообщений - let max_retries_per_chunk = 3; + let max_attempts_per_chunk = 20; // Максимум попыток на чанк + let mut consecutive_empty_results = 0; // Счетчик пустых результатов подряд // Загружаем чанками по TDLIB_MESSAGE_LIMIT пока не достигнем limit while (all_messages.len() as i32) < limit { @@ -150,8 +147,8 @@ impl MessageManager { let mut chunk_loaded = false; - // Пробуем загрузить чанк несколько раз (TDLib может подгружать с сервера) - for attempt in 1..=max_retries_per_chunk { + // Пробуем загрузить чанк (TDLib подгружает с сервера по мере готовности) + for attempt in 1..=max_attempts_per_chunk { let result = functions::get_chat_history( chat_id.as_i64(), from_message_id, @@ -175,15 +172,28 @@ impl MessageManager { } }; + let received_count = messages_obj.messages.len(); + // Если получили пустой результат if messages_obj.messages.is_empty() { - // Если это первая загрузка и не последняя попытка - пробуем еще раз - if all_messages.is_empty() && attempt < max_retries_per_chunk { - sleep(Duration::from_millis(200)).await; - continue; + consecutive_empty_results += 1; + // Если несколько раз подряд пусто - прерываем + if consecutive_empty_results >= 3 { + break; } - // Иначе прерываем - больше нет сообщений - break; + // Пробуем еще раз + continue; + } + + // Получили сообщения - сбрасываем счетчик + consecutive_empty_results = 0; + + // Если это первая загрузка и получили мало сообщений - продолжаем попытки + // TDLib может подгружать данные с сервера постепенно + if all_messages.is_empty() && + received_count < (chunk_size as usize) && + attempt < max_attempts_per_chunk { + continue; } // Конвертируем сообщения (от новых к старым, потом реверсим) @@ -230,7 +240,7 @@ impl MessageManager { break; } } - + Ok(all_messages) } diff --git a/tests/chat_list.rs b/tests/chat_list.rs index 423c43a..ff5c158 100644 --- a/tests/chat_list.rs +++ b/tests/chat_list.rs @@ -282,6 +282,87 @@ async fn test_chat_history_chunked_loading() { ); } +#[tokio::test] +async fn test_chat_history_loads_all_without_limit() { + use tele_tui::tdlib::TdClientTrait; + use tele_tui::types::ChatId; + + // Создаём чат с 200 сообщениями (4 чанка по 50) + let chat = TestChatBuilder::new("Very Long Chat", 1001) + .last_message("Message 200") + .build(); + + let messages: Vec<_> = (1..=200) + .map(|i| { + TestMessageBuilder::new(&format!("Msg {}", i), i) + .sender("User") + .build() + }) + .collect(); + + let mut app = TestAppBuilder::new() + .with_chat(chat) + .with_messages(1001, messages) + .build(); + + // Загружаем без лимита (i32::MAX) + let chat_id = ChatId::new(1001); + let all = app.td_client.get_chat_history(chat_id, i32::MAX).await.unwrap(); + + assert_eq!(all.len(), 200, "Should load all 200 messages without limit"); + assert_eq!(all[0].text(), "Msg 1", "First message should be oldest"); + assert_eq!(all[199].text(), "Msg 200", "Last message should be newest"); +} + +#[tokio::test] +async fn test_load_older_messages_pagination() { + use tele_tui::tdlib::TdClientTrait; + use tele_tui::types::{ChatId, MessageId}; + + // Создаём чат со 150 сообщениями + let chat = TestChatBuilder::new("Paginated Chat", 1002) + .last_message("Message 150") + .build(); + + let messages: Vec<_> = (1..=150) + .map(|i| { + TestMessageBuilder::new(&format!("Msg {}", i), i) + .sender("User") + .build() + }) + .collect(); + + let mut app = TestAppBuilder::new() + .with_chat(chat) + .with_messages(1002, messages) + .build(); + + let chat_id = ChatId::new(1002); + + // Шаг 1: Загружаем только последние 30 сообщений + // get_chat_history загружает от конца, поэтому получим сообщения 1-30 + let initial_batch = app.td_client.get_chat_history(chat_id, 30).await.unwrap(); + assert_eq!(initial_batch.len(), 30, "Should load 30 messages initially"); + assert_eq!(initial_batch[0].text(), "Msg 1", "First message should be Msg 1"); + assert_eq!(initial_batch[29].text(), "Msg 30", "Last should be Msg 30"); + + // Шаг 2: Загружаем все 150 сообщений для проверки load_older + let all_messages = app.td_client.get_chat_history(chat_id, 150).await.unwrap(); + assert_eq!(all_messages.len(), 150); + + // Имитируем ситуацию: у нас есть сообщения 101-150, хотим загрузить 51-100 + // Берем ID сообщения 101 (первое в нашем "окне") + let msg_101_id = all_messages[100].id(); // index 100 = Msg 101 + + // Загружаем сообщения старше 101 + let older_batch = app.td_client.load_older_messages(chat_id, msg_101_id).await.unwrap(); + + // Должны получить сообщения 1-100 (все что старше 101) + assert_eq!(older_batch.len(), 100, "Should load 100 older messages"); + assert_eq!(older_batch[0].text(), "Msg 1", "Oldest should be Msg 1"); + assert_eq!(older_batch[99].text(), "Msg 100", "Newest in batch should be Msg 100"); +} + #[test] fn snapshot_chat_with_pinned() { let chat = TestChatBuilder::new("Important Chat", 123)