fix: implement chunked message loading to fill screen on chat open

Проблема:
- 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 <noreply@anthropic.com>
This commit is contained in:
Mikhail Kilin
2026-02-04 02:48:30 +03:00
parent c881f74ecb
commit 222a21770c
2 changed files with 155 additions and 42 deletions

View File

@@ -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<MessageInfo>)` - Список загруженных сообщений (от старых к новым)
/// * `Err(String)` - Ошибка загрузки после всех попыток
/// * `Ok(Vec<MessageInfo>)` - Список сообщений (от старых к новым, до `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)

View File

@@ -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)