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:
Mikhail Kilin
2026-02-02 05:42:19 +03:00
parent ed5a4f9c72
commit 8e48d076de
38 changed files with 1053 additions and 161 deletions

View File

@@ -1,4 +1,5 @@
use crate::app::App;
use crate::tdlib::TdClientTrait;
use crate::message_grouping::{group_messages, MessageGroup};
use crate::ui::components;
use ratatui::{
@@ -113,7 +114,7 @@ fn wrap_text_with_offsets(text: &str, max_width: usize) -> Vec<WrappedLine> {
result
}
pub fn render(f: &mut Frame, area: Rect, app: &App) {
pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
// Режим профиля
if app.is_profile_mode() {
if let Some(profile) = app.get_profile_info() {
@@ -213,7 +214,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
f.render_widget(header, message_chunks[0]);
// Pinned bar (если есть закреплённое сообщение)
if let Some(pinned_msg) = &app.td_client.current_pinned_message() {
if let Some(pinned_msg) = app.td_client.current_pinned_message() {
let pinned_preview: String = pinned_msg.text().chars().take(40).collect();
let ellipsis = if pinned_msg.text().chars().count() > 40 {
"..."
@@ -251,7 +252,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
let mut selected_msg_line: Option<usize> = None;
// Используем message_grouping для группировки сообщений
let grouped = group_messages(app.td_client.current_chat_messages());
let grouped = group_messages(&app.td_client.current_chat_messages());
let mut is_first_date = true;
let mut is_first_sender = true;
@@ -357,9 +358,11 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
// Режим выбора сообщения - подсказка зависит от возможностей
let selected_msg = app.get_selected_message();
let can_edit = selected_msg
.as_ref()
.map(|m| m.can_be_edited() && m.is_outgoing())
.unwrap_or(false);
let can_delete = selected_msg
.as_ref()
.map(|m| m.can_be_deleted_only_for_self() || m.can_be_deleted_for_all_users())
.unwrap_or(false);
@@ -501,7 +504,7 @@ pub fn render(f: &mut Frame, area: Rect, app: &App) {
}
/// Рендерит режим поиска по сообщениям
fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
fn render_search_mode<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
// Извлекаем данные из ChatState
let (query, results, selected_index) =
if let crate::app::ChatState::SearchInChat {
@@ -696,7 +699,7 @@ fn render_search_mode(f: &mut Frame, area: Rect, app: &App) {
}
/// Рендерит режим просмотра закреплённых сообщений
fn render_pinned_mode(f: &mut Frame, area: Rect, app: &App) {
fn render_pinned_mode<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
// Извлекаем данные из ChatState
let (messages, selected_index) = if let crate::app::ChatState::PinnedMessages {
messages,