Merge pull request 'refactor' (#24) from refactor into main
Reviewed-on: #24
This commit was merged in pull request #24.
This commit is contained in:
@@ -164,7 +164,5 @@
|
||||
- `pending_account_switch` флаг → обработка в main loop
|
||||
|
||||
- [ ] **Этап 4: Расширенные возможности мультиаккаунта**
|
||||
- Удаление аккаунта из модалки
|
||||
- Хоткеи `Ctrl+1`..`Ctrl+9` — быстрое переключение
|
||||
- Бейджи непрочитанных с других аккаунтов (требует множественных TdClient)
|
||||
- Параллельный polling updates со всех аккаунтов
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
//! - Editing and sending messages
|
||||
//! - Loading older messages
|
||||
|
||||
use super::chat_list::open_chat_and_load_data;
|
||||
use super::chat_loader::{load_older_messages_if_needed, open_chat_and_load_data};
|
||||
use crate::app::methods::{
|
||||
compose::ComposeMethods, messages::MessageMethods, modal::ModalMethods,
|
||||
navigation::NavigationMethods,
|
||||
@@ -16,7 +16,7 @@ use crate::app::InputMode;
|
||||
use crate::input::handlers::{copy_to_clipboard, format_message_for_clipboard};
|
||||
use crate::tdlib::{ChatAction, TdClientTrait};
|
||||
use crate::types::{ChatId, MessageId};
|
||||
use crate::utils::{is_non_empty, with_timeout, with_timeout_msg};
|
||||
use crate::utils::{is_non_empty, with_timeout_msg};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
@@ -340,50 +340,6 @@ pub async fn send_reaction<T: TdClientTrait>(app: &mut App<T>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Подгружает старые сообщения если скролл близко к верху
|
||||
pub async fn load_older_messages_if_needed<T: TdClientTrait>(app: &mut App<T>) {
|
||||
// Check if there are messages to load from
|
||||
if app.td_client.current_chat_messages().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the oldest message ID
|
||||
let oldest_msg_id = app
|
||||
.td_client
|
||||
.current_chat_messages()
|
||||
.first()
|
||||
.map(|m| m.id())
|
||||
.unwrap_or(MessageId::new(0));
|
||||
|
||||
// Get current chat ID
|
||||
let Some(chat_id) = app.get_selected_chat_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check if scroll is near the top
|
||||
let message_count = app.td_client.current_chat_messages().len();
|
||||
if app.message_scroll_offset <= message_count.saturating_sub(10) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load older messages with timeout
|
||||
let Ok(older) = with_timeout(
|
||||
Duration::from_secs(3),
|
||||
app.td_client
|
||||
.load_older_messages(ChatId::new(chat_id), oldest_msg_id),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Add older messages to the beginning if any were loaded
|
||||
if !older.is_empty() {
|
||||
let msgs = app.td_client.current_chat_messages_mut();
|
||||
msgs.splice(0..0, older);
|
||||
}
|
||||
}
|
||||
|
||||
/// Обработка ввода клавиатуры в открытом чате
|
||||
///
|
||||
/// Обрабатывает:
|
||||
|
||||
@@ -5,14 +5,10 @@
|
||||
//! - Folder selection
|
||||
//! - Opening chats
|
||||
|
||||
use crate::app::methods::{
|
||||
compose::ComposeMethods, messages::MessageMethods, navigation::NavigationMethods,
|
||||
};
|
||||
use crate::app::methods::navigation::NavigationMethods;
|
||||
use crate::app::App;
|
||||
use crate::app::InputMode;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::types::{ChatId, MessageId};
|
||||
use crate::utils::{with_timeout, with_timeout_msg};
|
||||
use crate::utils::with_timeout;
|
||||
use crossterm::event::KeyEvent;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -79,62 +75,3 @@ pub async fn select_folder<T: TdClientTrait>(app: &mut App<T>, folder_idx: usize
|
||||
}
|
||||
}
|
||||
|
||||
/// Открывает чат и загружает последние сообщения (быстро).
|
||||
///
|
||||
/// Загружает только 50 последних сообщений для мгновенного отображения.
|
||||
/// Фоновые задачи (reply info, pinned, photos) откладываются в `pending_chat_init`
|
||||
/// и выполняются на следующем тике main loop.
|
||||
///
|
||||
/// При ошибке устанавливает error_message и очищает status_message.
|
||||
pub async fn open_chat_and_load_data<T: TdClientTrait>(app: &mut App<T>, chat_id: i64) {
|
||||
app.status_message = Some("Загрузка сообщений...".to_string());
|
||||
app.message_scroll_offset = 0;
|
||||
|
||||
// Загружаем только 50 последних сообщений (один запрос к TDLib)
|
||||
match with_timeout_msg(
|
||||
Duration::from_secs(10),
|
||||
app.td_client.get_chat_history(ChatId::new(chat_id), 50),
|
||||
"Таймаут загрузки сообщений",
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(messages) => {
|
||||
// Собираем ID всех входящих сообщений для отметки как прочитанные
|
||||
let incoming_message_ids: Vec<MessageId> = 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)));
|
||||
|
||||
// Загружаем черновик (локальная операция, мгновенно)
|
||||
app.load_draft();
|
||||
|
||||
// Показываем чат СРАЗУ
|
||||
app.status_message = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
app.start_message_selection();
|
||||
|
||||
// Фоновые задачи (reply info, pinned, photos) — на следующем тике main loop
|
||||
app.pending_chat_init = Some(ChatId::new(chat_id));
|
||||
}
|
||||
Err(e) => {
|
||||
app.error_message = Some(e);
|
||||
app.status_message = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
194
src/input/handlers/chat_loader.rs
Normal file
194
src/input/handlers/chat_loader.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Chat loading logic — all three phases of message loading
|
||||
//!
|
||||
//! - Phase 1: `open_chat_and_load_data` — fast initial load (50 messages)
|
||||
//! - Phase 2: `process_pending_chat_init` — background tasks (reply info, pinned, photos)
|
||||
//! - Phase 3: `load_older_messages_if_needed` — lazy load on scroll up
|
||||
|
||||
use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods};
|
||||
use crate::app::App;
|
||||
use crate::app::InputMode;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::types::{ChatId, MessageId};
|
||||
use crate::utils::{with_timeout, with_timeout_ignore, with_timeout_msg};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Открывает чат и загружает последние сообщения (быстро).
|
||||
///
|
||||
/// Загружает только 50 последних сообщений для мгновенного отображения.
|
||||
/// Фоновые задачи (reply info, pinned, photos) откладываются в `pending_chat_init`
|
||||
/// и выполняются на следующем тике main loop.
|
||||
///
|
||||
/// При ошибке устанавливает error_message и очищает status_message.
|
||||
pub async fn open_chat_and_load_data<T: TdClientTrait>(app: &mut App<T>, chat_id: i64) {
|
||||
app.status_message = Some("Загрузка сообщений...".to_string());
|
||||
app.message_scroll_offset = 0;
|
||||
|
||||
// Загружаем только 50 последних сообщений (один запрос к TDLib)
|
||||
match with_timeout_msg(
|
||||
Duration::from_secs(10),
|
||||
app.td_client.get_chat_history(ChatId::new(chat_id), 50),
|
||||
"Таймаут загрузки сообщений",
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(messages) => {
|
||||
// Собираем ID всех входящих сообщений для отметки как прочитанные
|
||||
let incoming_message_ids: Vec<MessageId> = 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)));
|
||||
|
||||
// Загружаем черновик (локальная операция, мгновенно)
|
||||
app.load_draft();
|
||||
|
||||
// Показываем чат СРАЗУ
|
||||
app.status_message = None;
|
||||
app.input_mode = InputMode::Normal;
|
||||
app.start_message_selection();
|
||||
|
||||
// Фоновые задачи (reply info, pinned, photos) — на следующем тике main loop
|
||||
app.pending_chat_init = Some(ChatId::new(chat_id));
|
||||
}
|
||||
Err(e) => {
|
||||
app.error_message = Some(e);
|
||||
app.status_message = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Выполняет фоновую инициализацию после открытия чата.
|
||||
///
|
||||
/// Вызывается на следующем тике main loop после `open_chat_and_load_data`.
|
||||
/// Загружает reply info, закреплённое сообщение и начинает авто-загрузку фото.
|
||||
pub async fn process_pending_chat_init<T: TdClientTrait>(app: &mut App<T>, chat_id: ChatId) {
|
||||
// Загружаем недостающие reply info (игнорируем ошибки)
|
||||
with_timeout_ignore(Duration::from_secs(5), app.td_client.fetch_missing_reply_info())
|
||||
.await;
|
||||
|
||||
// Загружаем последнее закреплённое сообщение (игнорируем ошибки)
|
||||
with_timeout_ignore(
|
||||
Duration::from_secs(2),
|
||||
app.td_client.load_current_pinned_message(chat_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Авто-загрузка фото — неблокирующая фоновая задача (до 5 фото параллельно)
|
||||
#[cfg(feature = "images")]
|
||||
{
|
||||
use crate::tdlib::PhotoDownloadState;
|
||||
|
||||
if app.config().images.auto_download_images && app.config().images.show_images {
|
||||
let photo_file_ids: Vec<i32> = app
|
||||
.td_client
|
||||
.current_chat_messages()
|
||||
.iter()
|
||||
.rev()
|
||||
.take(5)
|
||||
.filter_map(|msg| {
|
||||
msg.photo_info().and_then(|p| {
|
||||
matches!(p.download_state, PhotoDownloadState::NotDownloaded)
|
||||
.then_some(p.file_id)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !photo_file_ids.is_empty() {
|
||||
let client_id = app.td_client.client_id();
|
||||
let (tx, rx) =
|
||||
tokio::sync::mpsc::unbounded_channel::<(i32, Result<String, String>)>();
|
||||
app.photo_download_rx = Some(rx);
|
||||
|
||||
for file_id in photo_file_ids {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = tokio::time::timeout(Duration::from_secs(5), async {
|
||||
match tdlib_rs::functions::download_file(
|
||||
file_id, 1, 0, 0, true, client_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tdlib_rs::enums::File::File(file))
|
||||
if file.local.is_downloading_completed
|
||||
&& !file.local.path.is_empty() =>
|
||||
{
|
||||
Ok(file.local.path)
|
||||
}
|
||||
Ok(_) => Err("Файл не скачан".to_string()),
|
||||
Err(e) => Err(format!("{:?}", e)),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let result = match result {
|
||||
Ok(r) => r,
|
||||
Err(_) => Err("Таймаут загрузки".to_string()),
|
||||
};
|
||||
let _ = tx.send((file_id, result));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
|
||||
/// Подгружает старые сообщения если скролл близко к верху
|
||||
pub async fn load_older_messages_if_needed<T: TdClientTrait>(app: &mut App<T>) {
|
||||
// Check if there are messages to load from
|
||||
if app.td_client.current_chat_messages().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the oldest message ID
|
||||
let oldest_msg_id = app
|
||||
.td_client
|
||||
.current_chat_messages()
|
||||
.first()
|
||||
.map(|m| m.id())
|
||||
.unwrap_or(MessageId::new(0));
|
||||
|
||||
// Get current chat ID
|
||||
let Some(chat_id) = app.get_selected_chat_id() else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Check if scroll is near the top
|
||||
let message_count = app.td_client.current_chat_messages().len();
|
||||
if app.message_scroll_offset <= message_count.saturating_sub(10) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load older messages with timeout
|
||||
let Ok(older) = with_timeout(
|
||||
Duration::from_secs(3),
|
||||
app.td_client
|
||||
.load_older_messages(ChatId::new(chat_id), oldest_msg_id),
|
||||
)
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Add older messages to the beginning if any were loaded
|
||||
if !older.is_empty() {
|
||||
let msgs = app.td_client.current_chat_messages_mut();
|
||||
msgs.splice(0..0, older);
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,14 @@
|
||||
//! - profile: Profile helper functions
|
||||
//! - chat: Keyboard input handling for open chat view
|
||||
//! - chat_list: Navigation and interaction in the chat list
|
||||
//! - chat_loader: All phases of chat message loading
|
||||
//! - compose: Text input, editing, and message composition
|
||||
//! - modal: Modal dialogs (delete confirmation, emoji picker, etc.)
|
||||
//! - search: Search functionality (chat search, message search)
|
||||
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
pub mod chat_loader;
|
||||
pub mod clipboard;
|
||||
pub mod compose;
|
||||
pub mod global;
|
||||
@@ -19,6 +21,7 @@ pub mod modal;
|
||||
pub mod profile;
|
||||
pub mod search;
|
||||
|
||||
pub use chat_loader::{load_older_messages_if_needed, open_chat_and_load_data, process_pending_chat_init};
|
||||
pub use clipboard::*;
|
||||
pub use global::*;
|
||||
pub use profile::get_available_actions_count;
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::utils::with_timeout;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::chat_list::open_chat_and_load_data;
|
||||
use super::chat_loader::open_chat_and_load_data;
|
||||
use super::scroll_to_message;
|
||||
|
||||
/// Обработка режима поиска по чатам
|
||||
|
||||
72
src/main.rs
72
src/main.rs
@@ -29,6 +29,7 @@ use tdlib_rs::enums::Update;
|
||||
use app::{App, AppScreen};
|
||||
use constants::{POLL_TIMEOUT_MS, SHUTDOWN_TIMEOUT_SECS};
|
||||
use input::{handle_auth_input, handle_main_input};
|
||||
use input::handlers::process_pending_chat_init;
|
||||
use tdlib::AuthState;
|
||||
use utils::{disable_tdlib_logs, with_timeout_ignore};
|
||||
|
||||
@@ -345,76 +346,7 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
|
||||
|
||||
// Process pending chat initialization (reply info, pinned, photos)
|
||||
if let Some(chat_id) = app.pending_chat_init.take() {
|
||||
// Загружаем недостающие reply info (игнорируем ошибки)
|
||||
with_timeout_ignore(Duration::from_secs(5), app.td_client.fetch_missing_reply_info())
|
||||
.await;
|
||||
|
||||
// Загружаем последнее закреплённое сообщение (игнорируем ошибки)
|
||||
with_timeout_ignore(
|
||||
Duration::from_secs(2),
|
||||
app.td_client.load_current_pinned_message(chat_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Авто-загрузка фото — неблокирующая фоновая задача (до 5 фото параллельно)
|
||||
#[cfg(feature = "images")]
|
||||
{
|
||||
use crate::tdlib::PhotoDownloadState;
|
||||
|
||||
if app.config().images.auto_download_images && app.config().images.show_images {
|
||||
let photo_file_ids: Vec<i32> = app
|
||||
.td_client
|
||||
.current_chat_messages()
|
||||
.iter()
|
||||
.rev()
|
||||
.take(5)
|
||||
.filter_map(|msg| {
|
||||
msg.photo_info().and_then(|p| {
|
||||
matches!(p.download_state, PhotoDownloadState::NotDownloaded)
|
||||
.then_some(p.file_id)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !photo_file_ids.is_empty() {
|
||||
let client_id = app.td_client.client_id();
|
||||
let (tx, rx) =
|
||||
tokio::sync::mpsc::unbounded_channel::<(i32, Result<String, String>)>();
|
||||
app.photo_download_rx = Some(rx);
|
||||
|
||||
for file_id in photo_file_ids {
|
||||
let tx = tx.clone();
|
||||
tokio::spawn(async move {
|
||||
let result = tokio::time::timeout(Duration::from_secs(5), async {
|
||||
match tdlib_rs::functions::download_file(
|
||||
file_id, 1, 0, 0, true, client_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(tdlib_rs::enums::File::File(file))
|
||||
if file.local.is_downloading_completed
|
||||
&& !file.local.path.is_empty() =>
|
||||
{
|
||||
Ok(file.local.path)
|
||||
}
|
||||
Ok(_) => Err("Файл не скачан".to_string()),
|
||||
Err(e) => Err(format!("{:?}", e)),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let result = match result {
|
||||
Ok(r) => r,
|
||||
Err(_) => Err("Таймаут загрузки".to_string()),
|
||||
};
|
||||
let _ = tx.send((file_id, result));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
app.needs_redraw = true;
|
||||
process_pending_chat_init(app, chat_id).await;
|
||||
}
|
||||
|
||||
// Check pending account switch
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut App<T>) {
|
||||
let search_style = if app.is_searching {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
Style::default().fg(Color::Rgb(160, 160, 160))
|
||||
};
|
||||
let search = Paragraph::new(search_text)
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
|
||||
@@ -19,12 +19,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
NetworkState::Updating => "⏳ Обновление... | ",
|
||||
};
|
||||
|
||||
// Account indicator (shown if not "default")
|
||||
let account_indicator = if app.current_account_name != "default" {
|
||||
format!("[{}] ", app.current_account_name)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let account_indicator = format!("[{}] ", app.current_account_name);
|
||||
|
||||
let status = if let Some(msg) = &app.status_message {
|
||||
format!(" {}{}{} ", account_indicator, network_indicator, msg)
|
||||
@@ -57,7 +52,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
|
||||
} else if app.status_message.is_some() {
|
||||
Style::default().fg(Color::Yellow)
|
||||
} else {
|
||||
Style::default().fg(Color::DarkGray)
|
||||
Style::default().fg(Color::Rgb(160, 160, 160))
|
||||
};
|
||||
|
||||
let footer = Paragraph::new(status).style(style);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 22
|
||||
expression: output
|
||||
---
|
||||
Инициализация TDLib...
|
||||
[default] Инициализация TDLib...
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 90
|
||||
expression: output
|
||||
---
|
||||
⏳ Подключение... | Инициализация TDLib...
|
||||
[default] ⏳ Подключение... | Инициализация TDLib...
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 73
|
||||
expression: output
|
||||
---
|
||||
⏳ Прокси... | Инициализация TDLib...
|
||||
[default] ⏳ Прокси... | Инициализация TDLib...
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 56
|
||||
expression: output
|
||||
---
|
||||
⚠ Нет сети | Инициализация TDLib...
|
||||
[default] ⚠ Нет сети | Инициализация TDLib...
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 39
|
||||
expression: output
|
||||
---
|
||||
Инициализация TDLib...
|
||||
[default] Инициализация TDLib...
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/footer.rs
|
||||
assertion_line: 107
|
||||
expression: output
|
||||
---
|
||||
Инициализация TDLib...
|
||||
[default] Инициализация TDLib...
|
||||
|
||||
Reference in New Issue
Block a user