From 222a21770c211d26a61b1a379ea4da7d97d103a0 Mon Sep 17 00:00:00 2001 From: Mikhail Kilin Date: Wed, 4 Feb 2026 02:48:30 +0300 Subject: [PATCH] fix: implement chunked message loading to fill screen on chat open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: - get_chat_history() загружала только один чанк (50 сообщений max) - При запросе 100 сообщений возвращалось только 50 - Экран не заполнялся полностью при открытии чата Решение: - Добавлена чанковая загрузка по TDLIB_MESSAGE_LIMIT (50) сообщений - Автоматическая подгрузка пока не достигнут запрошенный limit - Правильная сборка сообщений (старые чанки вставляются в начало) - Retry логика для каждого чанка (до 3 попыток) Изменения в src/tdlib/messages.rs: - get_chat_history(): цикл загрузки чанков вместо одного запроса - Вставка более старых чанков в начало списка (splice) - Обработка edge cases (пустые результаты, ошибки, конец истории) Тесты: - test_chat_history_chunked_loading: проверка загрузки 100, 120, 200 сообщений - Проверка правильного порядка сообщений (от старых к новым) - Проверка границы между чанками (messages 50/51) Все тесты пройдены: 343/343 ✅ Co-Authored-By: Claude Sonnet 4.5 --- src/tdlib/messages.rs | 130 +++++++++++++++++++++++++++++------------- tests/chat_list.rs | 67 +++++++++++++++++++++- 2 files changed, 155 insertions(+), 42 deletions(-) diff --git a/src/tdlib/messages.rs b/src/tdlib/messages.rs index 91e0cbc..9367204 100644 --- a/src/tdlib/messages.rs +++ b/src/tdlib/messages.rs @@ -97,21 +97,20 @@ impl MessageManager { } } - /// Загружает историю сообщений чата. + /// Загружает историю сообщений чата с автоматической чанковой подгрузкой. /// - /// Запрашивает последние сообщения из указанного чата и сохраняет их - /// в [`current_chat_messages`](Self::current_chat_messages). Делает несколько попыток - /// загрузки при неудаче. + /// Автоматически подгружает сообщения чанками по [`TDLIB_MESSAGE_LIMIT`] штук, + /// пока не будет достигнут запрошенный `limit` или пока не кончатся сообщения. /// /// # Arguments /// - /// * `chat_id` - ID чата для загрузки истории - /// * `limit` - Максимальное количество сообщений (обычно до 50) + /// * `chat_id` - ID чата + /// * `limit` - Желаемое количество сообщений (может быть > [`TDLIB_MESSAGE_LIMIT`]) /// /// # Returns /// - /// * `Ok(Vec)` - Список загруженных сообщений (от старых к новым) - /// * `Err(String)` - Ошибка загрузки после всех попыток + /// * `Ok(Vec)` - Список сообщений (от старых к новым, до `limit` штук) + /// * `Err(String)` - Ошибка загрузки /// /// # Examples /// @@ -140,47 +139,96 @@ impl MessageManager { // Он будет установлен снаружи ПОСЛЕ сохранения истории // Это предотвращает race condition с Update::NewMessage - // Пробуем загрузить несколько раз, TDLib может подгружать с сервера let mut all_messages = Vec::new(); - let max_attempts = 3; + let mut from_message_id = 0i64; // 0 = начинаем с последних сообщений + let max_retries_per_chunk = 3; - for attempt in 1..=max_attempts { - let result = functions::get_chat_history( - chat_id.as_i64(), - 0, // from_message_id (0 = from latest) - 0, // offset - limit, - false, // only_local - false means can fetch from server - self.client_id, - ) - .await; + // Загружаем чанками по TDLIB_MESSAGE_LIMIT пока не достигнем limit + while (all_messages.len() as i32) < limit { + let remaining = limit - (all_messages.len() as i32); + let chunk_size = std::cmp::min(TDLIB_MESSAGE_LIMIT, remaining); - let messages_obj = match result { - Ok(tdlib_rs::enums::Messages::Messages(obj)) => obj, - Err(e) => return Err(format!("Ошибка загрузки истории: {:?}", e)), - }; + let mut chunk_loaded = false; - // Skip empty results - if messages_obj.messages.is_empty() { - // Ждём перед следующей попыткой - if attempt < max_attempts { - sleep(Duration::from_millis(200)).await; + // Пробуем загрузить чанк несколько раз (TDLib может подгружать с сервера) + for attempt in 1..=max_retries_per_chunk { + let result = functions::get_chat_history( + chat_id.as_i64(), + from_message_id, + 0, // offset + chunk_size, + false, // only_local - false means can fetch from server + self.client_id, + ) + .await; + + let messages_obj = match result { + Ok(tdlib_rs::enums::Messages::Messages(obj)) => obj, + Err(e) => { + // При первой загрузке (from_message_id == 0) возвращаем ошибку + // При последующих чанках - прерываем цикл (возможно кончились сообщения) + if all_messages.is_empty() { + return Err(format!("Ошибка загрузки истории: {:?}", e)); + } else { + break; + } + } + }; + + // Если получили пустой результат + if messages_obj.messages.is_empty() { + // Если это первая загрузка и не последняя попытка - пробуем еще раз + if all_messages.is_empty() && attempt < max_retries_per_chunk { + sleep(Duration::from_millis(200)).await; + continue; + } + // Иначе прерываем - больше нет сообщений + break; } - continue; + + // Конвертируем сообщения (от новых к старым, потом реверсим) + let mut chunk_messages = Vec::new(); + for msg in messages_obj.messages.iter().flatten() { + if let Some(info) = self.convert_message(msg).await { + chunk_messages.push(info); + } + } + + // Реверсим чтобы получить порядок от старых к новым + chunk_messages.reverse(); + + // Добавляем загруженные сообщения + if !chunk_messages.is_empty() { + // Для следующей итерации: ID самого старого сообщения из текущего чанка + from_message_id = chunk_messages[0].id().as_i64(); + + // ВАЖНО: Вставляем чанк В НАЧАЛО списка! + // Первый чанк содержит НОВЫЕ сообщения (например 51-100) + // Второй чанк содержит СТАРЫЕ сообщения (например 1-50) + // Поэтому более старые чанки должны быть в начале списка + if all_messages.is_empty() { + // Первый чанк - просто добавляем + all_messages = chunk_messages; + } else { + // Последующие чанки - вставляем в начало + all_messages.splice(0..0, chunk_messages); + } + + chunk_loaded = true; + } + + // Если получили меньше чем chunk_size, значит это последний доступный чанк + if (messages_obj.messages.len() as i32) < chunk_size { + return Ok(all_messages); + } + + break; // Чанк успешно загружен } - // Convert messages using iterator chains (flatten removes None values) - all_messages.clear(); // Очищаем предыдущие результаты - - for msg in messages_obj.messages.iter().rev().flatten() { - if let Some(info) = self.convert_message(msg).await { - all_messages.push(info); - } + // Если чанк не загрузился после всех попыток - прерываем + if !chunk_loaded { + break; } - - // Если получили непустой результат, прекращаем попытки - // (TDLib вернёт столько сообщений, сколько доступно, до limit) - break; } Ok(all_messages) diff --git a/tests/chat_list.rs b/tests/chat_list.rs index 297086c..423c43a 100644 --- a/tests/chat_list.rs +++ b/tests/chat_list.rs @@ -4,7 +4,7 @@ mod helpers; use helpers::app_builder::TestAppBuilder; use helpers::snapshot_utils::{buffer_to_string, render_to_buffer}; -use helpers::test_data::{create_test_chat, TestChatBuilder}; +use helpers::test_data::{create_test_chat, TestChatBuilder, TestMessageBuilder}; use insta::assert_snapshot; #[test] @@ -217,6 +217,71 @@ async fn test_opening_chat_loads_many_messages() { assert_eq!(loaded_messages[49].text(), "Message 50"); } +#[tokio::test] +async fn test_chat_history_chunked_loading() { + use tele_tui::tdlib::TdClientTrait; + use tele_tui::types::ChatId; + + // Создаём чат с 120 сообщениями (больше чем TDLIB_MESSAGE_LIMIT = 50) + let chat = TestChatBuilder::new("Long History Chat", 999) + .last_message("Message 120") + .build(); + + // Создаём 120 сообщений + let messages: Vec<_> = (1..=120) + .map(|i| { + TestMessageBuilder::new(&format!("Message {}", i), i) + .sender("Friend") + .build() + }) + .collect(); + + let mut app = TestAppBuilder::new() + .with_chat(chat) + .with_messages(999, messages) + .build(); + + // Тест 1: Загружаем 100 сообщений (больше чем 50, меньше чем 120) + let chat_id = ChatId::new(999); + let loaded_messages = app.td_client.get_chat_history(chat_id, 100).await.unwrap(); + + assert_eq!( + loaded_messages.len(), + 100, + "Should load 100 messages with chunked loading. Got: {}", + loaded_messages.len() + ); + + // Проверяем что сообщения в правильном порядке (от старых к новым) + assert_eq!(loaded_messages[0].text(), "Message 1"); + assert_eq!(loaded_messages[49].text(), "Message 50"); // Граница первого чанка + assert_eq!(loaded_messages[50].text(), "Message 51"); // Начало второго чанка + assert_eq!(loaded_messages[99].text(), "Message 100"); + + // Тест 2: Загружаем все 120 сообщений + let all_messages = app.td_client.get_chat_history(chat_id, 120).await.unwrap(); + + assert_eq!( + all_messages.len(), + 120, + "Should load all 120 messages. Got: {}", + all_messages.len() + ); + + assert_eq!(all_messages[0].text(), "Message 1"); + assert_eq!(all_messages[119].text(), "Message 120"); + + // Тест 3: Запрашиваем 200 сообщений, но есть только 120 + let limited_messages = app.td_client.get_chat_history(chat_id, 200).await.unwrap(); + + assert_eq!( + limited_messages.len(), + 120, + "Should load only available 120 messages when requesting 200. Got: {}", + limited_messages.len() + ); +} + #[test] fn snapshot_chat_with_pinned() { let chat = TestChatBuilder::new("Important Chat", 123)