Files
telegram-tui/REFACTORING_ROADMAP.md
Mikhail Kilin 0768283e8a refactor: eliminate code duplication - extract helpers and use retry utils
Extracted duplicate code and unified timeout handling across the codebase.

Changes:
- Extracted open_chat_and_load_data() function (eliminates 52 lines of duplication)
- Replaced manual y/н/Enter handling with handle_yes_no() from modal_handler (2 places)
- Replaced 7 direct tokio::time::timeout calls with retry utils (auth, main_input, main)
- Added with_timeout_ignore() for non-critical operations
- Fixed modal_handler.rs bug: corrected Russian 'y' key (д → н)
- Removed unused imports in handlers/mod.rs and utils/mod.rs

Impact:
- main_input.rs: 1164 → 958 lines (-206 lines, -18%)
- Code duplication: 52 lines eliminated
- Direct timeout calls: 7 → 1 (-86%)
- DRY principle applied throughout

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-02 14:20:33 +03:00

1121 lines
43 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Refactoring Roadmap
Этот документ содержит список технического долга и планов по рефакторингу кодовой базы.
## Приоритет 1: Критичные улучшения
### 1. Схлопнуть состояния чата в enum
**Проблема**: Сейчас состояния чата хранятся как отдельные boolean поля в `App`:
```rust
is_message_selection_mode: bool,
is_editing_mode: bool,
is_reply_mode: bool,
is_forward_mode: bool,
is_delete_confirmation: bool,
is_reaction_picker_mode: bool,
is_profile_mode: bool,
is_search_in_chat_mode: bool,
```
**Решение**: Создать enum `ChatState`:
```rust
enum ChatState {
Normal,
MessageSelection {
selected_message_id: i64,
},
Editing {
message_id: i64,
original_text: String,
},
Reply {
message_id: i64,
preview_text: String,
},
Forward {
message_id: i64,
selected_chat_index: usize,
},
DeleteConfirmation {
message_id: i64,
},
ReactionPicker {
message_id: i64,
available_reactions: Vec<String>,
selected_index: usize,
},
Profile {
info: ProfileInfo,
},
SearchInChat {
query: String,
results: Vec<i64>,
current_index: usize,
},
}
```
**Преимущества**:
- Невозможно иметь несколько состояний одновременно (type-safe)
- Проще обрабатывать переходы между состояниями
- Меньше полей в `App`
- Данные, связанные с состоянием, хранятся вместе с ним
**Затронутые файлы**:
- `src/app/mod.rs` (добавить enum, убрать boolean поля)
- `src/input/main_input.rs` (изменить логику обработки на match)
- `src/ui/messages.rs` (изменить рендеринг на match)
---
### 2. Разделить TdClient на несколько модулей
**Проблема**: `TdClient` в `src/tdlib/client.rs` (~1500+ строк) делает слишком много:
- Авторизация
- Управление чатами
- Управление сообщениями
- Кеширование пользователей
- Реакции
- Network state
**Решение**: Разделить на модули:
```
src/tdlib/
├── mod.rs # Экспорт публичных типов
├── client.rs # Основной TdClient
├── auth.rs # AuthManager
├── chats.rs # ChatManager
├── messages.rs # MessageManager
├── users.rs # UserCache
└── reactions.rs # ReactionManager
```
**Преимущества**:
- Принцип единственной ответственности
- Проще тестировать отдельные модули
- Легче найти и изменить код
---
### 3. Вынести константы в отдельный модуль
**Проблема**: Магические числа разбросаны по всему коду:
```rust
// В разных местах:
500 // MAX_MESSAGES_IN_CHAT
500 // MAX_USER_CACHE_SIZE
200 // MAX_CHATS
8 // Emoji picker columns
10 // Max input height
16 // Poll timeout (60 FPS)
```
**Решение**: Создать `src/constants.rs`:
```rust
// Memory limits
pub const MAX_MESSAGES_IN_CHAT: usize = 500;
pub const MAX_USER_CACHE_SIZE: usize = 500;
pub const MAX_CHATS: usize = 200;
pub const MAX_CHAT_USER_IDS: usize = 500;
// UI constants
pub const EMOJI_PICKER_COLUMNS: usize = 8;
pub const EMOJI_PICKER_ROWS: usize = 6;
pub const MAX_INPUT_HEIGHT: usize = 10;
pub const MIN_TERMINAL_WIDTH: u16 = 80;
pub const MIN_TERMINAL_HEIGHT: u16 = 20;
// Performance
pub const POLL_TIMEOUT_MS: u64 = 16; // 60 FPS
pub const SHUTDOWN_TIMEOUT_SECS: u64 = 2;
pub const LAZY_LOAD_USERS_PER_TICK: usize = 5;
// TDLib
pub const TDLIB_CHAT_LIMIT: i32 = 50;
pub const TDLIB_MESSAGE_LIMIT: i32 = 50;
```
**Преимущества**:
- Единое место для всех констант
- Проще изменить значения
- Самодокументирующийся код
---
## Приоритет 2: Улучшение типобезопасности
### 4. Newtype pattern для ID ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО (2026-01-31)
**Проблема**: Везде используется `i64` для `chat_id`, `message_id`, `user_id` — легко перепутать.
**Решение**: ✅ Реализовано в `src/types.rs`:
```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ChatId(pub i64);
impl ChatId {
pub fn new(id: i64) -> Self { Self(id) }
pub fn as_i64(&self) -> i64 { self.0 }
}
impl From<i64> for ChatId {
fn from(id: i64) -> Self { ChatId(id) }
}
impl Display for ChatId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
// Аналогично для MessageId и UserId
```
**Что сделано**:
- ✅ Создан `src/types.rs` с тремя типами: `ChatId`, `MessageId`, `UserId`
- ✅ Добавлены методы `new()`, `as_i64()`, `From<i64>`, `Display`
- ✅ Реализованы traits: `Hash`, `Eq`, `Serialize`, `Deserialize`
- ✅ Обновлены 15+ модулей:
- `tdlib/types.rs`, `tdlib/chats.rs`, `tdlib/messages.rs`, `tdlib/users.rs`
- `tdlib/reactions.rs`, `tdlib/client.rs`
- `app/mod.rs`, `app/chat_state.rs`, `input/main_input.rs`
- Test helpers: `app_builder.rs`, `test_data.rs`
- ✅ Исправлены 53 ошибки компиляции
- ✅ Код компилируется успешно
**Преимущества**:
- ✅ Невозможно случайно передать message_id вместо chat_id
- ✅ Компилятор ловит ошибки на этапе компиляции
- ✅ Улучшенная читаемость кода
- ✅ Самодокументирующиеся типы
---
### 5. Создать enum для ошибок
**Проблема**: Везде используется `Result<T, String>` — теряется контекст ошибок.
**Решение**: Создать `src/error.rs`:
```rust
#[derive(Debug, thiserror::Error)]
pub enum TeletuiError {
#[error("TDLib error: {0}")]
TdLib(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Network error: {0}")]
Network(String),
#[error("Authentication error: {0}")]
Auth(String),
#[error("Invalid timezone format: {0}")]
InvalidTimezone(String),
#[error("Invalid color: {0}")]
InvalidColor(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, TeletuiError>;
```
**Зависимости**: `thiserror = "1.0"`
**Преимущества**:
- Типобезопасная обработка ошибок
- Понятные сообщения об ошибках
- Возможность pattern matching
---
### 6. Группировка полей MessageInfo ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО (2026-01-31)
**Проблема**: `MessageInfo` имеет слишком много плоских полей (~15+).
**Решение**: ✅ Реализовано - группировка в логические структуры:
```rust
pub struct MessageInfo {
pub metadata: MessageMetadata,
pub content: MessageContent,
pub state: MessageState,
pub interactions: MessageInteractions,
}
pub struct MessageMetadata {
pub id: MessageId,
pub chat_id: ChatId,
pub sender_id: UserId,
pub date: i32,
}
pub struct MessageContent {
pub text: String,
pub formatted_text: Option<FormattedText>,
pub media_type: Option<String>,
}
pub struct MessageState {
pub is_outgoing: bool,
pub is_edited: bool,
pub is_pinned: bool,
}
pub struct MessageInteractions {
pub reply_to_message_id: Option<MessageId>,
pub forward_info: Option<ForwardInfo>,
pub reactions: Vec<ReactionInfo>,
pub read_count: i32,
}
```
**Что сделано**:
- ✅ Созданы 4 структуры: MessageMetadata, MessageContent, MessageState, MessageInteractions
- ✅ Обновлена MessageInfo для использования новых структур
- ✅ Добавлен конструктор MessageInfo::new()
- ✅ Добавлены getter методы (id(), text(), sender_name(), и др.)
- ✅ Обновлены 14 файлов (~200+ обращений):
- ui/messages.rs: рендеринг (100+ изменений)
- app/mod.rs: логика приложения
- input/main_input.rs: обработка ввода
- tdlib/client.rs: обработка updates
- Все тестовые файлы
- ✅ Код компилируется успешно
**Преимущества**:
- ✅ Логическая группировка данных
- ✅ Проще добавлять новые поля
- ✅ Улучшенная читаемость кода
- ✅ Меньше параметров в конструкторах (используется new())
---
### MessageBuilder pattern ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО (2026-01-31)
**Проблема**: MessageInfo::new() принимает 14 параметров, что неудобно и подвержено ошибкам.
**Решение**: ✅ Реализован MessageBuilder с fluent API:
```rust
let message = MessageBuilder::new(MessageId::new(123))
.sender_name("Alice")
.text("Hello, world!")
.outgoing()
.read()
.build();
```
**Что сделано**:
- ✅ Создана структура MessageBuilder в tdlib/types.rs
- ✅ Реализовано 16 методов fluent API:
- Базовые: sender_name, text, entities, date, edit_date
- Флаги: outgoing, incoming, read, unread, edited
- Права: editable, deletable_for_self, deletable_for_all
- Дополнительно: reply_to, forward_from, reactions, add_reaction
- ✅ Обновлён convert_message() для использования builder
- ✅ Добавлены 6 unit тестов
- ✅ Код компилируется успешно
**Преимущества**:
- ✅ Более читабельный код
- ✅ Самодокументирующийся API
- ✅ Гибкость в установке опциональных полей
- ✅ Проще поддерживать и расширять
**🎉 Priority 2 ЗАВЕРШЁН НА 100%! 🎉**
---
## Приоритет 3: Архитектурные улучшения
### 7. Выделить UI компоненты ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО (5/5 компонентов, 2026-02-02)
**Проблема**: Код рендеринга дублируется, сложно переиспользовать.
**Решение**: ✅ Создано `src/ui/components/`:
```
src/ui/components/
├── mod.rs ✅
├── modal.rs ✅ (87 строк, полностью реализовано)
├── input_field.rs ✅ (54 строк, полностью реализовано)
├── message_bubble.rs ⚠️ (27 строк, placeholder, блокируется P3.8 и P3.9)
├── chat_list_item.rs ✅ (78 строк, полностью реализовано)
└── emoji_picker.rs ✅ (112 строк, полностью реализовано)
```
**Что сделано**:
- ✅ Создана структура модулей `src/ui/components/`
- ✅ Реализовано 5 из 5 компонентов:
- `modal.rs` — базовые модалки с центрированием (87 строк)
- `input_field.rs` — текстовое поле с курсором (54 строки)
- `chat_list_item.rs` — элемент списка чатов (78 строк)
- `emoji_picker.rs` — picker реакций (112 строк)
- `message_bubble.rs` — рендеринг сообщений (437 строк) ✅ **ЗАВЕРШЕНО 2026-02-02**
-Все компоненты используются в UI
-`messages.rs` использует `message_grouping` и компоненты
**Преимущества**:
- ✅ Переиспользуемые компоненты
- ✅ Консистентный UI
- ✅ Проще тестировать
---
### 8. Вынести форматирование в отдельный модуль
**Проблема**: Markdown форматирование захардкожено в `messages.rs` (~200+ строк).
**Решение**: Создать `src/formatting.rs`:
```rust
pub struct FormattedSpan {
pub text: String,
pub style: Style,
}
pub fn format_text_entities(
text: &str,
entities: &[TextEntity],
) -> Vec<FormattedSpan> {
// Вся логика форматирования
}
```
**Преимущества**:
- Разделение ответственности
- Можно тестировать отдельно
- Переиспользование в других местах
---
### 9. Вынести логику группировки сообщений ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО (2026-01-31)
**Проблема**: Логика группировки сообщений смешана с рендерингом в `messages.rs`.
**Решение**: ✅ Создан `src/message_grouping.rs`:
```rust
pub enum MessageGroup {
DateSeparator(i32),
SenderHeader { is_outgoing: bool, sender_name: String },
Message(MessageInfo),
}
pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup> {
// Логика группировки по дате и отправителю
}
```
**Что сделано**:
- ✅ Создан модуль `src/message_grouping.rs` (255 строк)
- ✅ Реализован enum `MessageGroup` с тремя вариантами
- ✅ Реализована функция `group_messages()` для группировки по дате и отправителю
- ✅ Добавлена полная документация с примерами
- ✅ Написано 5 unit тестов (все проходят)
- ✅ Модуль добавлен в `src/lib.rs`
- ✅ Код компилируется успешно
**Преимущества**:
- ✅ Чистое разделение логики и представления
- ✅ Легче тестировать группировку (покрыто тестами)
- ✅ Можно переиспользовать
- ✅ Готово для интеграции в `messages.rs`
---
### 10. Hotkey mapping в конфиг ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО (2026-01-31)
**Проблема**: Хоткеи захардкожены в коде, нельзя настроить.
**Решение**: ✅ Добавлено в `config.toml`:
```toml
[hotkeys]
# Навигация (vim + русские + стрелки)
up = ["k", "р", "Up"]
down = ["j", "о", "Down"]
left = ["h", "р", "Left"]
right = ["l", "д", "Right"]
# Действия (англ + русские)
reply = ["r", "к"]
forward = ["f", "а"]
delete = ["d", "в", "Delete"]
copy = ["y", "н"]
react = ["e", "у"]
profile = ["i", "ш"]
```
**Что сделано**:
- ✅ Создана структура `HotkeysConfig` в `src/config.rs`
- ✅ Добавлены поля для всех действий (10 hotkeys)
- ✅ Реализован метод `matches(key: KeyCode, action: &str) -> bool`
- ✅ Поддержка символьных клавиш (англ + русские)
- ✅ Поддержка специальных клавиш (Up, Down, Left, Right, Delete, Enter, Esc)
- ✅ Добавлены дефолтные значения для всех hotkeys
- ✅ Написано 9 unit тестов (all passing ✅)
- ✅ Добавлена полная rustdoc документация
- ✅ Config::default() включает hotkeys
**Примеры использования**:
```rust
let config = Config::default();
// Проверяем английскую клавишу
if config.hotkeys.matches(KeyCode::Char('r'), "reply") {
// Начать ответ
}
// Проверяем русскую клавишу
if config.hotkeys.matches(KeyCode::Char('к'), "reply") {
// Начать ответ (та же логика)
}
// Проверяем стрелку
if config.hotkeys.matches(KeyCode::Up, "up") {
// Вверх по списку
}
```
**Преимущества**:
- ✅ Пользовательская настройка хоткеев через config.toml
- ✅ Проще добавлять новые действия
- ✅ Документация хоткеев в конфиге
- ✅ Централизованное управление клавишами
- ✅ Поддержка русской раскладки out of the box
**🎉 Priority 3 ЗАВЕРШЁН НА 100%! 🎉**
---
## Приоритет 4: Качество кода
### 11. Добавить юнит-тесты ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО 100% (+106 строк тестов, 2026-02-01)
**Что сделано**:
- ✅ Добавлены 9 unit тестов в `src/utils.rs` (в секции `#[cfg(test)]`)
- ✅ Покрыты все edge cases для форматирования времени
- ✅ Тестирование приватных функций через публичный API
-Все 54 unit теста проходят (было 45, +9 новых)
**Добавленные тесты**:
- `format_timestamp_with_tz` - положительный offset (+03:00)
- `format_timestamp_with_tz` - отрицательный offset (-05:00)
- `format_timestamp_with_tz` - нулевой offset (UTC)
- `format_timestamp_with_tz` - переход через полночь
- `format_timestamp_with_tz` - невалидный timezone (fallback)
- `get_day` - расчет дня из timestamp
- `get_day_grouping` - группировка сообщений по дням
- `format_datetime` - полная дата и время с MSK
- `parse_timezone_offset` - через публичный API (приватная функция)
**Примеры**:
```rust
#[test]
fn test_format_timestamp_with_tz_positive_offset() {
let timestamp = 1640000000; // 2021-12-20 11:33:20 UTC
assert_eq!(format_timestamp_with_tz(timestamp, "+03:00"), "14:33");
}
#[test]
fn test_get_day_grouping() {
let msg1 = 1640000000; // 2021-12-20 09:33:20
let msg2 = 1640040000; // 2021-12-20 20:40:00
assert_eq!(get_day(msg1), get_day(msg2)); // Один день
}
```
**Запуск**: `cargo test --lib utils::tests`
---
### 12. Добавить rustdoc комментарии ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО 100% (+900 строк документации, 2026-02-01)
**Что сделано**:
- ✅ Документированы все TDLib модули (auth, chats, messages, reactions, users)
- ✅ Документированы все публичные структуры и методы
- ✅ Добавлены примеры использования (34 doctests)
- ✅ Документация для Config и утилит (formatting)
-Все doctests работают (30 ignored для async, 4 compiled)
**Модули с документацией**:
- `src/tdlib/auth.rs` - AuthManager, AuthState (6 doctests)
- `src/tdlib/chats.rs` - ChatManager (8 doctests)
- `src/tdlib/messages.rs` - MessageManager, 14 методов (6 doctests)
- `src/tdlib/reactions.rs` - ReactionManager (3 doctests)
- `src/tdlib/users.rs` - UserCache, LruCache (2 doctests)
- `src/config.rs` - Config, ColorsConfig, GeneralConfig (4 doctests)
- `src/formatting.rs` - Форматирование текста (2 doctests)
- `src/tdlib/client.rs` - TdClient (1 doctest)
- `src/app/mod.rs` - App (1 doctest)
- `src/message_grouping.rs` - Группировка (1 doctest)
- `src/tdlib/types.rs` - MessageBuilder (1 doctest)
**Примеры**:
```rust
/// Менеджер авторизации TDLib.
///
/// # Examples
///
/// ```ignore
/// let mut auth_manager = AuthManager::new(client_id);
/// auth_manager.send_phone_number("+1234567890".to_string()).await?;
/// auth_manager.send_code("12345".to_string()).await?;
/// ```
pub struct AuthManager { ... }
```
**Генерация**: `cargo doc --open`
---
### 13. Config валидация ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО 100% (+149 строк тестов, 2026-02-01)
**Что сделано**:
- ✅ Валидация уже была реализована в `config.rs:344-389`
- ✅ Вызов валидации в `Config::load():450-456`
- ✅ Добавлено 15 comprehensive тестов для полного покрытия
-Все 23 config теста проходят (8 существующих + 15 новых)
**Добавленные тесты**:
- Валидация дефолтного конфига
- Timezone: валидный (+03:00, -05:00), невалидный (без знака)
- Цвета: все 18 стандартных ratatui цветов
- Невалидные цвета (rainbow, purple, pink)
- Case-insensitive парсинг (RED, Green, YELLOW)
- parse_color() для всех вариантов (standard, light, gray/grey)
- Fallback к White для невалидных цветов
**Реализация**: Уже была добавлена ранее:
```rust
impl Config {
pub fn validate(&self) -> Result<(), TeletuiError> {
// Проверка timezone
if !self.general.timezone.starts_with('+')
&& !self.general.timezone.starts_with('-') {
return Err(TeletuiError::InvalidTimezone(
format!("Timezone must start with + or -: {}", self.general.timezone)
));
}
// Проверка цветов
let valid_colors = [
"black", "red", "green", "yellow", "blue", "magenta",
"cyan", "gray", "white", "darkgray", "lightred",
"lightgreen", "lightyellow", "lightblue",
"lightmagenta", "lightcyan"
];
for color_name in [
&self.colors.incoming_message,
&self.colors.outgoing_message,
&self.colors.selected_message,
&self.colors.reaction_chosen,
&self.colors.reaction_other,
] {
if !valid_colors.contains(&color_name.to_lowercase().as_str()) {
return Err(TeletuiError::InvalidColor(
format!("Unknown color: {}", color_name)
));
}
}
Ok(())
}
}
```
Вызывать при загрузке:
```rust
pub fn load() -> Self {
let config = // ... загрузка из файла
if let Err(e) = config.validate() {
eprintln!("Config validation error: {}", e);
return Self::default();
}
config
}
```
---
### 14. Async/await консистентность ✅ ЗАВЕРШЕНО!
**Статус**: ЗАВЕРШЕНО 100% (проверка кода, 2026-02-01)
**Проверка показала**: Код уже соответствует требованиям!
**Что проверено**:
-`std::fs` используется только в `Config::load()` при старте (не в async runtime)
-`std::thread::sleep` - не найдено ни разу
-`tokio::time::sleep` используется в async функциях (messages.rs)
-`tokio::time::timeout` используется (auth.rs, main_input.rs, main.rs)
-Все файловые операции вызываются синхронно при инициализации
**Детали**:
```rust
// ✓ ПРАВИЛЬНО: Config::load() при старте, перед async runtime
#[tokio::main]
async fn main() -> Result<(), io::Error> {
let config = config::Config::load(); // Синхронно, при инициализации
// ... async runtime начинается позже
}
// ✓ ПРАВИЛЬНО: tokio::time::sleep в async функциях
async fn load_messages() {
use tokio::time::{sleep, Duration};
sleep(Duration::from_millis(100)).await; // Не блокирует
}
```
**Вывод**: Блокирующих вызовов в async контексте нет. Код async-clean.
---
## Приоритет 5: Опциональные улучшения
### 15. Feature flags для зависимостей ✅ ЗАВЕРШЕНО
**Проблема**: Все зависимости всегда включены.
**Решение**: В `Cargo.toml`:
```toml
[features]
default = ["clipboard", "url-open"]
clipboard = ["dep:arboard"]
url-open = ["dep:open"]
[dependencies]
arboard = { version = "3.4", optional = true }
open = { version = "5.0", optional = true }
```
**Реализовано**:
- ✅ Добавлены feature flags в Cargo.toml
- ✅ Зависимости `arboard` и `open` сделаны опциональными
- ✅ Условная компиляция в `src/input/main_input.rs`:
- `#[cfg(feature = "url-open")]` для `open::that()`
- `#[cfg(feature = "clipboard")]` / `#[cfg(not(feature = "clipboard"))]` для `copy_to_clipboard()`
- ✅ Условная компиляция в `tests/copy.rs`:
- `#[cfg(all(test, feature = "clipboard"))]` для clipboard тестов
**Преимущества**:
- ✅ Уменьшение размера бинарника
- ✅ Опциональная функциональность
- ✅ Graceful degradation при отключении фич
---
### 16. LRU cache обобщение ✅ ЗАВЕРШЕНО
**Проблема**: Отдельные LRU кеши для `user_names` и `user_statuses`.
**Решение**: Создать обобщённый `LruCache<K, V>` или использовать готовый крейт `lru = "0.12"`.
**Реализовано**:
- ✅ Обобщённая структура `LruCache<K, V>` в `src/tdlib/users.rs`
- ✅ Type parameters:
- `K: Eq + Hash + Clone + Copy` — тип ключа
- `V: Clone` — тип значения
- ✅ Обновлена `UserCache`:
- `user_usernames: LruCache<UserId, String>`
- `user_names: LruCache<UserId, String>`
- `user_statuses: LruCache<UserId, UserOnlineStatus>`
-Все методы обобщены: `get()`, `peek()`, `insert()`, `contains_key()`, `len()`
**Преимущества**:
- ✅ Переиспользуемая реализация для любых типов ключей
- ✅ Type-safe кеширование
- ✅ Без дополнительных зависимостей
---
### 17. Tracing вместо println! ✅ ЗАВЕРШЕНО
**Проблема**: Используется `eprintln!` для логов.
**Решение**: Использовать `tracing`:
```rust
use tracing::{info, warn, error, debug};
// Вместо
eprintln!("Warning: Could not load config: {}", e);
// Использовать
warn!("Could not load config: {}", e);
```
**Реализовано**:
- ✅ Добавлены зависимости в `Cargo.toml`:
- `tracing = "0.1"`
- `tracing-subscriber = { version = "0.3", features = ["env-filter"] }`
- ✅ Инициализирован subscriber в `main.rs`:
- Уровень логов по умолчанию: `warn`
- Настраивается через переменную окружения `RUST_LOG`
- ✅ Заменены все `eprintln!` на tracing макросы в `src/config.rs`:
- 4× `warn!()` для предупреждений
- 1× `error!()` для ошибок валидации
- 1× `warn!()` для fallback на дефолтную конфигурацию
**Преимущества**:
- ✅ Структурированное логирование
- ✅ Настраиваемые уровни логов (через `RUST_LOG`)
- ✅ Лучшая интеграция с async кодом
- ✅ Единый подход к логированию во всём проекте
---
## Метрики прогресса
- [x] Priority 1: 3/3 задач ✅ ЗАВЕРШЕНО!
- [x] P1.1 — ChatState enum
- [x] P1.2 — Разделить TdClient
- [x] P1.3 — Константы
- [x] Priority 2: 5/5 задач ✅ ЗАВЕРШЕНО! 🎉
- [x] P2.5 — Error enum
- [x] P2.3 — Config validation
- [x] P2.4 — Newtype для ID
- [x] P2.6 — MessageInfo реструктуризация
- [x] P2.7 — MessageBuilder pattern
- [x] Priority 3: 4/4 задач ✅ ЗАВЕРШЕНО! 🎉🎉
- [x] P3.7 — UI компоненты (5/5) ✅ ПОЛНОСТЬЮ ЗАВЕРШЕНО 2026-02-02!
- [x] P3.8 — Formatting модуль ✅
- [x] P3.9 — Message Grouping ✅
- [x] P3.10 — Hotkey Mapping ✅
- [x] Priority 4: 4/4 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉
- [x] P4.11 — Unit tests ✅
- [x] P4.12 — Rustdoc ✅
- [x] P4.13 — Config validation ✅
- [x] P4.14 — Async/await consistency ✅
- [x] Priority 5: 3/3 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉
- [x] P5.15 — Feature flags ✅
- [x] P5.16 — LRU cache обобщение ✅
- [x] P5.17 — Tracing ✅
- [x] Priority 6: 1/1 задач ✅ ЗАВЕРШЕНО! 🎉🎉🎉🎉
- [x] P6.1 — Dependency Injection для TdClient (ВСЕ 8 этапов завершены!)
**Всего**: 21/21 задач (100%) 🎊🎉 РЕФАКТОРИНГ ПОЛНОСТЬЮ ЗАВЕРШЁН!
---
## Предусловие: Тесты
**ВАЖНО**: Перед началом рефакторинга необходимо написать тесты!
См. [TESTING_ROADMAP.md](TESTING_ROADMAP.md) для плана покрытия тестами.
Минимальное покрытие для начала рефакторинга:
- ✅ Фаза 0: Инфраструктура (helpers, fake client)
- ✅ Snapshot тесты для основных экранов (chat list, messages)
- ✅ Integration тесты для критичных flow (send, edit, navigation)
**Зачем**: Тесты гарантируют, что рефакторинг не сломает функциональность.
---
## Порядок выполнения
Рекомендуется выполнять в следующем порядке:
1. **P1.3** — Константы (быстро, малый риск)
2. **P1.1** — ChatState enum (высокий impact)
3. **P2.5** — Error enum (улучшает весь код)
4. **P4.11** — Тесты для utils (базовая проверка)
5. **P1.2** — Разделить TdClient (большой рефакторинг)
6. **P2.4** — Newtype для ID (широкие изменения)
7. **P3.7** — UI компоненты (постепенно)
8. **P3.8** — Форматирование (изоляция логики)
9. **P3.9** — Группировка сообщений (изоляция логики)
10. Остальные по необходимости
---
## Принципы рефакторинга
1. **Один PR = одна задача** — не смешивать рефакторинг разных областей
2. **Тесты прежде всего** — добавить тесты перед рефакторингом
3. **Обратная совместимость** — сохранять работоспособность на каждом шаге
4. **Маленькие шаги** — лучше 10 маленьких PR, чем 1 огромный
5. **Документация** — обновлять документацию после изменений
---
## Приоритет 6: Улучшение тестируемости
### P6.1 — Dependency Injection для TdClient ✅ ЗАВЕРШЕНО!
**Статус**: ✅ ЗАВЕРШЕНО (ВСЕ 8 этапов завершены!) - 2026-02-02
**Проблема**:
В текущей реализации тесты создают **настоящий** `TdClient`, который вызывает `tdlib_rs::create_client()`. Это приводит к:
1. **Зависанию тестов** — TDLib не инициализирован и блокирует async вызовы
2. **Verbose логи** — TDLib выводит много логов при создании клиента
3. **Медленные тесты** — создание TDLib клиента занимает время
4. **Хаки в продакшн коде** — пришлось добавить `tokio::time::timeout(100ms)` для всех вызовов TDLib чтобы тесты не зависали
**Проблемные места** (src/input/main_input.rs):
```rust
// Строка 867-870: timeout для send_chat_action при вводе символов
let _ = tokio::time::timeout(
Duration::from_millis(100),
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Typing)
).await;
// Строка 683-686: timeout для set_draft_message при закрытии чата
let _ = tokio::time::timeout(
Duration::from_millis(100),
app.td_client.set_draft_message(chat_id, draft_text)
).await;
// Строка 592-594: timeout для send_chat_action Cancel при отправке сообщения
let _ = tokio::time::timeout(
Duration::from_millis(100),
app.td_client.send_chat_action(ChatId::new(chat_id), ChatAction::Cancel)
).await;
```
**Решения**:
#### Вариант 1: Trait-based Dependency Injection (рекомендуется)
Создать trait `TdClientTrait` и сделать `App` generic:
```rust
// src/tdlib/trait.rs
#[async_trait]
pub trait TdClientTrait {
async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction);
async fn set_draft_message(&self, chat_id: ChatId, text: String) -> Result<()>;
async fn get_chat_history(&mut self, chat_id: ChatId, limit: i32) -> Result<Vec<MessageInfo>>;
async fn send_message(&mut self, chat_id: ChatId, text: String, reply_to: Option<MessageId>, reply_info: Option<ReplyInfo>) -> Result<MessageInfo>;
async fn edit_message(&mut self, chat_id: ChatId, message_id: MessageId, text: String) -> Result<MessageInfo>;
async fn delete_messages(&mut self, chat_id: ChatId, message_ids: Vec<MessageId>, revoke: bool) -> Result<()>;
async fn forward_messages(&mut self, to_chat_id: ChatId, from_chat_id: ChatId, message_ids: Vec<MessageId>) -> Result<()>;
async fn toggle_reaction(&self, chat_id: ChatId, message_id: MessageId, emoji: String) -> Result<()>;
async fn get_message_available_reactions(&self, chat_id: ChatId, message_id: MessageId) -> Result<Vec<String>>;
async fn search_messages(&self, chat_id: ChatId, query: &str) -> Result<Vec<MessageInfo>>;
async fn get_profile_info(&self, chat_id: ChatId) -> Result<ProfileInfo>;
async fn leave_chat(&self, chat_id: ChatId) -> Result<()>;
async fn load_chats(&mut self, limit: usize) -> Result<Vec<ChatInfo>, String>;
async fn load_folder_chats(&mut self, folder_id: i32, limit: usize) -> Result<(), String>;
async fn get_pinned_messages(&self, chat_id: ChatId) -> Result<Vec<MessageInfo>, String>;
async fn load_current_pinned_message(&mut self, chat_id: ChatId);
async fn fetch_missing_reply_info(&mut self);
// ... все остальные методы
// Синхронные методы
fn current_chat_messages(&self) -> &[MessageInfo];
fn current_chat_messages_mut(&mut self) -> &mut Vec<MessageInfo>;
fn set_current_chat_id(&mut self, chat_id: Option<ChatId>);
fn folders(&self) -> &[FolderInfo];
fn network_state(&self) -> NetworkState;
fn typing_status(&self) -> Option<(i64, String)>;
fn current_pinned_message(&self) -> Option<&MessageInfo>;
fn push_message(&mut self, message: MessageInfo);
fn set_typing_status(&mut self, status: Option<(i64, String)>);
fn set_current_pinned_message(&mut self, message: Option<MessageInfo>);
}
// Real implementation
#[async_trait]
impl TdClientTrait for TdClient {
// Реализация всех методов, делегируя к существующим
async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) {
self.send_chat_action(chat_id, action).await
}
// ... остальные методы
}
// Fake implementation для тестов
#[async_trait]
impl TdClientTrait for FakeTdClient {
// Реализация для тестов (уже есть в tests/helpers/fake_tdclient.rs)
async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) {
self.chat_actions.lock().unwrap().push((chat_id.as_i64(), action.to_string()));
}
// ... остальные методы
}
// App становится generic
pub struct App<T: TdClientTrait = TdClient> {
pub td_client: T,
pub config: Config,
// ... остальные поля
}
impl<T: TdClientTrait> App<T> {
pub fn new(config: Config, td_client: T) -> Self {
// ...
}
// ... все остальные методы
}
// Специализация для продакшена
impl App<TdClient> {
pub fn new_default(config: Config) -> Self {
Self::new(config, TdClient::new())
}
}
// TestAppBuilder для тестов
impl TestAppBuilder {
pub fn build(self) -> App<FakeTdClient> {
let td_client = FakeTdClient::new()
.with_chats(self.chats)
.with_messages(self.selected_chat_id.unwrap_or(0), self.messages);
App::new(self.config, td_client)
}
}
```
**Плюсы**:
- ✅ Чистая архитектура, настоящий dependency injection
- ✅ Тесты не создают реальный TDLib — **быстрые и тихие**
- ✅ Убираем timeout'ы из продакшн кода — **чистота**
- ✅ Легко мокировать для unit-тестов
- ✅ Соответствует принципам SOLID (Dependency Inversion)
**Минусы**:
- ❌ Большой рефакторинг (~50+ файлов)
- ❌ Усложнение кода (generics везде: `App<T>`, `handle_input<T>`)
- ❌ Потеря простоты для небольшого проекта
- ❌ Нужна библиотека `async-trait` для async методов в trait
**Затронутые файлы**:
- `src/tdlib/trait.rs` (новый) — trait определение
- `src/tdlib/client.rs` — impl TdClientTrait for TdClient
- `src/tdlib/mod.rs` — экспорт trait
- `src/app/mod.rs` — App<T: TdClientTrait>
- `src/input/main_input.rs` — функции становятся generic
- `src/input/auth.rs` — функции становятся generic
- `src/ui/*.rs` — функции рендеринга становятся generic
- `src/main.rs` — использовать App<TdClient>
- `tests/helpers/fake_tdclient.rs` — impl TdClientTrait for FakeTdClient
- `tests/helpers/app_builder.rs` — build() возвращает App<FakeTdClient>
- Все интеграционные тесты (~15 файлов)
**Оценка трудозатрат**: ~2-3 дня работы
---
#### Вариант 2: Enum Dispatch (компромисс)
```rust
// src/tdlib/wrapper.rs
pub enum TdClientWrapper {
Real(TdClient),
Fake(FakeTdClient),
}
impl TdClientWrapper {
async fn send_chat_action(&self, chat_id: ChatId, action: ChatAction) {
match self {
Self::Real(c) => c.send_chat_action(chat_id, action).await,
Self::Fake(c) => c.send_chat_action(chat_id, action).await,
}
}
// ... все остальные методы с match на обе ветки
}
// App использует wrapper
pub struct App {
pub td_client: TdClientWrapper,
// ...
}
```
**Плюсы**:
- ✅ Меньше изменений чем trait (нет generics)
- ✅ Тесты используют Fake
- ✅ Проще понять чем trait + generics
**Минусы**:
- ❌ Всё равно много boilerplate (каждый метод требует match)
- ❌ Runtime dispatch overhead (небольшой)
-Не такой чистый как trait
-В продакшене всегда Real, но проверка match всё равно есть
**Затронутые файлы**: ~20-30 файлов (меньше чем Вариант 1)
**Оценка трудозатрат**: ~1 день работы
---
#### Вариант 3: Оставить как есть (текущее состояние)
**Обоснование**:
- Timeout'ы — это не "хак", а **защита от зависания UI**
- Даже в продакшене UI не должен зависать если TDLib глючит
- 100ms timeout на typing action и draft — нормально, это не критичные операции
- Защищает от deadlock'ов и network issues
- Простота важнее для небольшого проекта
**Плюсы**:
- ✅ Нет дополнительной работы
- ✅ Код остаётся простым
- ✅ Timeout'ы улучшают надёжность даже в продакшене
- ✅ Тесты работают (хоть и создают TDLib)
**Минусы**:
- ⚠️ Verbose логи TDLib в тестах (можно игнорировать)
- ⚠️ Тесты чуть медленнее (~0.1s на тест из-за инициализации TDLib)
- ⚠️ Timeout'ы в продакшн коде (но это не обязательно плохо)
---
**Рекомендация**:
- **Для прототипа/MVP**: Вариант 3 (текущее состояние) ✅
- **Для production-ready проекта**: Вариант 1 (trait injection) ⭐
- **Для быстрого улучшения**: Вариант 2 (enum dispatch)
**Финальное решение** (2026-02-02): Реализован **Вариант 1 (trait injection)** ✅🎉
После завершения всех 8 этапов рефакторинга:
- ✅ Создан `TdClientTrait` с 40+ методами
- ✅ Реализован trait для `TdClient` и `FakeTdClient`
-`App` стал generic: `App<T: TdClientTrait>`
-Все UI и input handlers обновлены на generic
- ✅ Тесты используют `FakeTdClient` (быстро, без логов TDLib)
- ✅ Продакшн использует `TdClient` (реальный TDLib)
- ✅ Timeout'ы убраны из продакшн кода
- ✅ Исправлен stack overflow в 6 методах trait реализации
-Все 196+ тестов проходят
**Преимущества реализации**:
- 🛡️ Чистая архитектура без timeout хаков
- ⚡ Быстрые тесты (FakeTdClient работает мгновенно)
- 📝 Нет verbose логов TDLib в тестах
- 🔧 Type-safe dependency injection
- 🎯 Легко добавлять новые реализации trait
---
## Примечания
- Этот документ живой и будет обновляться
- Новые пункты добавляются по мере обнаружения
- После завершения задачи отмечать в метриках
- При появлении блокеров — документировать в соответствующей секции