Обновлена документация для отражения завершения задачи P2.6 (реструктуризация MessageInfo). Изменения: - CONTEXT.md: добавлен P2.6 в завершённые задачи Priority 2 - CONTEXT.md: обновлён статус Priority 2 (80%, 4/5 задач) - CONTEXT.md: добавлена детальная секция "Последние обновления" - CONTEXT.md: обновлён технический долг - REFACTORING_ROADMAP.md: отмечен P2.6 как завершённый - REFACTORING_ROADMAP.md: обновлён общий прогресс (41%, 7/17 задач) - REFACTORING_ROADMAP.md: добавлено "Что сделано" для P2.6 Статус: Priority 2 - 80% (4/5 задач) Осталась последняя задача: P2.7 MessageBuilder pattern Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
21 KiB
Refactoring Roadmap
Этот документ содержит список технического долга и планов по рефакторингу кодовой базы.
Приоритет 1: Критичные улучшения
1. Схлопнуть состояния чата в enum
Проблема: Сейчас состояния чата хранятся как отдельные boolean поля в App:
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:
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. Вынести константы в отдельный модуль
Проблема: Магические числа разбросаны по всему коду:
// В разных местах:
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:
// 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:
#[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.rstdlib/reactions.rs,tdlib/client.rsapp/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:
#[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+).
Решение: ✅ Реализовано - группировка в логические структуры:
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())
Приоритет 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 эмодзи
Каждый компонент — функция:
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:
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:
pub enum MessageGroup {
DateSeparator(String),
SenderHeader(String),
Message(MessageInfo),
}
pub fn group_messages(messages: &[MessageInfo]) -> Vec<MessageGroup> {
// Логика группировки по дате и отправителю
}
Преимущества:
- Чистое разделение логики и представления
- Легче тестировать группировку
- Можно переиспользовать
10. Hotkey mapping в конфиг
Проблема: Хоткеи захардкожены в коде, нельзя настроить.
Решение: Добавить в config.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:
pub struct Hotkeys {
pub up: Vec<char>,
pub down: Vec<char>,
// ...
}
impl Hotkeys {
pub fn matches(&self, key: KeyCode, action: &str) -> bool {
// Проверка совпадения
}
}
Преимущества:
- Пользовательская настройка хоткеев
- Проще добавлять новые действия
- Документация хоткеев в конфиге
Приоритет 4: Качество кода
11. Добавить юнит-тесты
Проблема: Нет тестов, сложно убедиться в корректности.
Решение: Добавить тесты для:
// 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-комментарии:
/// 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 валидация
Проблема: Невалидные значения в конфиге молча игнорируются.
Решение: Добавить валидацию:
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(())
}
}
Вызывать при загрузке:
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:
[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:
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 = "0.3"
Метрики прогресса
- Priority 1: 3/3 задач ✅ ЗАВЕРШЕНО!
- P1.1 — ChatState enum
- P1.2 — Разделить TdClient
- P1.3 — Константы
- Priority 2: 4/5 задач (80%)
- P2.5 — Error enum
- P2.3 — Config validation
- P2.4 — Newtype для ID
- P2.6 — MessageInfo реструктуризация
- P2.7 — MessageBuilder pattern
- Priority 3: 0/4 задач
- Priority 4: 0/4 задач
- Priority 5: 0/3 задач
Всего: 7/17 задач (41%)
Предусловие: Тесты
ВАЖНО: Перед началом рефакторинга необходимо написать тесты!
См. TESTING_ROADMAP.md для плана покрытия тестами.
Минимальное покрытие для начала рефакторинга:
- ✅ Фаза 0: Инфраструктура (helpers, fake client)
- ✅ Snapshot тесты для основных экранов (chat list, messages)
- ✅ Integration тесты для критичных flow (send, edit, navigation)
Зачем: Тесты гарантируют, что рефакторинг не сломает функциональность.
Порядок выполнения
Рекомендуется выполнять в следующем порядке:
- P1.3 — Константы (быстро, малый риск)
- P1.1 — ChatState enum (высокий impact)
- P2.5 — Error enum (улучшает весь код)
- P4.11 — Тесты для utils (базовая проверка)
- P1.2 — Разделить TdClient (большой рефакторинг)
- P2.4 — Newtype для ID (широкие изменения)
- P3.7 — UI компоненты (постепенно)
- P3.8 — Форматирование (изоляция логики)
- P3.9 — Группировка сообщений (изоляция логики)
- Остальные по необходимости
Принципы рефакторинга
- Один PR = одна задача — не смешивать рефакторинг разных областей
- Тесты прежде всего — добавить тесты перед рефакторингом
- Обратная совместимость — сохранять работоспособность на каждом шаге
- Маленькие шаги — лучше 10 маленьких PR, чем 1 огромный
- Документация — обновлять документацию после изменений
Примечания
- Этот документ живой и будет обновляться
- Новые пункты добавляются по мере обнаружения
- После завершения задачи отмечать в метриках
- При появлении блокеров — документировать в соответствующей секции