refactor: implement trait-based DI for TdClient and fix stack overflow
Implement complete trait-based dependency injection pattern for TdClient to enable testing with FakeTdClient mock. Fix critical stack overflow bugs caused by infinite recursion in trait implementations. Breaking Changes: - App is now generic: App<T: TdClientTrait = TdClient> - All UI and input handlers are generic over TdClientTrait - TdClient methods now accessed through trait interface New Files: - src/tdlib/trait.rs: TdClientTrait definition with 40+ methods - src/tdlib/client_impl.rs: TdClientTrait impl for TdClient - tests/helpers/fake_tdclient_impl.rs: TdClientTrait impl for FakeTdClient Critical Fixes: - Fix stack overflow in send_message, edit_message, delete_messages - Fix stack overflow in forward_messages, current_chat_messages - Fix stack overflow in current_pinned_message - All methods now call message_manager directly to avoid recursion Testing: - FakeTdClient supports configurable auth_state for auth screen tests - Added pinned message support in FakeTdClient - All 196+ tests passing (188 tests + 8 benchmarks) Dependencies: - Added async-trait = "0.1" Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::AuthState;
|
||||
use crate::tdlib::{AuthState, TdClientTrait};
|
||||
use crossterm::event::KeyCode;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
pub async fn handle(app: &mut App, key_code: KeyCode) {
|
||||
pub async fn handle<T: TdClientTrait>(app: &mut App<T>, key_code: KeyCode) {
|
||||
match &app.td_client.auth_state() {
|
||||
AuthState::WaitPhoneNumber => match key_code {
|
||||
KeyCode::Char(c) => {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Chat list navigation input handling
|
||||
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
/// Обрабатывает ввод в списке чатов
|
||||
pub async fn handle_chat_list_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_chat_list_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement chat list input handling
|
||||
let _ = (app, key);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//! - Ctrl+F: Search messages in chat
|
||||
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::types::ChatId;
|
||||
use crate::utils::{with_timeout, with_timeout_msg};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
@@ -17,7 +18,7 @@ use std::time::Duration;
|
||||
/// # Returns
|
||||
///
|
||||
/// `true` если команда была обработана, `false` если нет
|
||||
pub async fn handle_global_commands(app: &mut App, key: KeyEvent) -> bool {
|
||||
pub async fn handle_global_commands<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) -> bool {
|
||||
let has_ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
|
||||
|
||||
match key.code {
|
||||
@@ -55,7 +56,7 @@ pub async fn handle_global_commands(app: &mut App, key: KeyEvent) -> bool {
|
||||
}
|
||||
|
||||
/// Обрабатывает загрузку и отображение закреплённых сообщений
|
||||
async fn handle_pinned_messages(app: &mut App) {
|
||||
async fn handle_pinned_messages<T: TdClientTrait>(app: &mut App<T>) {
|
||||
if app.selected_chat_id.is_some() && !app.is_pinned_mode() {
|
||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||
app.status_message = Some("Загрузка закреплённых...".to_string());
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Message input handling when chat is open
|
||||
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
/// Обрабатывает ввод когда открыт чат
|
||||
pub async fn handle_messages_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_messages_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement messages input handling
|
||||
let _ = (app, key);
|
||||
}
|
||||
|
||||
@@ -7,28 +7,29 @@
|
||||
//! - Forward mode
|
||||
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
/// Обрабатывает ввод в режиме закреплённых сообщений
|
||||
pub async fn handle_pinned_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_pinned_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement pinned messages input handling
|
||||
let _ = (app, key);
|
||||
}
|
||||
|
||||
/// Обрабатывает ввод в режиме выбора реакции
|
||||
pub async fn handle_reaction_picker_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_reaction_picker_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement reaction picker input handling
|
||||
let _ = (app, key);
|
||||
}
|
||||
|
||||
/// Обрабатывает ввод в режиме подтверждения удаления
|
||||
pub async fn handle_delete_confirmation_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_delete_confirmation_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement delete confirmation input handling
|
||||
let _ = (app, key);
|
||||
}
|
||||
|
||||
/// Обрабатывает ввод в режиме пересылки
|
||||
pub async fn handle_forward_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_forward_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement forward mode input handling
|
||||
let _ = (app, key);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
//! Profile mode input handling
|
||||
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
/// Обрабатывает ввод в режиме профиля
|
||||
pub async fn handle_profile_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_profile_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement profile input handling
|
||||
// Временно делегируем обратно в main_input
|
||||
let _ = (app, key);
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
//! Search mode input handling (chat search and message search)
|
||||
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crossterm::event::KeyEvent;
|
||||
|
||||
/// Обрабатывает ввод в режиме поиска чатов
|
||||
pub async fn handle_chat_search_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_chat_search_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement chat search input handling
|
||||
let _ = (app, key);
|
||||
}
|
||||
|
||||
/// Обрабатывает ввод в режиме поиска сообщений
|
||||
pub async fn handle_message_search_input(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle_message_search_input<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// TODO: Implement message search input handling
|
||||
let _ = (app, key);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::app::App;
|
||||
use crate::tdlib::TdClientTrait;
|
||||
use crate::input::handlers::{
|
||||
copy_to_clipboard, format_message_for_clipboard, get_available_actions_count,
|
||||
handle_global_commands,
|
||||
@@ -9,7 +10,7 @@ use crate::utils::{with_timeout, with_timeout_msg};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
pub async fn handle<T: TdClientTrait>(app: &mut App<T>, key: KeyEvent) {
|
||||
// Глобальные команды (работают всегда)
|
||||
if handle_global_commands(app, key).await {
|
||||
return;
|
||||
@@ -445,7 +446,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
{
|
||||
Ok(messages) => {
|
||||
// Сохраняем загруженные сообщения
|
||||
*app.td_client.current_chat_messages_mut() = messages;
|
||||
app.td_client.set_current_chat_messages(messages);
|
||||
// ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории
|
||||
// Это предотвращает race condition с Update::NewMessage
|
||||
app.td_client.set_current_chat_id(Some(ChatId::new(chat_id)));
|
||||
@@ -589,10 +590,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
app.last_typing_sent = None;
|
||||
|
||||
// Отменяем typing status
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel)
|
||||
).await;
|
||||
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel).await;
|
||||
|
||||
match with_timeout_msg(
|
||||
Duration::from_secs(5),
|
||||
@@ -633,7 +631,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
{
|
||||
Ok(messages) => {
|
||||
// Сохраняем загруженные сообщения
|
||||
*app.td_client.current_chat_messages_mut() = messages;
|
||||
app.td_client.set_current_chat_messages(messages);
|
||||
// ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории
|
||||
// Это предотвращает race condition с Update::NewMessage
|
||||
app.td_client.set_current_chat_id(Some(ChatId::new(chat_id)));
|
||||
@@ -680,17 +678,10 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
if let Some(chat_id) = app.selected_chat_id {
|
||||
if !app.message_input.is_empty() && !app.is_editing() && !app.is_replying() {
|
||||
let draft_text = app.message_input.clone();
|
||||
// Timeout чтобы не блокировать UI в тестах
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
app.td_client.set_draft_message(chat_id, draft_text)
|
||||
).await;
|
||||
let _ = app.td_client.set_draft_message(chat_id, draft_text).await;
|
||||
} else if app.message_input.is_empty() {
|
||||
// Очищаем черновик если инпут пустой
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
app.td_client.set_draft_message(chat_id, String::new())
|
||||
).await;
|
||||
let _ = app.td_client.set_draft_message(chat_id, String::new()).await;
|
||||
}
|
||||
}
|
||||
app.close_chat();
|
||||
@@ -733,7 +724,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
KeyCode::Char('y') | KeyCode::Char('н') => {
|
||||
// Копировать сообщение
|
||||
if let Some(msg) = app.get_selected_message() {
|
||||
let text = format_message_for_clipboard(msg);
|
||||
let text = format_message_for_clipboard(&msg);
|
||||
match copy_to_clipboard(&text) {
|
||||
Ok(_) => {
|
||||
app.status_message = Some("Сообщение скопировано".to_string());
|
||||
@@ -864,11 +855,7 @@ pub async fn handle(app: &mut App, key: KeyEvent) {
|
||||
.unwrap_or(true);
|
||||
if should_send_typing {
|
||||
if let Some(chat_id) = app.get_selected_chat_id() {
|
||||
// Используем короткий timeout чтобы не блокировать UI (особенно в тестах)
|
||||
let _ = tokio::time::timeout(
|
||||
Duration::from_millis(100),
|
||||
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Typing)
|
||||
).await;
|
||||
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Typing).await;
|
||||
app.last_typing_sent = Some(Instant::now());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user