fixes
This commit is contained in:
664
REFACTORING_ROADMAP.md
Normal file
664
REFACTORING_ROADMAP.md
Normal file
@@ -0,0 +1,664 @@
|
||||
# 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
|
||||
|
||||
**Проблема**: Везде используется `i64` для `chat_id`, `message_id`, `user_id` — легко перепутать.
|
||||
|
||||
**Решение**: Создать `src/types.rs`:
|
||||
```rust
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ChatId(pub i64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct MessageId(pub i64);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct UserId(pub i64);
|
||||
|
||||
impl From<i64> for ChatId {
|
||||
fn from(id: i64) -> Self {
|
||||
ChatId(id)
|
||||
}
|
||||
}
|
||||
|
||||
// Аналогично для MessageId и UserId
|
||||
```
|
||||
|
||||
**Преимущества**:
|
||||
- Невозможно случайно передать 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
|
||||
|
||||
**Проблема**: `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,
|
||||
}
|
||||
```
|
||||
|
||||
**Преимущества**:
|
||||
- Логическая группировка данных
|
||||
- Проще добавлять новые поля
|
||||
- Меньше параметров в конструкторах
|
||||
|
||||
---
|
||||
|
||||
## Приоритет 3: Архитектурные улучшения
|
||||
|
||||
### 7. Выделить UI компоненты
|
||||
|
||||
**Проблема**: Код рендеринга дублируется, сложно переиспользовать.
|
||||
|
||||
**Решение**: Создать `src/ui/components/`:
|
||||
```
|
||||
src/ui/components/
|
||||
├── mod.rs
|
||||
├── modal.rs # Базовый компонент модалки
|
||||
├── input_field.rs # Поле ввода с курсором
|
||||
├── message_bubble.rs # Пузырь сообщения
|
||||
├── chat_list_item.rs # Элемент списка чатов
|
||||
└── emoji_picker.rs # Picker эмодзи
|
||||
```
|
||||
|
||||
Каждый компонент — функция:
|
||||
```rust
|
||||
pub fn render_modal<F>(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
title: &str,
|
||||
render_content: F,
|
||||
) where
|
||||
F: FnOnce(&mut Frame, Rect),
|
||||
{
|
||||
// Общий код для всех модалок
|
||||
}
|
||||
```
|
||||
|
||||
**Преимущества**:
|
||||
- Переиспользуемые компоненты
|
||||
- Консистентный 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. Вынести логику группировки сообщений
|
||||
|
||||
**Проблема**: Логика группировки сообщений смешана с рендерингом в `messages.rs`.
|
||||
|
||||
**Решение**: Создать `src/message_grouping.rs`:
|
||||
```rust
|
||||
pub enum MessageGroup {
|
||||
DateSeparator(String),
|
||||
SenderHeader(String),
|
||||
Message(MessageInfo),
|
||||
}
|
||||
|
||||
pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup> {
|
||||
// Логика группировки по дате и отправителю
|
||||
}
|
||||
```
|
||||
|
||||
**Преимущества**:
|
||||
- Чистое разделение логики и представления
|
||||
- Легче тестировать группировку
|
||||
- Можно переиспользовать
|
||||
|
||||
---
|
||||
|
||||
### 10. Hotkey mapping в конфиг
|
||||
|
||||
**Проблема**: Хоткеи захардкожены в коде, нельзя настроить.
|
||||
|
||||
**Решение**: Добавить в `config.toml`:
|
||||
```toml
|
||||
[hotkeys]
|
||||
# Навигация
|
||||
up = ["k", "р", "Up"]
|
||||
down = ["j", "о", "Down"]
|
||||
left = ["h", "р", "Left"]
|
||||
right = ["l", "д", "Right"]
|
||||
|
||||
# Действия
|
||||
reply = ["r", "к"]
|
||||
forward = ["f", "а"]
|
||||
delete = ["d", "в", "Delete"]
|
||||
copy = ["y", "н"]
|
||||
react = ["e", "у"]
|
||||
```
|
||||
|
||||
Парсить в `src/config.rs`:
|
||||
```rust
|
||||
pub struct Hotkeys {
|
||||
pub up: Vec<char>,
|
||||
pub down: Vec<char>,
|
||||
// ...
|
||||
}
|
||||
|
||||
impl Hotkeys {
|
||||
pub fn matches(&self, key: KeyCode, action: &str) -> bool {
|
||||
// Проверка совпадения
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Преимущества**:
|
||||
- Пользовательская настройка хоткеев
|
||||
- Проще добавлять новые действия
|
||||
- Документация хоткеев в конфиге
|
||||
|
||||
---
|
||||
|
||||
## Приоритет 4: Качество кода
|
||||
|
||||
### 11. Добавить юнит-тесты
|
||||
|
||||
**Проблема**: Нет тестов, сложно убедиться в корректности.
|
||||
|
||||
**Решение**: Добавить тесты для:
|
||||
|
||||
```rust
|
||||
// tests/utils_test.rs
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_format_timestamp_with_tz() {
|
||||
let timestamp = 1640000000; // 2021-12-20 09:33:20 UTC
|
||||
assert_eq!(
|
||||
format_timestamp_with_tz(timestamp, "+03:00"),
|
||||
"12:33"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_timezone_offset() {
|
||||
assert_eq!(parse_timezone_offset("+03:00"), 3);
|
||||
assert_eq!(parse_timezone_offset("-05:00"), -5);
|
||||
assert_eq!(parse_timezone_offset("invalid"), 3); // fallback
|
||||
}
|
||||
}
|
||||
|
||||
// tests/config_test.rs
|
||||
#[test]
|
||||
fn test_parse_color() {
|
||||
let config = Config::default();
|
||||
assert_eq!(config.parse_color("red"), Color::Red);
|
||||
assert_eq!(config.parse_color("invalid"), Color::White); // fallback
|
||||
}
|
||||
|
||||
// tests/grouping_test.rs
|
||||
#[test]
|
||||
fn test_message_grouping_by_date() {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Запуск**: `cargo test`
|
||||
|
||||
---
|
||||
|
||||
### 12. Добавить rustdoc комментарии
|
||||
|
||||
**Проблема**: Публичное API не документировано.
|
||||
|
||||
**Решение**: Добавить doc-комментарии:
|
||||
```rust
|
||||
/// TDLib client wrapper for Telegram integration.
|
||||
///
|
||||
/// Handles authentication, chat management, message operations,
|
||||
/// and user caching.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```no_run
|
||||
/// let mut client = TdClient::new(api_id, api_hash).await?;
|
||||
/// client.start_authorization().await?;
|
||||
/// ```
|
||||
pub struct TdClient {
|
||||
// ...
|
||||
}
|
||||
|
||||
/// Loads configuration from ~/.config/tele-tui/config.toml
|
||||
///
|
||||
/// Creates default config if file doesn't exist.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Always returns a valid `Config`, using defaults if loading fails.
|
||||
pub fn load() -> Self {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Генерация**: `cargo doc --open`
|
||||
|
||||
---
|
||||
|
||||
### 13. Config валидация
|
||||
|
||||
**Проблема**: Невалидные значения в конфиге молча игнорируются.
|
||||
|
||||
**Решение**: Добавить валидацию:
|
||||
```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 консистентность
|
||||
|
||||
**Проблема**: Местами блокирующие вызовы в async контексте.
|
||||
|
||||
**Решение**: Ревью и исправление:
|
||||
- Использовать `tokio::fs` вместо `std::fs` для файловых операций в async
|
||||
- Использовать `tokio::time::sleep` вместо `std::thread::sleep`
|
||||
- Обернуть блокирующие вызовы в `spawn_blocking`
|
||||
|
||||
---
|
||||
|
||||
## Приоритет 5: Опциональные улучшения
|
||||
|
||||
### 15. Feature flags для зависимостей
|
||||
|
||||
**Проблема**: Все зависимости всегда включены.
|
||||
|
||||
**Решение**: В `Cargo.toml`:
|
||||
```toml
|
||||
[features]
|
||||
default = ["clipboard", "url-open"]
|
||||
clipboard = ["dep:arboard"]
|
||||
url-open = ["dep:open"]
|
||||
```
|
||||
|
||||
**Преимущества**:
|
||||
- Уменьшение размера бинарника
|
||||
- Опциональная функциональность
|
||||
|
||||
---
|
||||
|
||||
### 16. LRU cache обобщение
|
||||
|
||||
**Проблема**: Отдельные LRU кеши для `user_names` и `user_statuses`.
|
||||
|
||||
**Решение**: Создать обобщённый `LruCache<K, V>` или использовать готовый крейт `lru = "0.12"`.
|
||||
|
||||
---
|
||||
|
||||
### 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`:
|
||||
```toml
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Метрики прогресса
|
||||
|
||||
- [ ] Priority 1: 0/3 задач
|
||||
- [ ] Priority 2: 0/3 задач
|
||||
- [ ] Priority 3: 0/4 задач
|
||||
- [ ] Priority 4: 0/4 задач
|
||||
- [ ] Priority 5: 0/3 задач
|
||||
|
||||
**Всего**: 0/17 задач
|
||||
|
||||
---
|
||||
|
||||
## Предусловие: Тесты
|
||||
|
||||
**ВАЖНО**: Перед началом рефакторинга необходимо написать тесты!
|
||||
|
||||
См. [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. **Документация** — обновлять документацию после изменений
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
|
||||
- Этот документ живой и будет обновляться
|
||||
- Новые пункты добавляются по мере обнаружения
|
||||
- После завершения задачи отмечать в метриках
|
||||
- При появлении блокеров — документировать в соответствующей секции
|
||||
Reference in New Issue
Block a user