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:
@@ -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,16 +139,24 @@ 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 {
|
||||
// Загружаем чанками по 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 mut chunk_loaded = false;
|
||||
|
||||
// Пробуем загрузить чанк несколько раз (TDLib может подгружать с сервера)
|
||||
for attempt in 1..=max_retries_per_chunk {
|
||||
let result = functions::get_chat_history(
|
||||
chat_id.as_i64(),
|
||||
0, // from_message_id (0 = from latest)
|
||||
from_message_id,
|
||||
0, // offset
|
||||
limit,
|
||||
chunk_size,
|
||||
false, // only_local - false means can fetch from server
|
||||
self.client_id,
|
||||
)
|
||||
@@ -157,32 +164,73 @@ impl MessageManager {
|
||||
|
||||
let messages_obj = match result {
|
||||
Ok(tdlib_rs::enums::Messages::Messages(obj)) => obj,
|
||||
Err(e) => return Err(format!("Ошибка загрузки истории: {:?}", e)),
|
||||
Err(e) => {
|
||||
// При первой загрузке (from_message_id == 0) возвращаем ошибку
|
||||
// При последующих чанках - прерываем цикл (возможно кончились сообщения)
|
||||
if all_messages.is_empty() {
|
||||
return Err(format!("Ошибка загрузки истории: {:?}", e));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Skip empty results
|
||||
// Если получили пустой результат
|
||||
if messages_obj.messages.is_empty() {
|
||||
// Ждём перед следующей попыткой
|
||||
if attempt < max_attempts {
|
||||
// Если это первая загрузка и не последняя попытка - пробуем еще раз
|
||||
if all_messages.is_empty() && attempt < max_retries_per_chunk {
|
||||
sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Если получили непустой результат, прекращаем попытки
|
||||
// (TDLib вернёт столько сообщений, сколько доступно, до limit)
|
||||
// Иначе прерываем - больше нет сообщений
|
||||
break;
|
||||
}
|
||||
|
||||
// Конвертируем сообщения (от новых к старым, потом реверсим)
|
||||
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; // Чанк успешно загружен
|
||||
}
|
||||
|
||||
// Если чанк не загрузился после всех попыток - прерываем
|
||||
if !chunk_loaded {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(all_messages)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user