Compare commits

...

14 Commits

Author SHA1 Message Date
d3565c9ff9 Merge pull request 'fix(images): eliminate race condition when pressing v on downloading photo' (#26) from refactor into main
Reviewed-on: #26
2026-03-02 23:19:14 +00:00
Mikhail Kilin
90776448ce fix(images): eliminate race condition when pressing v on downloading photo
Some checks failed
ci/woodpecker/pr/check Pipeline failed
Previously, handle_view_image called td_client.download_file() synchronously
while process_pending_chat_init already had a background synchronous=true
download in flight for the same file. TDLib returned is_downloading_completed=false
causing the view to fail on first press.

Fix: replace the blocking download in NotDownloaded/Downloading branches with
a pending_image_open intent flag. The main loop opens the modal automatically
when the background download result arrives via photo_download_rx. If no
background channel exists, a new one is started via direct tdlib_rs call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 02:15:51 +03:00
6344e0ff6a Merge pull request 'refactor' (#25) from refactor into main
Reviewed-on: #25
2026-03-02 22:22:24 +00:00
Mikhail Kilin
c89a5e13f8 chore: remove leftover backup files from src/
Some checks failed
ci/woodpecker/pr/check Pipeline failed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 01:17:47 +03:00
Mikhail Kilin
07a41ff796 chore: remove unused and outdated files
- config.example.toml: duplicate of config.toml.example
- REFACTORING_ROADMAP.md, REFACTORING_OPPORTUNITIES.md: refactoring done in Phase 13
- TESTING_PROGRESS.md, TESTING_ROADMAP.md: stale since February, superseded by ROADMAP.md
- CHANGELOG.md: never maintained
- FAQ.md, CONTRIBUTING.md, SECURITY.md, INSTALL.md: boilerplate for a personal project
- .github/: GitHub templates unused (project hosted on Gitea)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 01:15:42 +03:00
Mikhail Kilin
e2971e5ff5 chore: add symbol_info_budget and language_backend fields to serena config
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 01:04:55 +03:00
de18d6978b Merge pull request 'refactor' (#24) from refactor into main
Reviewed-on: #24
2026-03-02 22:00:07 +00:00
Mikhail Kilin
dea3559da7 docs: remove out-of-scope items from Phase 14 Etap 4 roadmap
Some checks failed
ci/woodpecker/pr/check Pipeline failed
Remove account deletion from modal and parallel polling — these won't
be implemented in the current scope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:57:18 +03:00
Mikhail Kilin
260b81443e style: replace DarkGray with Rgb(160,160,160) for better terminal compatibility
DarkGray renders differently across terminals; a specific RGB value gives
consistent appearance. Also always show the account indicator in the footer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:57:06 +03:00
Mikhail Kilin
df89c4e376 test: update footer snapshots to always show account name
Snapshots now reflect the new behaviour where the account indicator
is always visible (including "default"), matching the footer.rs change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:52:56 +03:00
Mikhail Kilin
ec2758ce18 refactor: consolidate message loading logic into chat_loader.rs
Move all three phases of chat message loading from scattered locations
into a single dedicated module for better cohesion and navigability:
- Phase 1: open_chat_and_load_data (from handlers/chat_list.rs)
- Phase 2: process_pending_chat_init (extracted from 70-line inline block in main.rs)
- Phase 3: load_older_messages_if_needed (from handlers/chat.rs)

No behaviour changes — pure refactoring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 00:48:39 +03:00
564df43910 Merge pull request 'fix: always reserve space for selection marker to prevent text shift' (#23) from refactor into main
Reviewed-on: #23
2026-02-24 12:59:04 +00:00
Mikhail Kilin
a095fe277b fix: always reserve space for selection marker to prevent text shift
Some checks failed
ci/woodpecker/pr/check Pipeline failed
Render "  " (2 spaces) for unselected messages instead of nothing,
so text stays aligned when navigating with the ▶ selection indicator.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 15:49:08 +03:00
42f16b1a2b Merge pull request 'feat: per-account lock protection + fix message navigation' (#22) from refactor into main 2026-02-24 12:39:01 +00:00
56 changed files with 378 additions and 9432 deletions

View File

@@ -1,40 +0,0 @@
---
name: Bug Report
about: Сообщить о проблеме или баге
title: '[BUG] '
labels: bug
assignees: ''
---
## Описание бага
Четкое и краткое описание проблемы.
## Шаги для воспроизведения
1. Запустить '...'
2. Нажать на '...'
3. Прокрутить вниз до '...'
4. Увидеть ошибку
## Ожидаемое поведение
Что должно было произойти.
## Фактическое поведение
Что произошло на самом деле.
## Скриншоты
Если применимо, добавьте скриншоты для демонстрации проблемы.
## Окружение
- **ОС**: [например, macOS 14.0, Ubuntu 22.04, Windows 11]
- **Rust версия**: [вывод `rustc --version`]
- **tele-tui версия**: [вывод `cargo pkgid`]
- **Размер терминала**: [например, 100x30]
## Логи
Если есть логи или сообщения об ошибках, вставьте их сюда:
```
вставьте логи здесь
```
## Дополнительный контекст
Любая другая информация, которая может помочь в решении проблемы.

View File

@@ -1,34 +0,0 @@
---
name: Feature Request
about: Предложить новую функцию или улучшение
title: '[FEATURE] '
labels: enhancement
assignees: ''
---
## Связано с проблемой?
Есть ли проблема, которую это решит? Например: "Меня расстраивает, что [...]"
## Описание решения
Четкое и краткое описание того, что вы хотите.
## Альтернативы
Какие альтернативные решения или функции вы рассматривали?
## Примеры использования
Как эта функция будет использоваться? Приведите примеры:
1. Пользователь делает X
2. Система делает Y
3. Результат: Z
## Приоритет
- [ ] Критичная функция — без неё приложение малополезно
- [ ] Важная функция — значительно улучшит UX
- [ ] Nice to have — было бы удобно
## Проверка roadmap
- [ ] Я проверил [ROADMAP.md](../ROADMAP.md) и этой функции там нет
## Дополнительный контекст
Скриншоты, ссылки на похожие реализации в других приложениях, и т.д.

View File

@@ -1,51 +0,0 @@
## Описание
Краткое описание изменений в этом PR.
## Тип изменений
- [ ] Bug fix (исправление бага)
- [ ] New feature (новая функция)
- [ ] Breaking change (изменение, ломающее обратную совместимость)
- [ ] Refactoring (рефакторинг без изменения функциональности)
- [ ] Documentation (изменения в документации)
- [ ] Performance improvement (улучшение производительности)
## Связанные Issue
Fixes #(номер issue)
## Как протестировано?
Опишите тесты, которые вы провели:
- [ ] Тест A
- [ ] Тест B
- [ ] Тест C
## Сценарии тестирования
Подробные шаги для проверки изменений:
1. Запустить `cargo run`
2. Сделать X
3. Убедиться, что Y
## Чеклист
- [ ] Мой код следует стилю проекта
- [ ] Я запустил `cargo fmt`
- [ ] Я запустил `cargo clippy` и исправил warnings
- [ ] Код компилируется без ошибок (`cargo build`)
- [ ] Я протестировал изменения вручную
- [ ] Я обновил документацию (если необходимо)
- [ ] Я добавил тесты (если применимо)
- [ ] Все существующие тесты проходят
## Скриншоты (если применимо)
Добавьте скриншоты для демонстрации UI изменений.
## Дополнительные заметки
Любая дополнительная информация для ревьюверов.

View File

@@ -108,3 +108,14 @@ default_modes:
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
fixed_tools: []
# override of the corresponding setting in serena_config.yml, see the documentation there.
# If null or missing, the value from the global config is used.
symbol_info_budget:
# The language backend to use for this project.
# If not set, the global setting from serena_config.yml is used.
# Valid values: LSP, JetBrains
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:

View File

@@ -1,66 +0,0 @@
# Changelog
Все значительные изменения в этом проекте будут документированы в этом файле.
Формат основан на [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
и этот проект придерживается [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.1.0] - 2024-12-XX
### Добавлено
#### Базовая функциональность
- TDLib интеграция с авторизацией (телефон + код + 2FA)
- Отображение списка чатов с поддержкой папок
- Загрузка и отображение истории сообщений
- Отправка текстовых сообщений
- Vim-style навигация (hjkl) с поддержкой русской раскладки (ролд)
- Поиск по чатам (Ctrl+S)
- Поиск внутри чата (Ctrl+F)
#### Сообщения
- Группировка по дате и отправителю
- Markdown форматирование (жирный, курсив, подчёркивание, зачёркивание, код, спойлеры)
- Редактирование сообщений
- Удаление сообщений с подтверждением
- Reply на сообщения
- Forward сообщений
- Копирование в системный буфер обмена
- Реакции на сообщения с emoji picker
#### UI/UX
- Индикаторы: онлайн-статус (●), прочитанность (✓/✓✓), редактирование (✎)
- Иконки: 📌 закреплённые чаты, 🔇 замьюченные, @ упоминания
- Typing indicator ("печатает...")
- Закреплённые сообщения
- Профиль пользователя/чата
- Черновики с автосохранением
- Динамический инпут (расширение до 10 строк)
- Блочный курсор с навигацией
- Состояние сети в футере
#### Конфигурация
- TOML конфигурация (~/.config/tele-tui/config.toml)
- Настройка часового пояса
- Настройка цветовой схемы
- Приоритетная загрузка credentials из XDG config dir
#### Оптимизации
- 60 FPS рендеринг
- LRU кеширование пользователей (лимит 500)
- Lazy loading имён пользователей
- Лимиты памяти (500 сообщений на чат, 200 чатов)
- Graceful shutdown
### Изменено
- Время отображается с учётом настроенного timezone
### Исправлено
- Корректная обработка TDLib updates в отдельном потоке
- Правильное выравнивание для длинных сообщений
- Приоритет обработки input для модалок
[Unreleased]: https://github.com/your-username/tele-tui/compare/v0.1.0...HEAD
[0.1.0]: https://github.com/your-username/tele-tui/releases/tag/v0.1.0

View File

@@ -1,125 +0,0 @@
# Contributing to tele-tui
Спасибо за интерес к проекту! Мы рады любому вкладу.
## Как помочь проекту
### Сообщить о баге
1. Проверьте, нет ли уже такого issue в [Issues](https://github.com/your-username/tele-tui/issues)
2. Создайте новый issue с описанием:
- Шаги для воспроизведения
- Ожидаемое поведение
- Фактическое поведение
- Версия ОС и Rust
- Логи (если есть)
### Предложить новую фичу
1. Проверьте [ROADMAP.md](ROADMAP.md) — возможно, эта фича уже запланирована
2. Создайте issue с меткой `enhancement`
3. Опишите:
- Зачем нужна эта фича
- Как она должна работать
- Примеры использования
### Внести код
1. **Fork** репозитория
2. Создайте **feature branch**: `git checkout -b feature/amazing-feature`
3. Прочитайте [DEVELOPMENT.md](DEVELOPMENT.md) для понимания процесса разработки
4. Внесите изменения
5. Протестируйте локально
6. Commit: `git commit -m 'Add amazing feature'`
7. Push: `git push origin feature/amazing-feature`
8. Создайте **Pull Request**
## Правила кода
### Стиль кода
- Используйте `cargo fmt` перед коммитом
- Следуйте [Rust API Guidelines](https://rust-lang.github.io/api-guidelines/)
- Добавляйте комментарии для сложной логики
### Структура коммитов
```
<type>: <краткое описание>
<подробное описание (опционально)>
```
Типы:
- `feat`: новая фича
- `fix`: исправление бага
- `refactor`: рефакторинг без изменения функциональности
- `docs`: изменения в документации
- `style`: форматирование, отступы
- `test`: добавление тестов
- `chore`: обновление зависимостей, конфигурации
Примеры:
```
feat: add emoji reactions to messages
fix: correct timezone offset calculation
docs: update installation instructions
```
### Тестирование
- Протестируйте вручную все изменения
- Опишите сценарии тестирования в PR
- Убедитесь, что `cargo build` проходит без ошибок
- Убедитесь, что `cargo fmt` и `cargo clippy` не дают предупреждений
## Процесс Review
1. Maintainer проверит ваш PR
2. Возможны комментарии и запросы на изменения
3. После одобрения PR будет смержен
4. Ваш вклад появится в следующем релизе
## Архитектура проекта
Перед началом работы рекомендуем ознакомиться:
- [REQUIREMENTS.md](REQUIREMENTS.md) — функциональные требования
- [CONTEXT.md](CONTEXT.md) — текущий статус и архитектурные решения
- [ROADMAP.md](ROADMAP.md) — план развития
### Структура кода
```
src/
├── main.rs # Event loop, инициализация
├── config.rs # Конфигурация
├── app/ # Состояние приложения
├── ui/ # Отрисовка UI
├── input/ # Обработка ввода
├── utils.rs # Утилиты
└── tdlib/ # TDLib интеграция
```
### Ключевые принципы
1. **Неблокирующий UI**: TDLib updates в отдельном потоке
2. **Оптимизация памяти**: LRU кеши, лимиты на коллекции
3. **Vim-style навигация**: консистентные хоткеи
4. **Graceful degradation**: fallback для отсутствующих данных
## Code of Conduct
- Будьте вежливы и уважительны
- Конструктивная критика приветствуется
- Фокус на технических аспектах
## Вопросы?
Создайте issue с меткой `question` или свяжитесь с maintainers.
## Лицензия
Внося код в этот проект, вы соглашаетесь с лицензией [MIT](LICENSE).

227
FAQ.md
View File

@@ -1,227 +0,0 @@
# FAQ — Часто задаваемые вопросы
## Установка и запуск
### Где получить API credentials?
1. Перейдите на https://my.telegram.org/apps
2. Войдите с вашим номером телефона
3. Создайте новое приложение
4. Скопируйте `api_id` и `api_hash`
### Где хранить credentials?
**Рекомендуется** (приоритет 1):
```bash
~/.config/tele-tui/credentials
```
**Альтернатива** (приоритет 2):
```bash
.env # в корне проекта
```
### Ошибка "Telegram API credentials not found!"
Убедитесь, что вы создали файл credentials (см. выше) с правильным форматом:
```
API_ID=12345678
API_HASH=abcdef1234567890abcdef1234567890
```
### Где хранится сессия Telegram?
В папке `./tdlib_data/` в директории запуска приложения. Эта папка содержит:
- Токены авторизации
- Кеш сообщений
- Другие данные TDLib
**Важно**: Не удаляйте эту папку, иначе придётся заново авторизоваться.
## Использование
### Как переключаться между папками?
Нажмите клавиши `1-9` для переключения между первыми 9 папками Telegram.
### Как искать сообщения в чате?
1. Откройте чат
2. Нажмите `Ctrl+F`
3. Введите поисковый запрос
4. Используйте `n` / `N` для навигации по результатам
### Как скопировать текст сообщения?
1. При пустом поле ввода нажмите `↑` для выбора сообщения
2. Нажмите `y` (или `н` на русской раскладке)
3. Текст скопирован в системный буфер обмена
### Как ответить на сообщение?
1. Выберите сообщение (`↑` при пустом инпуте)
2. Нажмите `r` (или `к` на русской раскладке)
3. Введите ответ
4. Нажмите `Enter`
### Как удалить сообщение?
1. Выберите сообщение
2. Нажмите `d` / `в` / `Delete`
3. Подтвердите удаление: `y` / `Enter`
### Как добавить реакцию?
1. Выберите сообщение
2. Нажмите `e` (или `у` на русской раскладке)
3. Выберите emoji стрелками
4. Нажмите `Enter`
### Почему не работают хоткеи на русской раскладке?
Убедитесь, что вы используете **русскую раскладку**, а не транслит. Поддерживаемые комбинации:
- `р о л д``h j k l` (навигация)
- `к``r` (reply)
- `а``f` (forward)
- `в``d` (delete)
- `н``y` (copy)
- `у``e` (react)
## Конфигурация
### Где находится конфигурационный файл?
```bash
~/.config/tele-tui/config.toml
```
Создаётся автоматически при первом запуске.
### Как изменить часовой пояс?
Отредактируйте `~/.config/tele-tui/config.toml`:
```toml
[general]
timezone = "+05:00" # Ваш часовой пояс
```
### Как изменить цветовую схему?
Отредактируйте секцию `[colors]` в конфиге:
```toml
[colors]
incoming_message = "cyan"
outgoing_message = "lightgreen"
selected_message = "lightyellow"
```
Поддерживаемые цвета: black, red, green, yellow, blue, magenta, cyan, gray, white, darkgray, lightred, lightgreen, lightyellow, lightblue, lightmagenta, lightcyan.
### Нужно ли перезапускать приложение после изменения конфига?
Да, изменения в `config.toml` применяются только при запуске приложения.
## Проблемы
### Приложение зависает при запуске
Возможные причины:
1. **Нет интернета**: проверьте подключение
2. **TDLib не может подключиться**: проверьте firewall/прокси
3. **Неверные credentials**: проверьте API_ID и API_HASH
### Сообщения не загружаются
1. Проверьте статус сети в футере (внизу экрана)
2. Попробуйте обновить: `Ctrl+R`
3. Перезапустите приложение
### "Deleted Account" в списке чатов
Это пользователи, которые удалили свой аккаунт Telegram. Они автоматически фильтруются и не отображаются в списке.
### Не отображаются медиафайлы
Медиафайлы (фото, видео, голосовые, стикеры) отображаются как заглушки: [Фото], [Видео], [Голосовое], [Стикер]. Полная поддержка медиа может быть добавлена в будущем.
### Ошибка компиляции при сборке
**TDLib download failed**:
- Проверьте интернет-соединение
- Убедитесь, что у вас достаточно места на диске
**Linking with cc failed**:
- macOS: `xcode-select --install`
- Linux: `sudo apt-get install build-essential`
- Windows: установите Visual Studio Build Tools
### Как сбросить сессию?
Удалите папку `tdlib_data/`:
```bash
rm -rf tdlib_data/
```
При следующем запуске потребуется заново авторизоваться.
## Производительность
### Приложение тормозит
Проверьте:
1. Количество открытых чатов (лимит 200)
2. Количество сообщений в открытом чате (лимит 500)
3. Размер терминала (минимум 80x20)
Приложение автоматически очищает старые данные при достижении лимитов.
### Высокое использование памяти
Это нормально при большом количестве чатов и сообщений. Приложение использует LRU кеши с ограничениями:
- 500 пользователей в кеше
- 500 сообщений на чат
- 200 чатов
## Разработка
### Как внести вклад в проект?
См. [CONTRIBUTING.md](CONTRIBUTING.md)
### Где найти план развития?
См. [ROADMAP.md](ROADMAP.md)
### Как сообщить о баге?
Создайте issue на GitHub с описанием:
- Шаги для воспроизведения
- Ожидаемое и фактическое поведение
- Версия ОС и Rust
- Логи (если есть)
## Безопасность
### Безопасно ли хранить credentials в файле?
Да, если вы:
1. Используете `~/.config/tele-tui/credentials`
2. Установили права доступа: `chmod 600 ~/.config/tele-tui/credentials`
3. Не коммитите этот файл в git (уже в `.gitignore`)
### Что делать при компрометации credentials?
1. Удалите приложение на https://my.telegram.org/apps
2. Создайте новое приложение с новыми credentials
3. Обновите файл `credentials`
4. Удалите папку `tdlib_data/` и авторизуйтесь заново
### Включена ли двухфакторная аутентификация?
Если вы включили 2FA в Telegram, приложение запросит пароль при первой авторизации.
## Ещё вопросы?
Создайте issue на GitHub или свяжитесь с maintainers.

View File

@@ -1,122 +0,0 @@
# Установка tele-tui
## Требования
- **Rust**: версия 1.70 или выше ([установить](https://rustup.rs/))
- **TDLib**: скачивается автоматически через tdlib-rs
## Шаг 1: Клонирование репозитория
```bash
git clone https://github.com/your-username/tele-tui.git
cd tele-tui
```
## Шаг 2: Получение API credentials
1. Перейдите на https://my.telegram.org/apps
2. Войдите с вашим номером телефона
3. Создайте новое приложение
4. Скопируйте **api_id** и **api_hash**
## Шаг 3: Настройка credentials
### Вариант A: XDG config directory (рекомендуется)
Создайте файл `~/.config/tele-tui/credentials`:
```bash
mkdir -p ~/.config/tele-tui
cat > ~/.config/tele-tui/credentials << EOF
API_ID=your_api_id_here
API_HASH=your_api_hash_here
EOF
```
### Вариант B: .env файл
Создайте файл `.env` в корне проекта:
```bash
cp credentials.example .env
# Отредактируйте .env и вставьте ваши credentials
```
## Шаг 4: Сборка
```bash
cargo build --release
```
## Шаг 5: Запуск
```bash
cargo run --release
```
Или запустите скомпилированный бинарник:
```bash
./target/release/tele-tui
```
## Первый запуск
При первом запуске вам нужно будет:
1. Ввести номер телефона (с кодом страны, например: +79991234567)
2. Ввести код подтверждения из Telegram
3. Если включена 2FA — ввести пароль
Сессия сохраняется в `./tdlib_data/`, при следующем запуске авторизация не потребуется.
## Настройка (опционально)
Конфигурационный файл создаётся автоматически при первом запуске в `~/.config/tele-tui/config.toml`.
Вы можете отредактировать его для настройки:
- Часового пояса
- Цветовой схемы
Пример конфигурации см. в файле `config.toml.example`.
## Устранение неполадок
### "Telegram API credentials not found!"
Убедитесь, что вы создали файл credentials (см. Шаг 3).
### "error: linking with `cc` failed"
Убедитесь, что у вас установлен C компилятор:
- macOS: `xcode-select --install`
- Linux: `sudo apt-get install build-essential` (Debian/Ubuntu)
- Windows: установите Visual Studio Build Tools
### TDLib download failed
Проверьте подключение к интернету. TDLib скачивается автоматически при первой сборке.
## Обновление
```bash
git pull
cargo build --release
```
Ваши credentials и конфигурация сохранятся.
## Удаление
Чтобы полностью удалить приложение и все данные:
```bash
# Удалить проект
rm -rf tele-tui/
# Удалить конфигурацию и credentials
rm -rf ~/.config/tele-tui/
# Удалить сессию Telegram (опционально, потребуется новая авторизация)
# rm -rf ./tdlib_data/
```

View File

@@ -1,855 +0,0 @@
# Возможности для рефакторинга
> Результаты аудита кодовой базы от 2026-02-02
> Обновлено: 2026-02-04
> Статус: В работе (2/10 категорий полностью завершены, 3 частично)
## Оглавление
1. [Дублирование кода](#1-дублирование-кода)
2. [Большие файлы/функции](#2-большие-файлыфункции)
3. [Сложная вложенность](#3-сложная-вложенность)
4. [Нарушение Single Responsibility](#4-нарушение-single-responsibility)
5. [Плохая инкапсуляция](#5-плохая-инкапсуляция)
6. [Отсутствующие абстракции](#6-отсутствующие-абстракции)
7. [Несогласованность](#7-несогласованность)
8. [Перекрытие функциональности](#8-перекрытие-функциональности)
9. [Проблемы производительности](#9-проблемы-производительности)
10. [Отсутствующие архитектурные паттерны](#10-отсутствующие-архитектурные-паттерны)
---
## 1. Дублирование кода
**Приоритет:** 🔴 Высокий
**Статус:** ✅ ПОЛНОСТЬЮ ЗАВЕРШЕНО! (2026-02-02)
**Объем:** 15-20% кодовой базы → Устранено!
### Проблемы
- **Timeout/Retry паттерны** (~20 экземпляров в обработке ввода)
- Повторяющаяся логика таймаутов в `src/input/main_input.rs`
- Одинаковые паттерны retry в разных обработчиках
- **Обработка модальных окон** (5+ мест)
- Логика открытия/закрытия модалок дублируется
- Валидация ввода в модальных окнах повторяется
- Обработка Escape для закрытия модалок в каждом месте
- **Паттерны валидации**
- Проверка пустых строк
- Валидация ID чатов/сообщений
- Проверка длины текста
### Решение
- [x] Создать `retry_utils.rs` с функциями `with_timeout()`, `with_retry()` - **Выполнено и интегрировано** (2026-02-02)
- Создан `src/utils/retry.rs` с тремя функциями: `with_timeout()`, `with_timeout_msg()`, `with_timeout_ignore()`
- Заменены ВСЕ прямые использования `tokio::time::timeout` (8+ мест: main_input.rs, auth.rs, main.rs)
- Код стал чище и короче (убрано вложенное Ok/Err матчинг)
- **100% покрытие** - больше нет прямых timeout вызовов
- [x] Создать `modal_handler.rs` с общей логикой модальных окон - **Выполнено** (2026-02-01)
- Создан `src/utils/modal_handler.rs` (120+ строк)
- 4 функции: `handle_modal_key()`, `should_close_modal()`, `should_confirm_modal()`, `handle_yes_no()`
- Enum `ModalAction` для type-safe обработки
- Поддержка английской и русской раскладки (y/д, n/т)
- 4 unit теста (все проходят)
- [x] Создать `validation.rs` с переиспользуемыми валидаторами - **Выполнено и интегрировано** (2026-02-02)
- Создан `src/utils/validation.rs` (180+ строк)
- 7 функций валидации: `is_non_empty()`, `is_within_length()`, `is_valid_chat_id()`, `is_valid_message_id()`, `is_valid_user_id()`, `has_items()`, `validate_text_input()`
- Покрывает все основные паттерны валидации
- 7 unit тестов (все проходят)
- **Интегрировано в 4 местах:** auth.rs (phone/code/password), main_input.rs (message validation)
### Файлы
- `src/input/main_input.rs`
- `src/app/handlers/*.rs`
- `src/ui/modals/*.rs`
---
## 2. Большие файлы/функции
**Приоритет:** 🔴 Высокий
**Статус:****ПОЛНОСТЬЮ ЗАВЕРШЕНО!** (обновлено 2026-02-04)
**Объем:** Все 4 файла отрефакторены! (4/4, 100%! 🎉)
### Проблемы
| Файл | Строки | Проблема | Статус |
|------|--------|----------|--------|
| `src/input/main_input.rs` | ~~1164~~ → ~1200 | ~~Одна функция `handle()` на ~800 строк~~ | ✅ **РЕШЕНО** (handle() → 82 строки) |
| `src/tdlib/client.rs` | ~~1259~~ → 599 | ~~Смешение facade и бизнес-логики~~ | ✅ **РЕШЕНО** (1259 → 599 строк, -52%) |
| `src/ui/messages.rs` | 905 | ~~Рендеринг всех типов сообщений~~ | ✅ **НЕ ТРЕБУЕТСЯ** (render() → 92 строки, Phase 5) |
| `src/tdlib/messages.rs` | ~~850~~ → 757 | ~~Обработка всех типов обновлений сообщений~~ | ✅ **РЕШЕНО** (convert_message() → 57 строк, -62%) |
### Решение
#### 2.1. Разделить `src/input/main_input.rs` - ✅ **ЗАВЕРШЕНО** (2026-02-03)
**Phase 1-2** (2026-02-02):
- [x] Создана структура `src/input/handlers/` (7 модулей) - ПОДГОТОВКА
- [x] Создан `handlers/clipboard.rs` (~100 строк) - извлечён из main_input
- [x] Создан `handlers/global.rs` (~90 строк) - извлечён из main_input
- [x] Созданы заглушки: `profile.rs`, `search.rs`, `modal.rs`, `messages.rs`, `chat_list.rs`
**Phase 2-3** (2026-02-03):
- [x] **Извлечено 13 специализированных функций-обработчиков** (~946 строк):
- `handle_open_chat_keyboard_input()` (~129 строк)
- `handle_chat_list_navigation()` (~34 строки)
- `handle_profile_mode()` (~120 строк)
- `handle_message_search_mode()` (~73 строки)
- `handle_pinned_mode()` (~42 строки)
- `handle_reaction_picker_mode()` (~90 строк)
- `handle_delete_confirmation()` (~60 строк)
- `handle_forward_mode()` (~52 строки)
- `handle_chat_search_mode()` (~43 строки)
- `handle_enter_key()` (~145 строк)
- `handle_escape_key()` (~35 строк)
- `handle_message_selection()` (~95 строк)
- `handle_profile_open()` (~28 строк)
**Phase 4** (2026-02-03):
- [x] **Упрощена вложенность** (early returns, let-else guards)
- [x] **Извлечено 3 вспомогательных функции**:
- `edit_message()` (~50 строк)
- `send_new_message()` (~55 строк)
- `perform_message_search()` (~20 строк)
**Итоговый результат**:
- ✅ Функция `handle()` сократилась с **891 до 82 строк** (91% сокращение! 🎉)
- ✅ Глубина вложенности: **6+ уровней → 2-3 уровня**
-Все 196 тестов проходят успешно
- ✅ Код стал **линейным и простым для понимания**
**Примечание**: Вместо создания отдельных файлов в handlers/ (что привело бы к поломке), мы выбрали подход извлечения функций внутри main_input.rs. Это позволило радикально упростить код без риска регрессий.
#### 2.2. Разделить `src/tdlib/client.rs` - ✅ **ЗАВЕРШЕНО** (2026-02-04)
**Этап 1** (2026-02-04): Извлечение Update Handlers
- [x] Создан модуль `src/tdlib/update_handlers.rs` (302 строки)
- [x] **Извлечено 8 handler функций** (~350 строк):
- `handle_new_message_update()` — добавление новых сообщений (44 строки)
- `handle_chat_action_update()` — статус набора текста (32 строки)
- `handle_chat_position_update()` — управление позициями чатов (36 строк)
- `handle_user_update()` — обработка информации о пользователях (40 строк)
- `handle_message_interaction_info_update()` — обновление реакций (44 строки)
- `handle_message_send_succeeded_update()` — успешная отправка (35 строк)
- `handle_chat_draft_message_update()` — черновики сообщений (15 строк)
- `handle_auth_state()` — изменение состояния авторизации (10 строк)
- [x] Обновлён `handle_update()` для делегирования в update_handlers
- [x] Результат: **client.rs 1259 → 983 строки** (22% сокращение)
**Этап 2** (2026-02-04): Извлечение Message Converter
- [x] Создан модуль `src/tdlib/message_converter.rs` (250 строк)
- [x] **Извлечено 6 conversion функций** (~240 строк):
- `convert_message()` — основная конвертация TDLib → MessageInfo (150+ строк)
- `extract_reply_info()` — извлечение reply информации (30 строк)
- `extract_forward_info()` — извлечение forward информации (25 строк)
- `extract_reactions()` — извлечение реакций (20 строк)
- `get_origin_sender_name()` — получение имени отправителя (15 строк)
- `update_reply_info_from_loaded_messages()` — обновление reply из кэша (30 строк)
- [x] Исправлены ошибки компиляции с неверными именами полей
- [x] Обновлены вызовы в update_handlers.rs
- [x] Результат: **client.rs 983 → 754 строки** (23% сокращение)
**Этап 3** (2026-02-04): Извлечение Chat Helpers
- [x] Создан модуль `src/tdlib/chat_helpers.rs` (149 строк)
- [x] **Извлечено 3 helper функции** (~140 строк):
- `find_chat_mut()` — поиск чата по ID (15 строк)
- `update_chat()` — обновление чата через closure (15 строк, используется 9+ раз)
- `add_or_update_chat()` — добавление/обновление чата в списке (110+ строк)
- [x] Использован sed для замены вызовов методов по всей кодовой базе
- [x] Результат: **client.rs 754 → 599 строк** (21% сокращение)
**Итоговый результат**:
- ✅ Файл `client.rs` сократился с **1259 до 599 строк** (52% сокращение! 🎉)
- ✅ Создано **3 новых модуля** с чёткой ответственностью:
- `update_handlers.rs` — обработка всех типов TDLib Update
- `message_converter.rs` — конвертация TDLib Message → MessageInfo
- `chat_helpers.rs` — утилиты для работы с чатами
-Все **590+ тестов** проходят успешно
- ✅ Код стал **модульным и лучше организованным**
-`TdClient` теперь ближе к **facade pattern** (делегирует в специализированные модули)
#### 2.3. Упростить `src/ui/messages.rs` - ✅ **ЗАВЕРШЕНО** (Phase 5, 2026-02-03)
**Уже выполнено в Phase 5**:
- [x] Извлечены 4 функции рендеринга (~350 строк):
- `render_chat_header()` — заголовок с typing status (~47 строк)
- `render_pinned_bar()` — панель закреплённого сообщения (~30 строк)
- `render_message_list()` — список сообщений с автоскроллом (~98 строк)
- `render_input_box()` — input с режимами (forward/select/edit/reply) (~146 строк)
- [x] Функция `render()` сократилась с **390 до 92 строк** (76% сокращение! 🎉)
- [x] Глубина вложенности: **6+ уровней → 2-3 уровня**
- [x] Код стал **модульным и простым для понимания**
**Итоговый результат**:
- ✅ Файл остался цельным (905 строк), но хорошо организован
- ✅ Главная функция `render()` компактная (92 строки)
-Все вспомогательные функции извлечены (render_search_mode, render_pinned_mode, и др.)
-**Дальнейшее разделение не требуется** — цели достигнуты
#### 2.4. Упростить `src/tdlib/messages.rs` - ✅ **ЗАВЕРШЕНО** (2026-02-04)
**Этап 1** (2026-02-04): Извлечение Message Conversion Helpers
- [x] Создан модуль `src/tdlib/message_conversion.rs` (158 строк)
- [x] **Извлечено 6 вспомогательных функций**:
- `extract_content_text()` — извлечение текста из различных типов сообщений (~80 строк)
- `extract_entities()` — извлечение форматирования (~10 строк)
- `extract_sender_name()` — получение имени отправителя с API вызовом (~15 строк)
- `extract_forward_info()` — информация о пересылке (~12 строк)
- `extract_reply_info()` — информация об ответе (~15 строк)
- `extract_reactions()` — реакции на сообщение (~26 строк)
- [x] Метод `convert_message()` сократился с **150 до 57 строк** (62% сокращение! 🎉)
- [x] Результат: **messages.rs 850 → 757 строк** (11% сокращение)
**Итоговый результат**:
- ✅ Файл `messages.rs` сократился до **757 строк**
- ✅ Создан модуль **message_conversion.rs** с переиспользуемыми функциями
- ✅ Метод `convert_message()` теперь **компактный и читаемый** (57 строк)
-Все **629 тестов** проходят успешно
-**Дальнейшее разделение не требуется** — MessageManager хорошо организован
### Файлы
- `src/input/main_input.rs`
- `src/tdlib/client.rs`
- `src/ui/messages.rs`
- `src/tdlib/messages.rs`
---
## 3. Сложная вложенность
**Приоритет:** 🟡 Средний
**Статус:****ПОЛНОСТЬЮ ЗАВЕРШЕНО!** (обновлено 2026-02-04)
**Объем:** ~30 функций → 0 функций (все проблемные решены)
### Проблемы
- ~~4-5 уровней вложенности в обработке ввода~~ ✅ **Решено в main_input.rs**
- Глубокая вложенность в обработке обновлений TDLib
- ~~Множественные `if let` / `match` вложенные друг в друга~~ ✅ **Решено в main_input.rs**
### Примеры
```rust
// src/input/main_input.rs - было (типичный пример)
if let Some(chat_id) = app.selected_chat {
if let Some(message_id) = app.selected_message {
if app.is_message_outgoing(chat_id, message_id) {
match key.code {
// еще больше вложенности (6+ уровней)
}
}
}
}
// Стало (после Phase 4 рефакторинга)
let Some(chat_id) = app.selected_chat else { return Ok(false) };
let Some(message_id) = app.selected_message else { return Ok(false) };
if !app.is_message_outgoing(chat_id, message_id) {
return Ok(false); // early return
}
// Линейная логика (2-3 уровня максимум)
```
### Решение
#### Выполнено в `src/input/main_input.rs` (2026-02-03)
- [x] **Применены early returns** - уменьшили вложенность с 6+ до 2-3 уровней
- [x] **Извлечена вложенная логика** в 3 функции:
- `edit_message()` — редактирование сообщения (~50 строк)
- `send_new_message()` — отправка нового сообщения (~55 строк)
- `perform_message_search()` — поиск по сообщениям (~20 строк)
- [x] **Использованы let-else guard clauses** — современный Rust паттерн
- [x] **Упрощены 6 функций**:
- `handle_profile_mode()` — упрощён блок Enter с let-else
- `handle_profile_open()` — применён early return guard
- `handle_enter_key()` — разделена на части, сокращена с ~130 до ~40 строк
- `handle_message_search_mode()` — извлечена логика поиска
- `handle_escape_key()` — преобразован в early returns
- `handle_message_selection()` — применены let-else guards
**Результат Phase 4**:
- ✅ Глубина вложенности: **6+ уровней → 2-3 уровня**
- ✅ Код стал **максимально линейным и читаемым**
- ✅ Применены современные Rust паттерны (let-else, guards)
#### Выполнено в `src/tdlib/client.rs` (2026-02-03, Этап 3)
- [x] **Добавлены helper методы** для устранения дублирования:
- `find_chat_mut()` — поиск чата по ID
- `update_chat()` — обновление чата через closure (использовано 9+ раз)
- [x] **Извлечено 5 handler методов** из `handle_update()`:
- `handle_chat_position_update()` — управление позициями чатов (43 строки)
- `handle_user_update()` — обработка информации о пользователях (46 строк)
- `handle_message_interaction_info_update()` — обновление реакций (44 строки)
- `handle_message_send_succeeded_update()` — успешная отправка (38 строк)
- `handle_chat_draft_message_update()` — черновики (18 строк)
- [x] **Упрощено 7 функций** с применением let-else guards, early returns, unwrap_or_else:
- `handle_chat_action_update()` — статус набора текста (4 → 2 уровня)
- `handle_new_message_update()` — добавление сообщений (3 → 2 уровня)
- `handle_chat_draft_message_update()` — черновики (if-let → match)
- `handle_user_update()` — usernames (вложенные if-let → and_then)
- `convert_message()` — кэш имён (if-let → unwrap_or_else)
- `extract_reply_info()` — reply информация (вложенные if-let → map/or_else)
- `update_reply_info_from_loaded_messages()` — обновление reply (4 → 1-2 уровня)
**Результат Этапа 3 (client.rs)**:
- ✅ Функция `handle_update()` сократилась с **268 до 122 строк** (54% сокращение!)
- ✅ Устранено дублирование: ~9 повторений pattern → 2 helper метода
- ✅ Глубина вложенности: **4-5 уровней → 2-3 уровня**
- ✅ Применены modern patterns: let-else guards, early returns, filter chains
#### Дополнительные улучшения вложенности (2026-02-04)
- [x] **Упрощена `src/tdlib/messages.rs`** (строки 718-755)
- `fetch_missing_reply_info()`: 7 уровней → 2-3 уровня
- Извлечена функция `fetch_and_update_reply()`
- Использованы let-else guards и iterator chains
- Максимальная вложенность: **44 → 28 пробелов**
- [x] **Упрощена `src/tdlib/messages.rs`** (строки 147-182)
- `get_chat_history()` retry loop: 6 уровней → 3 уровня
- Извлечен `messages_obj` после match
- Early continue для пустых результатов
- Использован `.flatten()` вместо вложенного if-let
- [x] **Упрощена `src/input/main_input.rs`** (строки 500-546)
- `handle_forward_mode()`: 7 уровней → 2-3 уровня
- Извлечена функция `forward_selected_message()`
- Использованы early returns (let-else guards)
- Максимальная вложенность: **40 → 36 пробелов**
- [x] **Упрощена `src/input/main_input.rs`** (reaction picker)
- Извлечена функция `send_reaction()`
- Использованы let-else guards
- Вложенность: 5 уровней → 2-3 уровня
- [x] **Упрощена `src/input/main_input.rs`** (scroll + load older)
- Извлечена функция `load_older_messages_if_needed()`
- Использованы early returns
- Вложенность: 6 уровней → 2-3 уровня
- [x] **Упрощена `src/config.rs`** (строки 563-609)
- `load_credentials()`: 7 уровней → 2-3 уровня
- Извлечены функции `load_credentials_from_file()` и `load_credentials_from_env()`
- Использованы `?` operator для Option chains
- Максимальная вложенность: **36 → 32 пробелов**
**Итоговый результат**:
-Все файлы с вложенностью >32 пробелов обработаны
- ✅ Применены современные Rust паттерны (let-else guards, early returns, ? operator, iterator chains)
- ✅ Извлечено 8 новых функций для разделения ответственности
- ✅ Максимальная вложенность во всем проекте: **≤32 пробелов (8 уровней)**
### Файлы
-`src/input/main_input.rs`**ПОЛНОСТЬЮ ЗАВЕРШЕНО** (Phase 4 + доп. улучшения: 40 → 36 пробелов)
-`src/tdlib/client.rs`**ЗАВЕРШЕНО** (Этап 3: 268 → 122 строки в handle_update)
-`src/tdlib/messages.rs`**ПОЛНОСТЬЮ ЗАВЕРШЕНО** (44 → 28 пробелов)
-`src/config.rs`**ПОЛНОСТЬЮ ЗАВЕРШЕНО** (36 → 32 пробелов)
-Все остальные модули — **проверены, вложенность приемлема** (≤32 пробелов)
---
## 4. Нарушение Single Responsibility
**Приоритет:** 🟡 Средний
**Статус:**Не начато
**Объем:** 2 основных структуры
### Проблемы
#### 4.1. `App` struct (50+ методов)
Смешивает ответственности:
- UI state management
- Business logic
- TDLib interaction
- Input handling
- Search logic
- Profile management
- Folder management
#### 4.2. `TdClient` (facade + бизнес-логика)
Смешивает:
- Facade pattern (делегирование)
- Update processing
- Cache management
- Network operations
### Решение
#### Разделить `App`
- [ ] Создать `ChatListState` (состояние списка чатов)
- [ ] Создать `MessageViewState` (состояние просмотра сообщений)
- [ ] Создать `ComposeState` (состояние написания сообщения)
- [ ] Создать `SearchState` (состояние поиска)
- [ ] Создать `ProfileState` (состояние профиля)
- [ ] `App` становится координатором этих state объектов
#### Разделить `TdClient`
- [ ] `TdClient` только facade (делегирование)
- [ ] Бизнес-логика в `MessageService`, `ChatService`, etc.
- [ ] Update processing в отдельном модуле
### Файлы
- `src/app/mod.rs`
- `src/tdlib/client.rs`
---
## 5. Плохая инкапсуляция
**Приоритет:** 🔴 Высокий
**Статус:** ✅ Частично выполнено (2026-02-01)
**Объем:** Вся структура `App`
### Проблемы
- **22 публичных поля** в `App`
```rust
pub struct App {
pub td_client: TdClient,
pub chats: Vec<ChatInfo>,
pub selected_chat: Option<ChatId>,
pub messages: HashMap<ChatId, Vec<MessageInfo>>,
// ... еще 18 полей
}
```
- **Прямой доступ везде**
```rust
app.selected_chat = Some(chat_id); // Плохо
app.chats.push(new_chat); // Плохо
app.messages.clear(); // Плохо
```
- **Тесты манипулируют внутренностями**
```rust
app.td_client.user_cache.chat_user_ids.insert(...); // Слишком глубоко
```
### Решение
- [x] Сделать критичные поля приватными - **Частично выполнено** (2026-02-01)
- ✅ `config` сделан приватным (readonly через getter `app.config()`)
- ✅ Добавлены 30+ методов-геттеров и сеттеров для всех полей
- ⏳ Остальные поля оставлены pub для совместимости (требуется массовый рефакторинг)
- [x] Добавить getter методы где нужно - **Выполнено**
- 30+ методов: `phone_input()`, `set_phone_input()`, `screen()`, `set_screen()`, `is_loading()`, и т.д.
- [ ] Полная инкапсуляция всех полей (требует обновления 170+ мест в коде)
- [ ] Создать методы для операций (вместо прямого доступа)
```rust
// Вместо app.selected_chat = Some(chat_id)
app.select_chat(chat_id); // Уже есть!
// Вместо app.chats.push(new_chat)
app.add_chat(new_chat); // TODO
```
### Файлы
- `src/app/mod.rs`
- `src/app/state.rs` (новый)
- Все тесты
---
## 6. Отсутствующие абстракции
**Приоритет:** 🟡 Средний
**Статус:** ✅ Частично выполнено (2026-02-04)
**Объем:** 3 основные абстракции (2/3 завершены, 1/3 уже была)
### Проблемы
#### 6.1. Создать `KeyHandler` trait ✅ ЗАВЕРШЕНО! (2026-02-04)
- [x] Создать `src/input/key_handler.rs` - **Выполнено** (380+ строк)
- Enum `KeyResult` (Handled, HandledNeedsRedraw, NotHandled, Quit)
- Trait `KeyHandler` с методом `handle_key()` и `priority()`
- Struct `GlobalKeyHandler` - обработчик глобальных команд (Quit, OpenSearch)
- Struct `ChatListKeyHandler` - навигация по списку чатов, выбор папок
- Struct `MessageViewKeyHandler` - скролл сообщений, поиск в чате
- Struct `MessageSelectionKeyHandler` - действия с выбранным сообщением
- Struct `KeyHandlerChain` - цепочка обработчиков с приоритетами
- 3 unit теста (все проходят)
- [ ] Интегрировать в main_input.rs (заменить текущую логику)
- [ ] Добавить недостающие методы в App (enter_search_mode и т.д.)
#### 6.2. Нет абстракции для network operations
Timeout/retry логика дублируется:
```rust
// Повторяется ~20 раз
let result = tokio::time::timeout(
Duration::from_millis(100),
operation()
).await;
```
#### 6.3. Хардкод горячих клавиш
Невозможно изменить без правки кода:
```rust
KeyCode::Char('e') => edit_message(), // Хардкод
KeyCode::Char('d') => delete_message(), // Хардкод
```
### Решение
#### 6.1. Создать `KeyHandler` trait
- [ ] Создать `src/input/key_handler.rs`
```rust
trait KeyHandler {
fn handle_key(&mut self, app: &mut App, key: KeyEvent) -> Result<bool>;
}
```
- [ ] Реализовать для каждого экрана:
- `ChatListKeyHandler`
- `MessagesKeyHandler`
- `ComposeKeyHandler`
- `SearchKeyHandler`
#### 6.2. Создать network utilities
- [ ] Создать `src/utils/network.rs`
```rust
async fn with_timeout<F, T>(f: F, timeout_ms: u64) -> Result<T>
async fn with_retry<F, T>(f: F, max_retries: u32) -> Result<T>
```
#### 6.3. Создать систему горячих клавиш ✅ ЗАВЕРШЕНО! (2026-02-04)
- [x] Создать `src/config/keybindings.rs` - **Выполнено**
- Enum `Command` с 40+ командами (навигация, чат, сообщения, input)
- Struct `KeyBinding` с поддержкой модификаторов (Ctrl, Shift, Alt и т.д.)
- Struct `Keybindings` с HashMap<Command, Vec<KeyBinding>>
- Поддержка множественных bindings для одной команды (EN/RU раскладки)
- Сериализация/десериализация KeyCode и KeyModifiers
- 4 unit теста (все проходят)
- [ ] Интегрировать в приложение (вместо HotkeysConfig)
- [ ] Загружать из конфига (опционально, с fallback на defaults)
### Файлы
- `src/input/key_handler.rs` (новый)
- `src/utils/network.rs` (новый)
- `src/config/keybindings.rs` (новый)
---
## 7. Несогласованность
**Приоритет:** 🟢 Низкий
**Статус:** ❌ Не начато
**Объем:** Вся кодовая база
### Проблемы
#### 7.1. Разные типы ошибок
```rust
// В одних местах
Result<T, String>
// В других
Result<T, Box<dyn Error>>
// В третьих
Result<T> // с неявным типом ошибки
```
#### 7.2. Разные паттерны state management
- В одних местах флаги (`is_editing: bool`)
- В других энумы (`EditMode::Active`)
- В третьих Option (`editing_message: Option<MessageId>`)
#### 7.3. Разные подходы к валидации
- Иногда в UI слое
- Иногда в бизнес-логике
- Иногда в обработчиках ввода
### Решение
- [ ] Стандартизировать обработку ошибок (один тип ошибки)
- [ ] Выбрать единый подход к state management (enum-based)
- [ ] Определить слой для валидации (бизнес-логика)
- [ ] Создать style guide в документации
### Файлы
- Вся кодовая база
---
## 8. Перекрытие функциональности
**Приоритет:** 🟡 Средний
**Статус:** ✅ Выполнено (2026-02-04)
**Объем:** 2 основные области (2/2 завершены)
### Проблемы
#### 8.1. Централизовать фильтрацию чатов ✅ ЗАВЕРШЕНО! (2026-02-04)
- [x] Создать `src/app/chat_filter.rs` - **Выполнено** (470+ строк)
- Struct `ChatFilterCriteria` с builder pattern
- Поддержка фильтрации по: папке, поиску, pinned, unread, mentions, muted, archived
- Struct `ChatFilter` с методами фильтрации
- Enum `ChatSortOrder` для сортировки (ByLastMessage, ByTitle, ByUnreadCount, PinnedFirst)
- Методы подсчёта: count, count_unread, count_unread_mentions
- 6 unit тестов (все проходят)
- [ ] Заменить дублирующуюся логику в App и UI на ChatFilter
- [ ] Удалить старые методы фильтрации из App
#### 8.2. Централизовать обработку сообщений ✅ ЗАВЕРШЕНО! (2026-02-04)
- [x] Создать `src/app/message_service.rs` - **Выполнено** (508+ строк)
- Struct `MessageGroup` для группировки по дате
- Struct `SenderGroup` для группировки по отправителю
- Struct `MessageSearchResult` с контекстом поиска
- Struct `MessageService` с 13 методами бизнес-логики:
- `group_by_date()` - группировка сообщений по датам
- `group_by_sender()` - группировка по отправителю
- `search()` - полнотекстовый поиск с контекстом
- `find_next()` / `find_previous()` - навигация по результатам
- `filter_by_sender()` / `filter_unread()` - фильтрация
- `find_by_id()` / `find_index_by_id()` - поиск по ID
- `get_last_n()` - получение последних N сообщений
- `get_in_date_range()` - фильтрация по диапазону дат
- `count_by_sender_type()` - статистика по типам
- `create_index()` - создание быстрого индекса
- 7 unit тестов (все проходят)
- [ ] Заменить разрозненную логику в App/UI на MessageService
- [ ] Чёткое разделение: TDLib → Service → UI
### Решение
#### 8.1. Централизовать фильтрацию ✅
- [x] Создать `src/app/chat_filter.rs` - **Выполнено**
- [x] Один источник правды для фильтрации - **Выполнено**
- [ ] UI и App используют его - TODO (требует интеграции)
#### 8.2. Четко разделить слои обработки сообщений ✅
- [x] `tdlib/messages.rs` - только получение и преобразование - **Выполнено**
- [x] `app/message_service.rs` - бизнес-логика - **Выполнено**
- [x] `ui/messages.rs` - только рендеринг - **Было уже реализовано**
### Файлы
- `src/app/chat_filter.rs` (новый)
- `src/app/message_service.rs` (новый)
- `src/tdlib/messages.rs`
- `src/ui/messages.rs`
---
## 9. Проблемы производительности
**Приоритет:** 🟢 Низкий
**Статус:** ❌ Не начато
**Объем:** Локальные оптимизации
### Проблемы
#### 9.1. Множественные клоны в обработке ввода
```rust
let text = app.input_text.clone(); // Клон
let chat_id = app.selected_chat.clone(); // Клон
// Используются только для чтения
```
#### 9.2. Нет кеширования результатов поиска
- Каждый поиск выполняется заново
- Нет инвалидации кеша при изменениях
#### 9.3. Неэффективная LRU cache
- `Vec::retain()` + `Vec::push()` на каждый доступ
- O(n) вместо потенциального O(1)
### Решение
- [ ] Заменить клоны на borrowing где возможно
- [ ] Добавить `SearchCache` с TTL
- [ ] Оптимизировать `LruCache` (использовать `VecDeque` или готовую библиотеку)
### Файлы
- `src/input/main_input.rs`
- `src/app/search.rs`
- `src/tdlib/users.rs` (LruCache)
---
## 10. Отсутствующие архитектурные паттерны
**Приоритет:** 🟢 Низкий
**Статус:** ❌ Не начато
**Объем:** Архитектурные изменения
### Проблемы
#### 10.1. Нет Event Bus
Компоненты напрямую вызывают друг друга:
- Сложно тестировать
- Сильная связанность
- Тяжело добавлять новые фичи
#### 10.2. Нет Repository паттерна
Прямой доступ к данным везде:
- `app.messages.get(chat_id)`
- `app.chats.iter().find(...)`
- Нет единой точки доступа к данным
#### 10.3. Нет Service Layer
Бизнес-логика размазана:
- Часть в `App`
- Часть в `TdClient`
- Часть в UI handlers
### Решение
#### 10.1. Event Bus (опционально)
- [ ] Создать `src/event_bus.rs`
- [ ] Pub/Sub для событий между компонентами
- [ ] Decoupling
#### 10.2. Repository Pattern
- [ ] Создать `src/repositories/chat_repository.rs`
- [ ] Создать `src/repositories/message_repository.rs`
- [ ] Создать `src/repositories/user_repository.rs`
- [ ] Единая точка доступа к данным
#### 10.3. Service Layer
- [ ] Создать `src/services/chat_service.rs`
- [ ] Создать `src/services/message_service.rs`
- [ ] Создать `src/services/search_service.rs`
- [ ] Вся бизнес-логика в сервисах
### Файлы
- `src/event_bus.rs` (новый, опционально)
- `src/repositories/*.rs` (новые)
- `src/services/*.rs` (новые)
---
## Приоритизация
### 🔴 Высокий приоритет (начать первым)
1. **Дублирование кода** - быстрый win, улучшит поддерживаемость
2. **Большие файлы** - критично для навигации и понимания кода
3. **Плохая инкапсуляция** - защитит от ошибок, улучшит API
### 🟡 Средний приоритет (после высокого)
4. **Сложная вложенность** - улучшит читаемость
5. **Single Responsibility** - улучшит архитектуру
6. **Отсутствующие абстракции** - упростит расширение
7. **Перекрытие функциональности** - уберет путаницу
### 🟢 Низкий приоритет (когда будет время)
8. **Несогласованность** - косметические улучшения
9. **Производительность** - пока не critical path
10. **Архитектурные паттерны** - nice to have
---
## План выполнения
### Фаза 1: Быстрые победы (1-2 дня)
- [x] #1: Создать утилиты для дублирующегося кода - **ЗАВЕРШЕНО** (2026-02-02)
- retry utils: 100% покрытие (все timeout заменены)
- modal_handler: интегрирован в 2 диалогах
- validation: интегрирован в 4 местах
- [ ] #5: Инкапсулировать поля App - **Частично** (геттеры добавлены)
### Фаза 2: Разделение больших файлов (3-5 дней)
- [ ] #2.1: Разделить `main_input.rs`
- [ ] #2.2: Разделить `client.rs`
- [ ] #2.3: Разделить `messages.rs`
### Фаза 3: Улучшение архитектуры (5-7 дней)
- [ ] #4: Разделить ответственности App/TdClient
- [ ] #6: Добавить абстракции (KeyHandler, network utils)
- [ ] #8: Убрать перекрытие функциональности
### Фаза 4: Полировка (2-3 дня)
- [x] #3: Упростить вложенность - **Частично** (main_input.rs завершён 2026-02-03)
- [ ] #7: Стандартизировать подходы
- [ ] #9: Оптимизировать производительность
### Фаза 5: Архитектурные паттерны (опционально)
- [ ] #10: Рассмотреть Event Bus / Repository / Service Layer
---
## Метрики
### До рефакторинга
- Строк кода: ~15,000
- Файлов: ~50
- Средний размер файла: 300 строк
- Максимальный файл: 1167 строк
- Дублирование: ~15-20%
- Публичных полей в App: 22
- Прямые вызовы timeout: 8+
### Текущее состояние (2026-02-04)
- ✅ Дублирование timeout: **УСТРАНЕНО** (0 прямых вызовов, все через retry utils)
- ✅ Дублирование modal: **УСТРАНЕНО** (используется modal_handler)
- ✅ Дублирование validation: **УСТРАНЕНО** (используется validation utils)
- ✅ Вложенность в main_input.rs: **УПРОЩЕНА** (6+ уровней → 2-3 уровня)
- ✅ Размер handle() в main_input.rs: **СОКРАЩЁН** (891 строк → 82 строки, 91% сокращение)
- ✅ Размер client.rs: **СОКРАЩЁН** (1259 строк → 599 строк, 52% сокращение)
- ✅ Размер render() в ui/messages.rs: **СОКРАЩЁН** (390 строк → 92 строки, 76% сокращение)
- ✅ Размер convert_message() в tdlib/messages.rs: **СОКРАЩЁН** (150 строк → 57 строк, 62% сокращение)
- ⏳ Публичных полей в App: 22 → 21 (config приватный, геттеры добавлены)
-**Все большие функции отрефакторены!** 🎉
### Цели после рефакторинга
- Максимальный файл: <500 строк
- Дублирование: <5% ✅ **ДОСТИГНУТО для категории #1!**
- Глубина вложенности: ≤3 уровня ✅ **ДОСТИГНУТО для main_input.rs!**
- Публичных полей в App: 0
- Все файлы <400 строк (в идеале)
- Улучшенная тестируемость
- Более четкое разделение ответственностей

File diff suppressed because it is too large Load Diff

View File

@@ -164,7 +164,5 @@
- `pending_account_switch` флаг → обработка в main loop
- [ ] **Этап 4: Расширенные возможности мультиаккаунта**
- Удаление аккаунта из модалки
- Хоткеи `Ctrl+1`..`Ctrl+9` — быстрое переключение
- Бейджи непрочитанных с других аккаунтов (требует множественных TdClient)
- Параллельный polling updates со всех аккаунтов

View File

@@ -1,64 +0,0 @@
# Security Policy
## Поддерживаемые версии
| Версия | Поддержка |
| ------ | ------------------ |
| 0.1.x | :white_check_mark: |
## Сообщить об уязвимости
Если вы обнаружили уязвимость в безопасности, пожалуйста:
1. **НЕ создавайте публичный issue**
2. Отправьте email на: [security@example.com]
3. Опишите:
- Тип уязвимости
- Шаги для воспроизведения
- Потенциальное влияние
- Предложения по исправлению (если есть)
## Безопасность credentials
### Важно
- **НИКОГДА** не коммитьте файлы с credentials в git
- Файлы `credentials` и `.env` уже добавлены в `.gitignore`
- TDLib сессия в `tdlib_data/` содержит токены авторизации — НЕ делитесь этой папкой
### Рекомендации
1. **Используйте XDG config directory**:
- Храните credentials в `~/.config/tele-tui/credentials`
- Установите права доступа: `chmod 600 ~/.config/tele-tui/credentials`
2. **Защита сессии**:
- Папка `tdlib_data/` содержит вашу авторизованную сессию
- Не копируйте её на другие машины
- При компрометации — удалите папку и авторизуйтесь заново
3. **Двухфакторная аутентификация**:
- Настоятельно рекомендуется включить 2FA в Telegram
- Это защитит ваш аккаунт даже при утечке API credentials
## Известные ограничения
### TDLib
- Приложение использует официальную библиотеку TDLib от Telegram
- Безопасность зависит от актуальности TDLib (автообновление через tdlib-rs)
### Конфигурация
- Конфигурационный файл `config.toml` НЕ содержит чувствительных данных
- Credentials хранятся отдельно в файле `credentials`
## Обновления безопасности
Мы оперативно реагируем на сообщения об уязвимостях и выпускаем патчи в течение:
- **Критические**: 24-48 часов
- **Высокие**: 3-7 дней
- **Средние**: 2-4 недели
- **Низкие**: включаются в следующий релиз
## Спасибо
Мы ценим ваш вклад в безопасность проекта!

View File

@@ -1,571 +0,0 @@
# Testing Progress Report
## Текущий статус: ВСЕ ТЕСТЫ ЗАВЕРШЕНЫ! 🎉🎊🚀
Все UI snapshot тесты и все integration тесты готовы! Превзошли план!
Дата: 2026-01-30 (обновлено #6 — ФИНАЛ)
---
## ✅ Что сделано
### Phase 2: Integration Tests (99%) 🔥
**Всего:** 73 integration теста из 74 запланированных
#### Phase 2.1: Send Message Flow (100%) ✅
**Файл**: `tests/send_message.rs` (6 тестов)
- ✅ Отправка текстового сообщения
- ✅ Отправка нескольких сообщений обновляет список
- ✅ Отправка с markdown форматированием
- ✅ Отправка в разные чаты
- ✅ Получение входящего сообщения
- ✅ Отправка с reply
#### Phase 2.2: Edit Message Flow (100%) ✅
**Файл**: `tests/edit_message.rs` (6 тестов)
- ✅ Редактирование текста сообщения
- ✅ Установка edit_date после редактирования
- ✅ Проверка can_be_edited перед редактированием
- ✅ Редактирование только своих сообщений
- ✅ Множественные редактирования
- ✅ Редактирование с форматированием
#### Phase 2.3: Delete Message Flow (100%) ✅
**Файл**: `tests/delete_message.rs` (6 тестов)
- ✅ Удаление сообщения из списка
- ✅ Множественные удаления
- ✅ Проверка can_be_deleted
- ✅ Удаление только своих сообщений
- ✅ Удаление из разных чатов
- ✅ Delete with revoke
#### Phase 2.4: Reply & Forward Flow (100%) ✅
**Файл**: `tests/reply_forward.rs` (8 тестов)
- ✅ Reply на сообщение с превью
- ✅ Reply сохраняет связь с оригиналом
- ✅ Forward сообщения
- ✅ Forward с sender_name
- ✅ Forward в разные чаты
- ✅ Reply + Forward комбо
- ✅ Reply на forwarded сообщение
- ✅ Forward reply сообщения
#### Phase 2.5: Reactions Flow (100%) ✅
**Файл**: `tests/reactions.rs` (10 тестов)
- ✅ Добавление реакции на сообщение
- ✅ Удаление реакции (toggle)
- ✅ Множественные реакции на одно сообщение
- ✅ Реакции от разных пользователей
- ✅ Подсчёт реакций
- ✅ Chosen реакция (своя)
- ✅ Реакции обновляются в реальном времени
- ✅ Получение доступных реакций чата
- ✅ Реакции на forwarded сообщения
- ✅ Очистка всех реакций
#### Phase 2.6: Search Flow (100%) ✅
**Файл**: `tests/search.rs` (8 тестов)
- ✅ Поиск по названию чата
- ✅ Поиск по @username
- ✅ Поиск по сообщениям в чате
- ✅ Навигация по результатам поиска
- ✅ Case-insensitive поиск
- ✅ Поиск с пробелами
- ✅ Поиск возвращает пустой список если нет совпадений
- ✅ Очистка поиска
#### Phase 2.7: Drafts Flow (100%) ✅
**Файл**: `tests/drafts.rs` (7 тестов)
- ✅ Сохранение черновика при переключении чатов
- ✅ Восстановление черновика при возврате
- ✅ Удаление черновика после отправки
- ✅ Черновики для разных чатов независимы
- ✅ Индикатор черновика в списке чатов
- ✅ Пустой черновик не сохраняется
- ✅ Черновик сохраняется при закрытии чата
#### Phase 2.8: Navigation Flow (100%) ✅
**Файл**: `tests/navigation.rs` (7 тестов)
- ✅ Навигация по списку чатов (↑/↓)
- ✅ Открытие чата (Enter)
- ✅ Закрытие чата (Esc)
- ✅ Скролл сообщений (↑/↓)
- ✅ Переключение между папками (1-9)
- ✅ Навигация с wrap (переход с конца на начало)
- ✅ Навигация в пустом списке
#### Phase 2.9: Profile Flow (100%) ✅
**Файл**: `tests/profile.rs` (6 тестов)
- ✅ Открытие профиля личного чата
- ✅ Профиль показывает имя и username
- ✅ Профиль показывает телефон
- ✅ Открытие профиля группы
- ✅ Профиль группы показывает участников
- ✅ Закрытие профиля (Esc)
#### Phase 2.10: Network & Typing Flow (100%) ✅
**Файл**: `tests/network_typing.rs` (9 тестов)
- ✅ Typing indicator при наборе текста
- ✅ Отправка typing action
- ✅ Получение typing статуса
- ✅ Typing timeout
- ✅ Network state: WaitingForNetwork
- ✅ Network state: ConnectingToProxy
- ✅ Network state: Connecting
- ✅ Network state: Updating
- ✅ Network state: Ready
#### Phase 2.11: Copy Flow (100%) ✅
**Файл**: `tests/copy.rs` (9 тестов)
- ✅ Форматирование простого сообщения
- ✅ Форматирование с forward контекстом
- ✅ Форматирование с reply контекстом
- ✅ Форматирование с forward + reply одновременно
- ✅ Форматирование длинного сообщения
- ✅ Форматирование с markdown entities
- ✅ Clipboard initialization (игнорируется в CI)
- ✅ Копирование в реальный clipboard (ручное тестирование)
- ✅ Кроссплатформенность clipboard
#### Phase 2.12: Config Flow (100%) ✅
**Файл**: `tests/config.rs` (11 тестов)
- ✅ Дефолтные значения конфигурации
- ✅ Кастомные значения конфигурации
- ✅ Парсинг валидных цветов (red, green, blue, etc.)
- ✅ Парсинг light цветов (lightred, lightgreen, etc.)
- ✅ Парсинг невалидного цвета с fallback на White
- ✅ Case-insensitive парсинг цветов
- ✅ TOML сериализация и десериализация
- ✅ Частичный TOML использует дефолты
- ✅ Различные форматы timezone (+03:00, -05:00, +00:00)
- ✅ Загрузка credentials из переменных окружения
- ✅ Проверка формата ошибки когда credentials не найдены
---
### Фаза 1: UI Snapshot Tests (100%) ✅
**Всего:** 55 snapshot тестов
#### Фаза 1.1: Chat List (100%) ✅
**Файл**: `tests/chat_list.rs` (9 тестов)
#### Фаза 1.2: Messages (100%) ✅
**Файл**: `tests/messages.rs` (18 тестов)
#### Фаза 1.3: Modals (100%) ✅
**Файл**: `tests/modals.rs` (8 тестов)
#### Фаза 1.4: Input Field (100%) ✅
**Файл**: `tests/input_field.rs` (7 тестов)
#### Snapshot тесты для поля ввода:
-`snapshot_empty_input` — пустое поле ввода с плейсхолдером
-`snapshot_input_with_text` — поле с текстом и курсором █
-`snapshot_input_long_text_2_lines` — длинный текст на 2 строки
-`snapshot_input_long_text_max_lines` — очень длинный текст (максимум 10 строк)
-`snapshot_input_editing_mode` — режим редактирования с превью оригинального сообщения
-`snapshot_input_reply_mode` — режим ответа с превью сообщения
-`snapshot_input_search_mode` — поле поиска с query
#### Результаты:
- **7 новых snapshot тестов** — все проходят ✅
- **7 snapshots приняты** через `cargo insta accept`
- **Все тесты проходят**: 90 тестов (21 chat_list + 19 input_field + 30 messages + 20 modals)
---
### Фаза 1.6: Screens Snapshot Tests (100%) ✅
**Файл**: `tests/screens.rs` (7 тестов)
#### Snapshot тесты для полных экранов:
-`snapshot_loading_screen_default` — экран загрузки (дефолтный)
-`snapshot_loading_screen_with_status` — экран загрузки со статусом
-`snapshot_auth_screen_phone` — экран авторизации (ввод телефона)
-`snapshot_auth_screen_code` — экран авторизации (ввод кода)
-`snapshot_auth_screen_password` — экран авторизации (ввод пароля 2FA)
-`snapshot_main_screen_empty` — главный экран (пустой список чатов)
-`snapshot_main_screen_terminal_too_small` — предупреждение о маленьком терминале
#### Обновления TestAppBuilder:
- ✅ Добавлен метод `status_message(message)` — установить статус для loading screen
- ✅ Добавлен метод `auth_state(state)` — установить состояние авторизации
- ✅ Добавлен метод `phone_input(phone)` — установить phone input
- ✅ Добавлен метод `code_input(code)` — установить code input
- ✅ Добавлен метод `password_input(password)` — установить password input
- ✅ Добавлены поля: `status_message`, `auth_state`, `phone_input`, `code_input`, `password_input`
- ✅ Обновлен `build()` — применяет auth состояние и inputs
#### Результаты:
- **7 новых snapshot тестов** — все проходят ✅
- **7 snapshots приняты** через `cargo insta accept`
- **Все тесты проходят**: 127 тестов (21 chat_list + 19 input_field + 30 messages + 20 modals + 18 footer + 19 screens)
---
### Фаза 1.5: Footer Snapshot Tests (100%) ✅
**Файл**: `tests/footer.rs` (6 тестов)
#### Snapshot тесты для нижней панели:
-`snapshot_footer_chat_list` — footer в списке чатов
-`snapshot_footer_open_chat` — footer в открытом чате
-`snapshot_footer_network_waiting` — footer с "⚠ Нет сети"
-`snapshot_footer_network_connecting_proxy` — footer с "⏳ Прокси..."
-`snapshot_footer_network_connecting` — footer с "⏳ Подключение..."
-`snapshot_footer_search_mode` — footer в режиме поиска
#### Изменения:
- ✅ Сделан `footer` модуль публичным в `src/ui/mod.rs`
#### Результаты:
- **6 новых snapshot тестов** — все проходят ✅
- **6 snapshots приняты** через `cargo insta accept`
- **Все тесты проходят**: 96 тестов (21 chat_list + 19 input_field + 30 messages + 20 modals + 18 footer)
---
### Фаза 1.4: Input Field Snapshot Tests (100%) ✅
**Файл**: `tests/modals.rs` (8 тестов)
#### Snapshot тесты для модальных окон:
-`snapshot_delete_confirmation_modal` — модалка подтверждения удаления
-`snapshot_emoji_picker_default` — emoji picker с дефолтным выбором
-`snapshot_emoji_picker_with_selection` — emoji picker с выбранной реакцией (курсор)
-`snapshot_profile_personal_chat` — профиль личного чата
-`snapshot_profile_group_chat` — профиль группы (с участниками)
-`snapshot_pinned_message` — закреплённое сообщение вверху чата
-`snapshot_search_in_chat` — поиск в чате с результатами
-`snapshot_forward_mode` — режим пересылки (выбор чата)
#### Обновления TestAppBuilder:
- ✅ Добавлен метод `with_chats(chats)` — добавить несколько чатов сразу
- ✅ Добавлен метод `message_search(query)` — режим поиска по сообщениям
- ✅ Добавлен метод `forward_mode(message_id)` — режим пересылки
- ✅ Добавлены поля: `message_search_mode`, `message_search_query`, `forwarding_message_id`, `is_selecting_forward_chat`
#### Исправления:
- ✅ Переименованы тесты с динамическими датами (today/yesterday) на фиксированный old_date
- ✅ Удалены нестабильные snapshots зависящие от текущей даты
-Все модальные режимы теперь тестируются через snapshots
#### Результаты:
- **8 новых snapshot тестов** — все проходят ✅
- **8 snapshots приняты** через `cargo insta accept`
- **Все тесты проходят**: 71 тест (21 chat_list + 30 messages + 20 modals)
---
### Фаза 1.2: Messages Snapshot Tests (95%) ✅
**Файл**: `tests/messages.rs` (19 тестов)
#### Snapshot тесты для области сообщений:
-`snapshot_empty_chat` — пустой чат без сообщений
-`snapshot_single_incoming_message` — одно входящее сообщение
-`snapshot_single_outgoing_message` — одно исходящее сообщение
-`snapshot_date_separator_today` — разделитель "Сегодня"
-`snapshot_date_separator_yesterday` — разделитель "Вчера"
-`snapshot_sender_grouping` — группировка по отправителю (Alice → Alice → Bob)
-`snapshot_outgoing_sent` — исходящее с ✓ (отправлено)
-`snapshot_outgoing_read` — исходящее с ✓✓ (прочитано)
-`snapshot_edited_message` — сообщение с индикатором ✎
-`snapshot_long_message_wrap` — длинное сообщение с переносом
-`snapshot_markdown_bold_italic_code`**bold** *italic* `code`
-`snapshot_markdown_link_mention` — [links](url) и @mentions
-`snapshot_markdown_spoiler` — ||спойлер||
-`snapshot_media_placeholder` — [Фото], [Видео] и т.д.
-`snapshot_reply_message` — reply с превью оригинала
-`snapshot_forwarded_message` — ↪ Переслано от Alice
-`snapshot_single_reaction` — сообщение с одной реакцией [👍]
-`snapshot_multiple_reactions` — [👍] 5 👎 3
-`snapshot_selected_message` — выбранное сообщение (подсветка)
#### Обновления TestAppBuilder:
- ✅ Добавлен метод `with_message(chat_id, message)` — добавить одно сообщение
- ✅ Добавлен метод `with_messages(chat_id, messages)` — добавить несколько сообщений
- ✅ Добавлен метод `selecting_message(index)` — установить выбранное сообщение
- ✅ Обновлен `build()` — применяет сообщения к `app.td_client.current_chat_messages`
#### Результаты:
- **19 новых snapshot тестов** — все проходят ✅
- **19 snapshots приняты** через `cargo insta accept`
- **Все тесты проходят**: 52 теста (21 chat_list + 31 messages)
---
### Фаза 0: Инфраструктура (100%)
#### 1. Зависимости
- ✅ Добавлено `insta = "1.34"` для snapshot тестов
- ✅ Добавлено `tokio-test = "0.4"` для async тестов
- ✅ Настроен `.gitignore` для `.snap.new` файлов
#### 2. Test Helpers (5 модулей)
**`tests/helpers/mod.rs`**
- Экспортирует все вспомогательные модули
- Удобный доступ к TestAppBuilder, FakeTdClient и утилитам
**`tests/helpers/test_data.rs`**
-`TestChatBuilder` — fluent API для создания тестовых чатов
-`TestMessageBuilder` — fluent API для создания тестовых сообщений
- ✅ Хелперы: `create_test_chat()`, `create_test_message()`, `create_test_user()`
- ✅ Поддержка всех полей: unread, pinned, muted, mentions, reactions, reply, forward
**`tests/helpers/fake_tdclient.rs`**
-`FakeTdClient` — in-memory мок для интеграционных тестов
- ✅ Методы: `send_message()`, `edit_message()`, `delete_message()`, `add_reaction()`
- ✅ Tracking отправленных/отредактированных/удалённых сообщений
- ✅ Fluent API для построения клиента с данными
- ✅ Встроенные юнит-тесты для проверки мока
**`tests/helpers/snapshot_utils.rs`**
-`buffer_to_string()` — конвертация ratatui Buffer в строку для snapshots
-`render_to_buffer()` — рендеринг UI в виртуальный терминал
-`assert_ui_snapshot!` макрос для упрощения snapshot тестов
- ✅ Удаление trailing spaces для чистых snapshots
- ✅ Встроенные тесты
**`tests/helpers/app_builder.rs`**
-`TestAppBuilder` — fluent API для создания тестового App
- ✅ Методы: `with_chat()`, `selected_chat()`, `message_input()`, `searching()`, etc.
- ✅ Поддержка всех режимов: edit, reply, search, reaction_picker, profile
- ✅ Встроенные тесты для билдера
#### 3. Первые UI тесты
**`tests/ui/chat_list_test.rs`** (9 тестов)
- ✅ snapshot_empty_chat_list
- ✅ snapshot_chat_list_with_three_chats
- ✅ snapshot_chat_with_unread_count
- ✅ snapshot_chat_with_pinned
- ✅ snapshot_chat_with_muted
- ✅ snapshot_chat_with_mentions
- ✅ snapshot_selected_chat
- ✅ snapshot_chat_long_title
- ✅ snapshot_chat_search_mode
---
## 📊 Метрики
**Создано файлов**: 18
- 5 helpers
- 6 snapshot test files (chat_list, messages, modals, input_field, footer, screens)
- 10 integration test files (send_message, edit_message, delete_message, reply_forward, reactions, search, drafts, navigation, profile, network_typing)
- 1 mod.rs
**Строк кода**: ~6500+
- Helpers: ~1000 строк
- Snapshot тесты: ~1200 строк
- Integration тесты: ~4300 строк
**Тестов написано**:
- Snapshot тесты: 55
- Integration тесты: 73
- Helper тесты: ~12
- **Всего: 140+ тестов**
**Покрытие**:
- Фаза 0: Инфраструктура ✅ (100%)
- Фаза 1: UI Snapshot Tests ✅ (100%)
- 1.1 Chat List: 9/9 ✅
- 1.2 Messages: 18/18 ✅
- 1.3 Modals: 8/8 ✅
- 1.4 Input Field: 7/7 ✅
- 1.5 Footer: 6/6 ✅
- 1.6 Screens: 7/7 ✅
- Фаза 2: Integration Tests ✅ (100%!)
- 2.1 Send Message: 6/6 ✅
- 2.2 Edit Message: 6/6 ✅
- 2.3 Delete Message: 6/6 ✅
- 2.4 Reply & Forward: 8/8 ✅
- 2.5 Reactions: 10/10 ✅
- 2.6 Search: 8/8 ✅
- 2.7 Drafts: 7/7 ✅
- 2.8 Navigation: 7/7 ✅
- 2.9 Profile: 6/6 ✅
- 2.10 Network & Typing: 9/9 ✅
- 2.11 Copy: 9/9 ✅ (вместо 3!)
- 2.12 Config: 11/11 ✅ (вместо 8!)
- **Общий прогресс: 148/151 (98%) — ПРЕВЗОШЛИ ПЛАН!** 🎉
---
## 🏗️ Структура
```
tests/
├── helpers/
│ ├── mod.rs ✅ Создан
│ ├── app_builder.rs ✅ Создан + 5 тестов
│ ├── fake_tdclient.rs ✅ Создан + 4 теста
│ ├── snapshot_utils.rs ✅ Создан + 2 теста
│ └── test_data.rs ✅ Создан
└── ui/
├── mod.rs ✅ Создан
└── chat_list_test.rs ✅ Создан (9 snapshot тестов)
```
---
## 🎯 Примеры использования
### Создание тестового чата
```rust
let chat = TestChatBuilder::new("Mom", 123)
.unread_count(5)
.pinned()
.muted()
.draft("Hello...")
.build();
```
### Создание тестового App
```rust
let app = TestAppBuilder::new()
.with_chat(chat)
.selected_chat(123)
.message_input("Hello!")
.build();
```
### Snapshot тест
```rust
#[test]
fn snapshot_my_ui() {
let app = TestAppBuilder::new()
.with_chat(create_test_chat("Mom", 123))
.build();
let buffer = render_to_buffer(80, 24, |f| {
render_chat_list(f, f.size(), &app);
});
assert_snapshot!("my_ui", buffer_to_string(&buffer));
}
```
### Мок клиент для интеграционных тестов
```rust
let mut client = FakeTdClient::new()
.with_chat(create_test_chat("Mom", 123));
let msg_id = client.send_message(123, "Hello".to_string(), None);
assert_eq!(client.sent_messages().len(), 1);
```
---
## 🎉 ВСЕ ОСНОВНЫЕ ТЕСТЫ ЗАВЕРШЕНЫ!
### Прогресс: 98% (148/151 тестов) — ПРЕВЗОШЛИ ПЛАН! 🚀
**Все основные тесты готовы:**
- ✅ Phase 0: Инфраструктура (100%)
- ✅ Phase 1: UI Snapshot Tests (100%) — 55 тестов
- ✅ Phase 2: Integration Tests (100%!) — 93 теста
**Превзошли план на 9 тестов!**
- Copy Flow: 9 тестов (вместо 3)
- Config Flow: 11 тестов (вместо 8)
### Опциональные тесты (можно сделать позже)
#### Фаза 3: E2E Smoke Tests (4 теста)
**Файл**: `tests/e2e/smoke_test.rs`
- [ ] Приложение запускается без краша
- [ ] Приложение рендерит loading screen
- [ ] Приложение корректно завершается по Ctrl+C
- [ ] Минимальный размер терминала не крашит приложение
**Примечание**: E2E тесты требуют реального TDLib или сложного мока, поэтому опциональны.
#### Фаза 4: Дополнительные тесты (8 тестов)
**4.1 Utils Tests** (5 тестов)
- [ ] `format_timestamp_with_tz` с разными timezone
- [ ] `parse_timezone_offset` валидные значения
- [ ] `parse_timezone_offset` инвалидные значения (fallback)
- [ ] `format_date` для сегодня, вчера, старых дат
- [ ] `format_was_online` для разных временных промежутков
**4.2 Performance Benchmarks** (3 теста)
- [ ] Benchmark рендеринга 100 сообщений
- [ ] Benchmark рендеринга списка 50 чатов
- [ ] Benchmark форматирования markdown текста
### Итого
**Завершено**: 148 тестов (98%)
**Опционально**: 12 тестов (2%)
**Всего**: 160 тестов потенциально
---
## 💡 Технические заметки
### Текущие ограничения
1. **TestAppBuilder создаёт реальный TdClient** — подходит только для UI/snapshot тестов
2. **Для интеграционных тестов** понадобится рефакторинг: либо trait для TdClient, либо dependency injection
### Решения
- Snapshot тесты используют TestAppBuilder (UI рендеринг без вызова TdClient методов)
- Интеграционные тесты будут использовать FakeTdClient напрямую
- Возможно потребуется создать `IntegrationTestSession` для комплексных сценариев
---
## ✨ Качество кода
**Все helpers покрыты тестами**:
- `app_builder.rs`: 5 тестов
- `fake_tdclient.rs`: 4 теста
- `snapshot_utils.rs`: 2 теста
**Документация**:
- Все публичные функции имеют doc-комментарии
- Примеры использования в комментариях
- README-секция в TESTING_ROADMAP.md
---
## 🎓 Что изучили
1. **Snapshot testing** с insta — мощный инструмент для TUI
2. **ratatui::backend::TestBackend** — виртуальный терминал для тестов
3. **Fluent builder pattern** — удобно для построения тестовых данных
4. **Test helpers organization** — разделение на модули для переиспользования
---
## 📝 Обновлённые файлы
- `Cargo.toml` — добавлены dev-dependencies
- `.gitignore` — добавлены правила для snapshots
- `TESTING_ROADMAP.md` — обновлён прогресс
- `README.md` — добавлена ссылка на TESTING_ROADMAP
- `REFACTORING_ROADMAP.md` — добавлено предусловие о тестах
---
**Статус**: Готов к продолжению! 🚀
**Следующий шаг**: Запустить тесты и убедиться что всё компилируется, затем продолжить с Фазы 1.2

View File

@@ -1,620 +0,0 @@
# Testing Roadmap
План покрытия tele-tui тестами с фокусом на интеграционные и e2e тесты.
## Стратегия тестирования
### Подход: Комбо (Snapshot + Integration + E2E)
1. **Snapshot Testing (70%)** — проверка UI рендеринга через insta
2. **Integration Testing (25%)** — проверка логики и flow через FakeTdClient
3. **E2E Smoke Testing (5%)** — базовая проверка что приложение запускается
### Почему не юнит-тесты?
- TUI сложно тестировать через юниты (моки, хрупкость)
- Интеграционные тесты дают больше уверенности
- Snapshots ловят UI регрессии лучше, чем assert координат
---
## Фаза 0: Инфраструктура
### Зависимости
- [x] Добавить `insta = "1.34"` в dev-dependencies
- [x] Добавить `tokio-test = "0.4"` в dev-dependencies
- [x] Настроить `.gitignore` для snapshots (добавить `tests/snapshots/*.new`)
### Helpers и Test Utilities
- [x] Создать `tests/helpers/mod.rs`
- [x] Создать `tests/helpers/app_builder.rs` — builder для тестового App
- [x] Создать `tests/helpers/fake_tdclient.rs` — mock TDLib клиент
- [x] Создать `tests/helpers/snapshot_utils.rs` — утилиты для snapshot тестов
- [x] Создать `tests/helpers/test_data.rs` — фикстуры данных (чаты, сообщения)
```rust
// tests/helpers/mod.rs
pub mod app_builder;
pub mod fake_tdclient;
pub mod snapshot_utils;
pub mod test_data;
pub use app_builder::TestAppBuilder;
pub use fake_tdclient::FakeTdClient;
pub use snapshot_utils::{render_to_string, assert_ui_snapshot};
pub use test_data::{create_test_chat, create_test_message};
```
**Файлы для создания**:
```
tests/
├── helpers/
│ ├── mod.rs
│ ├── app_builder.rs
│ ├── fake_tdclient.rs
│ ├── snapshot_utils.rs
│ └── test_data.rs
└── snapshots/ # Создаётся insta автоматически
```
---
## Фаза 1: Snapshot Tests для UI (Приоритет: ВЫСОКИЙ)
### 1.1 Chat List — Список чатов
**Файл**: `tests/ui/chat_list_test.rs`
- [x] Пустой список чатов
- [x] Список с 3 чатами (без индикаторов)
- [x] Чат с непрочитанными сообщениями `(5)`
- [x] Чат с иконкой закреплённого 📌
- [x] Чат с иконкой mute 🔇
- [x] Чат с индикатором mention @
- [ ] Чат с онлайн-статусом ●
- [x] Выбранный чат (с ▌)
- [x] Список чатов в режиме поиска
- [x] Длинное название чата (обрезка)
**Пример теста**:
```rust
#[test]
fn snapshot_chat_list_with_unread() {
let app = TestAppBuilder::new()
.with_chat(create_test_chat("Mom", 123, unread: 5))
.with_chat(create_test_chat("Boss", 456, unread: 0))
.build();
assert_ui_snapshot!("chat_list_with_unread", app, |f, app| {
render_chat_list(f, f.size(), app);
});
}
```
---
### 1.2 Messages — Область сообщений
**Файл**: `tests/messages.rs`
- [x] Пустой чат (нет сообщений)
- [x] Одно входящее сообщение
- [x] Одно исходящее сообщение
- [x] Группировка по дате (разделитель "Сегодня")
- [x] Группировка по дате (разделитель "Вчера")
- [x] Группировка по отправителю (заголовок с именем)
- [x] Исходящее сообщение с ✓ (отправлено)
- [x] Исходящее сообщение с ✓✓ (прочитано)
- [x] Сообщение с индикатором редактирования ✎
- [x] Длинное сообщение (wrap на несколько строк)
- [x] Markdown: жирный, курсив, код
- [x] Markdown: ссылка, упоминание
- [x] Markdown: спойлер
- [x] Сообщение с медиа-заглушкой [Фото]
- [x] Reply сообщение с превью
- [x] Пересланное сообщение (↪ Переслано от)
- [x] Сообщение с одной реакцией [👍]
- [x] Сообщение с несколькими реакциями [👍] 5 👎 3
- [x] Выбранное сообщение (подсветка)
---
### 1.3 Modals — Модальные окна
**Файл**: `tests/modals.rs`
- [x] Delete confirmation модалка
- [x] Emoji picker (8x6 сетка)
- [x] Emoji picker с выбранной реакцией (курсор)
- [x] Profile модалка (личный чат)
- [x] Profile модалка (группа)
- [x] Pinned message вверху чата
- [x] Search в чате (с результатами)
- [x] Forward mode (список чатов для пересылки)
---
### 1.4 Input Field — Поле ввода
**Файл**: `tests/input_field.rs`
- [x] Пустое поле ввода
- [x] Поле ввода с текстом и курсором █
- [x] Поле ввода с длинным текстом (2 строки)
- [x] Поле ввода с длинным текстом (10 строк, максимум)
- [x] Режим редактирования (с превью)
- [x] Режим reply (с превью сообщения)
- [x] Режим поиска (с query)
---
### 1.5 Footer — Нижняя панель ✅
**Файл**: `tests/footer.rs`
- [x] Footer в списке чатов (команды навигации)
- [x] Footer в открытом чате (команды сообщений)
- [x] Footer с индикатором "⚠ Нет сети"
- [x] Footer с индикатором "⏳ Подключение к прокси..."
- [x] Footer с индикатором "⏳ Подключение..."
- [x] Footer в режиме поиска
---
### 1.6 Screens — Полные экраны ✅
**Файл**: `tests/screens.rs`
- [x] Loading screen (default)
- [x] Loading screen (со статусом)
- [x] Auth screen (ввод телефона)
- [x] Auth screen (ввод кода)
- [x] Auth screen (ввод пароля 2FA)
- [x] Main screen (пустой список чатов)
- [x] Минимальный размер терминала (предупреждение)
---
## Фаза 2: Integration Tests для логики (Приоритет: ВЫСОКИЙ)
### 2.1 Send Message Flow ✅
**Файл**: `tests/send_message.rs` (6 тестов)
- [x] Отправка текстового сообщения
- [x] Отправка нескольких сообщений
- [x] Отправка с markdown форматированием
- [x] Отправка в разные чаты
- [x] Получение входящего сообщения
- [x] Отправка с reply
---
### 2.2 Edit Message Flow ✅
**Файл**: `tests/edit_message.rs` (6 тестов)
- [x] Редактирование текста сообщения
- [x] Установка edit_date после редактирования
- [x] Проверка can_be_edited перед редактированием
- [x] Редактирование только своих сообщений
- [x] Множественные редактирования
- [x] Редактирование с форматированием
---
### 2.3 Delete Message Flow ✅
**Файл**: `tests/delete_message.rs` (6 тестов)
- [x] Удаление сообщения из списка
- [x] Множественные удаления
- [x] Проверка can_be_deleted
- [x] Удаление только своих сообщений
- [x] Удаление из разных чатов
- [x] Delete with revoke
---
### 2.4 Reply & Forward Flow ✅
**Файл**: `tests/reply_forward.rs` (8 тестов)
- [x] Reply на сообщение с превью
- [x] Reply сохраняет связь с оригиналом
- [x] Forward сообщения
- [x] Forward с sender_name
- [x] Forward в разные чаты
- [x] Reply + Forward комбо
- [x] Reply на forwarded сообщение
- [x] Forward reply сообщения
---
### 2.5 Reactions Flow ✅
**Файл**: `tests/reactions.rs` (10 тестов)
- [x] Добавление реакции на сообщение
- [x] Удаление реакции (toggle)
- [x] Множественные реакции на одно сообщение
- [x] Реакции от разных пользователей
- [x] Подсчёт реакций
- [x] Chosen реакция (своя)
- [x] Реакции обновляются в реальном времени
- [x] Получение доступных реакций чата
- [x] Реакции на forwarded сообщения
- [x] Очистка всех реакций
---
### 2.6 Search Flow ✅
**Файл**: `tests/search.rs` (8 тестов)
- [x] Поиск по названию чата
- [x] Поиск по @username
- [x] Поиск по сообщениям в чате
- [x] Навигация по результатам поиска
- [x] Case-insensitive поиск
- [x] Поиск с пробелами
- [x] Поиск возвращает пустой список если нет совпадений
- [x] Очистка поиска
---
### 2.7 Drafts Flow ✅
**Файл**: `tests/drafts.rs` (7 тестов)
- [x] Сохранение черновика при переключении чатов
- [x] Восстановление черновика при возврате
- [x] Удаление черновика после отправки
- [x] Черновики для разных чатов независимы
- [x] Индикатор черновика в списке чатов
- [x] Пустой черновик не сохраняется
- [x] Черновик сохраняется при закрытии чата
---
### 2.8 Navigation Flow ✅
**Файл**: `tests/navigation.rs` (7 тестов)
- [x] Навигация по списку чатов (↑/↓)
- [x] Открытие чата (Enter)
- [x] Закрытие чата (Esc)
- [x] Скролл сообщений (↑/↓)
- [x] Переключение между папками (1-9)
- [x] Навигация с wrap (переход с конца на начало)
- [x] Навигация в пустом списке
---
### 2.9 Profile Flow ✅
**Файл**: `tests/profile.rs` (6 тестов)
- [x] Открытие профиля личного чата
- [x] Профиль показывает имя и username
- [x] Профиль показывает телефон
- [x] Открытие профиля группы
- [x] Профиль группы показывает участников
- [x] Закрытие профиля (Esc)
---
### 2.10 Network & Typing Flow ✅
**Файл**: `tests/network_typing.rs` (9 тестов)
- [x] Typing indicator при наборе текста
- [x] Отправка typing action
- [x] Получение typing статуса
- [x] Typing timeout
- [x] Network state: WaitingForNetwork
- [x] Network state: ConnectingToProxy
- [x] Network state: Connecting
- [x] Network state: Updating
- [x] Network state: Ready
---
### 2.11 Copy Flow ✅
**Файл**: `tests/copy.rs` (9 тестов - ПРЕВЗОШЛИ ПЛАН!)
- [x] Форматирование простого сообщения
- [x] Форматирование с forward контекстом
- [x] Форматирование с reply контекстом
- [x] Форматирование с forward + reply одновременно
- [x] Форматирование длинного сообщения
- [x] Форматирование с markdown entities
- [x] Clipboard initialization
- [x] Копирование в реальный clipboard (ручное)
- [x] Кроссплатформенность clipboard
---
### 2.12 Config Flow ✅
**Файл**: `tests/config.rs` (11 тестов - ПРЕВЗОШЛИ ПЛАН!)
- [x] Дефолтные значения конфигурации
- [x] Кастомные значения конфигурации
- [x] Парсинг валидных цветов
- [x] Парсинг light цветов
- [x] Парсинг невалидного цвета с fallback
- [x] Case-insensitive парсинг цветов
- [x] TOML сериализация и десериализация
- [x] Частичный TOML использует дефолты
- [x] Различные форматы timezone
- [x] Загрузка credentials из переменных окружения
- [x] Проверка формата ошибки когда credentials не найдены
---
## Фаза 3: E2E Integration Tests (Приоритет: СРЕДНИЙ) ✅
### 3.1 Smoke Tests ✅
**Файл**: `tests/e2e_smoke.rs` (4 теста)
- [x] Приложение запускается без краша
- [x] Проверка минимального размера терминала
- [x] Базовые константы приложения
- [x] Graceful shutdown флаг
### 3.2 User Journey Tests ✅
**Файл**: `tests/e2e_user_journey.rs` (8 тестов)
- [x] App Launch → Auth → Chat List
- [x] Open Chat → Load History → Send Message
- [x] Receive Incoming Message While Chat Open
- [x] Multi-step conversation flow
- [x] Switch between chats
- [x] Edit message in conversation flow
- [x] Reply to message in conversation
- [x] Network state changes during conversation
**Итого**: 12/12 E2E тестов (100%) ✅
**Примечание**: Все тесты используют FakeTdClient для полной симуляции TDLib без реального подключения.
---
## Фаза 4: Дополнительные тесты (Приоритет: НИЗКИЙ)
### 4.1 Utils Tests
**Файл**: `tests/unit/utils_test.rs`
- [ ] `format_timestamp_with_tz` с разными timezone
- [ ] `parse_timezone_offset` валидные значения
- [ ] `parse_timezone_offset` инвалидные значения (fallback)
- [ ] `format_date` для сегодня, вчера, старых дат
- [ ] `format_was_online` для разных временных промежутков
### 4.2 Performance Tests
**Файл**: `tests/performance/render_bench.rs`
- [ ] Benchmark рендеринга 100 сообщений
- [ ] Benchmark рендеринга списка 50 чатов
- [ ] Benchmark форматирования markdown текста
---
## Метрики прогресса
### Фаза 0: Инфраструктура
- [x] 8/8 задач выполнено ✅
### Фаза 1: Snapshot Tests ✅
- [x] 1.1 Chat List: 10/10 (100%) ✅
- [x] 1.2 Messages: 19/19 (100%) ✅
- [x] 1.3 Modals: 8/8 (100%) ✅
- [x] 1.4 Input Field: 7/7 (100%) ✅
- [x] 1.5 Footer: 6/6 (100%) ✅
- [x] 1.6 Screens: 7/7 (100%) ✅
- **Итого: 57/57 snapshot тестов (100%)** ✅
### Фаза 2: Integration Tests ✅
- [x] 2.1 Send Message: 6/6 ✅
- [x] 2.2 Edit Message: 6/6 ✅
- [x] 2.3 Delete Message: 6/6 ✅
- [x] 2.4 Reply & Forward: 8/8 ✅
- [x] 2.5 Reactions: 10/10 ✅
- [x] 2.6 Search: 8/8 ✅
- [x] 2.7 Drafts: 7/7 ✅
- [x] 2.8 Navigation: 7/7 ✅
- [x] 2.9 Profile: 6/6 ✅
- [x] 2.10 Network & Typing: 9/9 ✅
- [x] 2.11 Copy: 9/9 ✅ (вместо 3!)
- [x] 2.12 Config: 11/11 ✅ (вместо 8!)
- **Итого: 93/93 интеграционных тестов (100%!) — ПРЕВЗОШЛИ ПЛАН!** 🎉
### Фаза 3: E2E Integration
- [x] 3.1 Smoke Tests: 4/4 ✅
- [x] 3.2 User Journey: 8/8 ✅
- **Итого: 12/12 E2E тестов (100%)** ✅
### Фаза 4: Дополнительно
- [ ] 4.1 Utils: 0/5
- [ ] 4.2 Performance: 0/3
- **Итого: 0/8 дополнительных тестов**
---
## Общий прогресс
**Всего**: 164/171 тестов (96%) — ПРЕВЗОШЛИ ПЛАН! 🎉🎉🎉
**Фаза 0 (Инфраструктура)**: ✅ Завершена (100%)
**Фаза 1 (UI Snapshot Tests)**: ✅ 57/57 (100%) — ЗАВЕРШЕНА! 🎉
- 1.1 Chat List: 10/10 (включая онлайн-статус) ✅
- 1.2 Messages: 19/19 ✅
- 1.3 Modals: 8/8 ✅
- 1.4 Input Field: 7/7 ✅
- 1.5 Footer: 6/6 ✅
- 1.6 Screens: 7/7 ✅
**Фаза 2 (Integration Tests)**: ✅ 93/93 (100%!) — ПРЕВЗОШЛИ ПЛАН!
- Завершено: 2.1-2.12 ✅
- Превзошли план на 9 тестов: Copy (9 вместо 3), Config (11 вместо 8)
**Фаза 3 (E2E Integration Tests)**: ✅ 12/12 (100%) — ЗАВЕРШЕНА! 🎉
- Smoke Tests: 4/4 ✅
- User Journey: 8/8 ✅
**Опционально**:
- Фаза 4 (Utils + Performance): 0/8
---
## Приоритизация
### Критичные (делать в первую очередь):
1. **Фаза 0**: Инфраструктура (без неё никуда)
2. **1.2**: Messages snapshots (ядро приложения)
3. **2.1**: Send message (основной flow)
4. **2.8**: Navigation (базовая навигация)
### Важные (делать после критичных):
5. **1.1**: Chat list snapshots
6. **2.2**: Edit message
7. **2.3**: Delete message
8. **2.5**: Reactions
9. **2.6**: Search
### Желательные (можно отложить):
10. **1.3-1.6**: Остальные snapshots
11. **2.4, 2.7, 2.9-2.12**: Остальные flows
12. **Фаза 3**: E2E smoke tests
### Опциональные (по желанию):
13. **Фаза 4**: Utils и performance
---
## Технологии
### Основные
- **insta** — snapshot testing
- **tokio-test** — async testing utilities
- **ratatui::backend::TestBackend** — виртуальный терминал
### Дополнительные (опционально)
- **expectrl** — для E2E тестов с реальным бинарником
- **criterion** — для бенчмарков (фаза 4.2)
- **mockall** — если понадобятся моки (скорее всего нет)
---
## Примеры структуры тестов
### Snapshot Test
```rust
use insta::assert_snapshot;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
#[test]
fn snapshot_messages_with_reactions() {
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
let app = TestAppBuilder::new()
.with_message(create_test_message("Hello!", reactions: vec![
reaction("👍", 1, chosen: true),
reaction("👎", 3, chosen: false),
]))
.build();
terminal.draw(|f| {
render_messages(f, f.size(), &app);
}).unwrap();
let buffer = terminal.backend().buffer();
assert_snapshot!(buffer_to_string(buffer));
}
```
### Integration Test
```rust
use crate::helpers::{TestAppBuilder, FakeTdClient};
#[tokio::test]
async fn test_send_message_updates_ui() {
let fake_client = FakeTdClient::new()
.with_chat("Mom", 123);
let mut app = TestAppBuilder::new()
.with_client(fake_client)
.with_selected_chat(123)
.build();
// Ввод текста
app.input_text = "Hello!".to_string();
// Отправка
app.handle_key(KeyCode::Enter).await;
// Проверки
assert_eq!(app.input_text, ""); // Инпут очистился
assert_eq!(app.current_messages().len(), 1);
assert_eq!(app.current_messages()[0].text, "Hello!");
assert_eq!(fake_client.sent_messages().len(), 1);
}
```
---
## Команды
```bash
# Прогнать все тесты
cargo test
# Прогнать только snapshot тесты
cargo test --test ui
# Прогнать только integration тесты
cargo test --test integration
# Обновить snapshots (после ревью изменений)
cargo insta review
# Принять все новые snapshots
cargo insta accept
# Показать diff для изменённых snapshots
cargo insta test --review
```
---
## Правила
1. **Один тест = один сценарий** — не делать мега-тесты
2. **Snapshots коммитим** — они часть тестов
3. **Фикстуры переиспользуем** — общие данные в `test_data.rs`
4. **Тесты изолированы** — каждый тест создаёт свой App
5. **Порядок не важен** — тесты можно запускать в любом порядке
---
## TODO перед началом
- [ ] Прочитать документацию insta: https://insta.rs/
- [ ] Решить: нужен ли trait для TdClient или достаточно FakeTdClient
- [ ] Обсудить: какие тесты делать в первую очередь
---
## Примечания
- Этот документ будет обновляться по мере написания тестов
- После завершения фазы — отмечать в метриках
- Если тест падает или не актуален — документировать причину
- Snapshots хранятся в `tests/snapshots/__snapshots__/`

View File

@@ -1,35 +0,0 @@
# Telegram TUI Configuration Example
# Copy this file to ~/.config/tele-tui/config.toml
[general]
# Timezone offset (e.g., "+03:00", "-05:00")
timezone = "+03:00"
[colors]
# Colors: red, green, blue, yellow, cyan, magenta, white, black, gray
# Also available: lightred, lightgreen, lightblue, lightyellow, lightcyan, lightmagenta
incoming_message = "white"
outgoing_message = "green"
selected_message = "yellow"
reaction_chosen = "yellow"
reaction_other = "gray"
[notifications]
# Enable desktop notifications for new messages
enabled = true
# Only notify when you are mentioned (@username)
only_mentions = false
# Show message preview text in notifications
show_preview = true
# Notification timeout in milliseconds (0 = system default)
timeout_ms = 5000
# Notification urgency level: "low", "normal", "critical"
# Note: Only works on Linux (libnotify), ignored on macOS/Windows
urgency = "normal"
# Note: Notifications respect Telegram's mute settings
# Muted chats won't trigger notifications

View File

@@ -87,6 +87,7 @@ impl<T: TdClientTrait> NavigationMethods<T> for App<T> {
#[cfg(feature = "images")]
{
self.photo_download_rx = None;
self.pending_image_open = None;
}
// Сбрасываем состояние чата в нормальный режим
self.chat_state = ChatState::Normal;

View File

@@ -20,6 +20,19 @@ use crate::types::ChatId;
use ratatui::widgets::ListState;
use std::path::PathBuf;
/// Pending intent to open the image modal once a photo finishes downloading.
///
/// Set when the user presses `v` on a photo that is still downloading.
/// The main loop opens the modal automatically when the download completes.
#[cfg(feature = "images")]
#[derive(Debug, Clone)]
pub struct PendingImageOpen {
pub file_id: i32,
pub message_id: crate::types::MessageId,
pub photo_width: i32,
pub photo_height: i32,
}
/// State of the account switcher modal overlay.
#[derive(Debug, Clone)]
pub enum AccountSwitcherState {
@@ -123,6 +136,9 @@ pub struct App<T: TdClientTrait = TdClient> {
/// Время последнего рендеринга изображений (для throttling до 15 FPS)
#[cfg(feature = "images")]
pub last_image_render_time: Option<std::time::Instant>,
/// Pending intent: открыть модалку для этого фото когда загрузится
#[cfg(feature = "images")]
pub pending_image_open: Option<PendingImageOpen>,
// Account lock
/// Advisory file lock to prevent concurrent access to the same account
pub account_lock: Option<std::fs::File>,
@@ -219,6 +235,8 @@ impl<T: TdClientTrait> App<T> {
image_modal: None,
#[cfg(feature = "images")]
last_image_render_time: None,
#[cfg(feature = "images")]
pending_image_open: None,
// Voice playback
audio_player: crate::audio::AudioPlayer::new().ok(),
voice_cache: crate::audio::VoiceCache::new(audio_cache_size_mb).ok(),

View File

@@ -6,7 +6,7 @@
//! - Editing and sending messages
//! - Loading older messages
use super::chat_list::open_chat_and_load_data;
use super::chat_loader::{load_older_messages_if_needed, open_chat_and_load_data};
use crate::app::methods::{
compose::ComposeMethods, messages::MessageMethods, modal::ModalMethods,
navigation::NavigationMethods,
@@ -16,7 +16,7 @@ use crate::app::InputMode;
use crate::input::handlers::{copy_to_clipboard, format_message_for_clipboard};
use crate::tdlib::{ChatAction, TdClientTrait};
use crate::types::{ChatId, MessageId};
use crate::utils::{is_non_empty, with_timeout, with_timeout_msg};
use crate::utils::{is_non_empty, with_timeout_msg};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::time::{Duration, Instant};
@@ -340,50 +340,6 @@ pub async fn send_reaction<T: TdClientTrait>(app: &mut App<T>) {
}
}
/// Подгружает старые сообщения если скролл близко к верху
pub async fn load_older_messages_if_needed<T: TdClientTrait>(app: &mut App<T>) {
// Check if there are messages to load from
if app.td_client.current_chat_messages().is_empty() {
return;
}
// Get the oldest message ID
let oldest_msg_id = app
.td_client
.current_chat_messages()
.first()
.map(|m| m.id())
.unwrap_or(MessageId::new(0));
// Get current chat ID
let Some(chat_id) = app.get_selected_chat_id() else {
return;
};
// Check if scroll is near the top
let message_count = app.td_client.current_chat_messages().len();
if app.message_scroll_offset <= message_count.saturating_sub(10) {
return;
}
// Load older messages with timeout
let Ok(older) = with_timeout(
Duration::from_secs(3),
app.td_client
.load_older_messages(ChatId::new(chat_id), oldest_msg_id),
)
.await
else {
return;
};
// Add older messages to the beginning if any were loaded
if !older.is_empty() {
let msgs = app.td_client.current_chat_messages_mut();
msgs.splice(0..0, older);
}
}
/// Обработка ввода клавиатуры в открытом чате
///
/// Обрабатывает:
@@ -628,45 +584,44 @@ async fn handle_view_image<T: TdClientTrait>(app: &mut App<T>) {
});
app.needs_redraw = true;
}
PhotoDownloadState::Downloading => {
app.status_message = Some("Загрузка фото...".to_string());
}
PhotoDownloadState::NotDownloaded => {
// Скачиваем фото и открываем
PhotoDownloadState::NotDownloaded | PhotoDownloadState::Downloading => {
// Запоминаем намерение открыть модалку — откроется когда загрузится
app.pending_image_open = Some(crate::app::PendingImageOpen {
file_id,
message_id: msg_id,
photo_width,
photo_height,
});
app.status_message = Some("Загрузка фото...".to_string());
app.needs_redraw = true;
match app.td_client.download_file(file_id).await {
Ok(path) => {
// Обновляем состояние загрузки в сообщении
for msg in app.td_client.current_chat_messages_mut() {
if let Some(photo) = msg.photo_info_mut() {
if photo.file_id == file_id {
photo.download_state = PhotoDownloadState::Downloaded(path.clone());
break;
// Если нет активной фоновой загрузки — запускаем свою
if app.photo_download_rx.is_none() {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
app.photo_download_rx = Some(rx);
let client_id = app.td_client.client_id();
tokio::spawn(async move {
let result = tokio::time::timeout(Duration::from_secs(30), async {
match tdlib_rs::functions::download_file(
file_id, 1, 0, 0, true, client_id,
)
.await
{
Ok(tdlib_rs::enums::File::File(f))
if f.local.is_downloading_completed
&& !f.local.path.is_empty() =>
{
Ok(f.local.path)
}
Ok(_) => Err("Файл не скачан".to_string()),
Err(e) => Err(format!("{:?}", e)),
}
}
// Открываем модалку
app.image_modal = Some(ImageModalState {
message_id: msg_id,
photo_path: path,
photo_width,
photo_height,
});
app.status_message = None;
}
Err(e) => {
for msg in app.td_client.current_chat_messages_mut() {
if let Some(photo) = msg.photo_info_mut() {
if photo.file_id == file_id {
photo.download_state = PhotoDownloadState::Error(e.clone());
break;
}
}
}
app.error_message = Some(format!("Ошибка загрузки фото: {}", e));
app.status_message = None;
}
})
.await;
let result =
result.unwrap_or_else(|_| Err("Таймаут загрузки".to_string()));
let _ = tx.send((file_id, result));
});
}
}
PhotoDownloadState::Error(_) => {

View File

@@ -5,14 +5,10 @@
//! - Folder selection
//! - Opening chats
use crate::app::methods::{
compose::ComposeMethods, messages::MessageMethods, navigation::NavigationMethods,
};
use crate::app::methods::navigation::NavigationMethods;
use crate::app::App;
use crate::app::InputMode;
use crate::tdlib::TdClientTrait;
use crate::types::{ChatId, MessageId};
use crate::utils::{with_timeout, with_timeout_msg};
use crate::utils::with_timeout;
use crossterm::event::KeyEvent;
use std::time::Duration;
@@ -79,62 +75,3 @@ pub async fn select_folder<T: TdClientTrait>(app: &mut App<T>, folder_idx: usize
}
}
/// Открывает чат и загружает последние сообщения (быстро).
///
/// Загружает только 50 последних сообщений для мгновенного отображения.
/// Фоновые задачи (reply info, pinned, photos) откладываются в `pending_chat_init`
/// и выполняются на следующем тике main loop.
///
/// При ошибке устанавливает error_message и очищает status_message.
pub async fn open_chat_and_load_data<T: TdClientTrait>(app: &mut App<T>, chat_id: i64) {
app.status_message = Some("Загрузка сообщений...".to_string());
app.message_scroll_offset = 0;
// Загружаем только 50 последних сообщений (один запрос к TDLib)
match with_timeout_msg(
Duration::from_secs(10),
app.td_client.get_chat_history(ChatId::new(chat_id), 50),
"Таймаут загрузки сообщений",
)
.await
{
Ok(messages) => {
// Собираем ID всех входящих сообщений для отметки как прочитанные
let incoming_message_ids: Vec<MessageId> = messages
.iter()
.filter(|msg| !msg.is_outgoing())
.map(|msg| msg.id())
.collect();
// Сохраняем загруженные сообщения
app.td_client.set_current_chat_messages(messages);
// Добавляем входящие сообщения в очередь для отметки как прочитанные
if !incoming_message_ids.is_empty() {
app.td_client
.pending_view_messages_mut()
.push((ChatId::new(chat_id), incoming_message_ids));
}
// ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории
// Это предотвращает race condition с Update::NewMessage
app.td_client
.set_current_chat_id(Some(ChatId::new(chat_id)));
// Загружаем черновик (локальная операция, мгновенно)
app.load_draft();
// Показываем чат СРАЗУ
app.status_message = None;
app.input_mode = InputMode::Normal;
app.start_message_selection();
// Фоновые задачи (reply info, pinned, photos) — на следующем тике main loop
app.pending_chat_init = Some(ChatId::new(chat_id));
}
Err(e) => {
app.error_message = Some(e);
app.status_message = None;
}
}
}

View File

@@ -0,0 +1,194 @@
//! Chat loading logic — all three phases of message loading
//!
//! - Phase 1: `open_chat_and_load_data` — fast initial load (50 messages)
//! - Phase 2: `process_pending_chat_init` — background tasks (reply info, pinned, photos)
//! - Phase 3: `load_older_messages_if_needed` — lazy load on scroll up
use crate::app::methods::{compose::ComposeMethods, messages::MessageMethods};
use crate::app::App;
use crate::app::InputMode;
use crate::tdlib::TdClientTrait;
use crate::types::{ChatId, MessageId};
use crate::utils::{with_timeout, with_timeout_ignore, with_timeout_msg};
use std::time::Duration;
/// Открывает чат и загружает последние сообщения (быстро).
///
/// Загружает только 50 последних сообщений для мгновенного отображения.
/// Фоновые задачи (reply info, pinned, photos) откладываются в `pending_chat_init`
/// и выполняются на следующем тике main loop.
///
/// При ошибке устанавливает error_message и очищает status_message.
pub async fn open_chat_and_load_data<T: TdClientTrait>(app: &mut App<T>, chat_id: i64) {
app.status_message = Some("Загрузка сообщений...".to_string());
app.message_scroll_offset = 0;
// Загружаем только 50 последних сообщений (один запрос к TDLib)
match with_timeout_msg(
Duration::from_secs(10),
app.td_client.get_chat_history(ChatId::new(chat_id), 50),
"Таймаут загрузки сообщений",
)
.await
{
Ok(messages) => {
// Собираем ID всех входящих сообщений для отметки как прочитанные
let incoming_message_ids: Vec<MessageId> = messages
.iter()
.filter(|msg| !msg.is_outgoing())
.map(|msg| msg.id())
.collect();
// Сохраняем загруженные сообщения
app.td_client.set_current_chat_messages(messages);
// Добавляем входящие сообщения в очередь для отметки как прочитанные
if !incoming_message_ids.is_empty() {
app.td_client
.pending_view_messages_mut()
.push((ChatId::new(chat_id), incoming_message_ids));
}
// ВАЖНО: Устанавливаем current_chat_id ТОЛЬКО ПОСЛЕ сохранения истории
// Это предотвращает race condition с Update::NewMessage
app.td_client
.set_current_chat_id(Some(ChatId::new(chat_id)));
// Загружаем черновик (локальная операция, мгновенно)
app.load_draft();
// Показываем чат СРАЗУ
app.status_message = None;
app.input_mode = InputMode::Normal;
app.start_message_selection();
// Фоновые задачи (reply info, pinned, photos) — на следующем тике main loop
app.pending_chat_init = Some(ChatId::new(chat_id));
}
Err(e) => {
app.error_message = Some(e);
app.status_message = None;
}
}
}
/// Выполняет фоновую инициализацию после открытия чата.
///
/// Вызывается на следующем тике main loop после `open_chat_and_load_data`.
/// Загружает reply info, закреплённое сообщение и начинает авто-загрузку фото.
pub async fn process_pending_chat_init<T: TdClientTrait>(app: &mut App<T>, chat_id: ChatId) {
// Загружаем недостающие reply info (игнорируем ошибки)
with_timeout_ignore(Duration::from_secs(5), app.td_client.fetch_missing_reply_info())
.await;
// Загружаем последнее закреплённое сообщение (игнорируем ошибки)
with_timeout_ignore(
Duration::from_secs(2),
app.td_client.load_current_pinned_message(chat_id),
)
.await;
// Авто-загрузка фото — неблокирующая фоновая задача (до 5 фото параллельно)
#[cfg(feature = "images")]
{
use crate::tdlib::PhotoDownloadState;
if app.config().images.auto_download_images && app.config().images.show_images {
let photo_file_ids: Vec<i32> = app
.td_client
.current_chat_messages()
.iter()
.rev()
.take(5)
.filter_map(|msg| {
msg.photo_info().and_then(|p| {
matches!(p.download_state, PhotoDownloadState::NotDownloaded)
.then_some(p.file_id)
})
})
.collect();
if !photo_file_ids.is_empty() {
let client_id = app.td_client.client_id();
let (tx, rx) =
tokio::sync::mpsc::unbounded_channel::<(i32, Result<String, String>)>();
app.photo_download_rx = Some(rx);
for file_id in photo_file_ids {
let tx = tx.clone();
tokio::spawn(async move {
let result = tokio::time::timeout(Duration::from_secs(5), async {
match tdlib_rs::functions::download_file(
file_id, 1, 0, 0, true, client_id,
)
.await
{
Ok(tdlib_rs::enums::File::File(file))
if file.local.is_downloading_completed
&& !file.local.path.is_empty() =>
{
Ok(file.local.path)
}
Ok(_) => Err("Файл не скачан".to_string()),
Err(e) => Err(format!("{:?}", e)),
}
})
.await;
let result = match result {
Ok(r) => r,
Err(_) => Err("Таймаут загрузки".to_string()),
};
let _ = tx.send((file_id, result));
});
}
}
}
}
app.needs_redraw = true;
}
/// Подгружает старые сообщения если скролл близко к верху
pub async fn load_older_messages_if_needed<T: TdClientTrait>(app: &mut App<T>) {
// Check if there are messages to load from
if app.td_client.current_chat_messages().is_empty() {
return;
}
// Get the oldest message ID
let oldest_msg_id = app
.td_client
.current_chat_messages()
.first()
.map(|m| m.id())
.unwrap_or(MessageId::new(0));
// Get current chat ID
let Some(chat_id) = app.get_selected_chat_id() else {
return;
};
// Check if scroll is near the top
let message_count = app.td_client.current_chat_messages().len();
if app.message_scroll_offset <= message_count.saturating_sub(10) {
return;
}
// Load older messages with timeout
let Ok(older) = with_timeout(
Duration::from_secs(3),
app.td_client
.load_older_messages(ChatId::new(chat_id), oldest_msg_id),
)
.await
else {
return;
};
// Add older messages to the beginning if any were loaded
if !older.is_empty() {
let msgs = app.td_client.current_chat_messages_mut();
msgs.splice(0..0, older);
}
}

View File

@@ -6,12 +6,14 @@
//! - profile: Profile helper functions
//! - chat: Keyboard input handling for open chat view
//! - chat_list: Navigation and interaction in the chat list
//! - chat_loader: All phases of chat message loading
//! - compose: Text input, editing, and message composition
//! - modal: Modal dialogs (delete confirmation, emoji picker, etc.)
//! - search: Search functionality (chat search, message search)
pub mod chat;
pub mod chat_list;
pub mod chat_loader;
pub mod clipboard;
pub mod compose;
pub mod global;
@@ -19,6 +21,7 @@ pub mod modal;
pub mod profile;
pub mod search;
pub use chat_loader::{load_older_messages_if_needed, open_chat_and_load_data, process_pending_chat_init};
pub use clipboard::*;
pub use global::*;
pub use profile::get_available_actions_count;

View File

@@ -13,7 +13,7 @@ use crate::utils::with_timeout;
use crossterm::event::{KeyCode, KeyEvent};
use std::time::Duration;
use super::chat_list::open_chat_and_load_data;
use super::chat_loader::open_chat_and_load_data;
use super::scroll_to_message;
/// Обработка режима поиска по чатам

File diff suppressed because it is too large Load Diff

View File

@@ -29,6 +29,7 @@ use tdlib_rs::enums::Update;
use app::{App, AppScreen};
use constants::{POLL_TIMEOUT_MS, SHUTDOWN_TIMEOUT_SECS};
use input::{handle_auth_input, handle_main_input};
use input::handlers::process_pending_chat_init;
use tdlib::AuthState;
use utils::{disable_tdlib_logs, with_timeout_ignore};
@@ -215,6 +216,47 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
}
}
}
// Если это фото ждёт открытия в модалке — открываем
let pending_matches = app
.pending_image_open
.as_ref()
.map(|p| p.file_id == file_id)
.unwrap_or(false);
if pending_matches {
// Ищем путь из обновлённого состояния
let downloaded_path = app
.td_client
.current_chat_messages()
.iter()
.find_map(|m| {
m.photo_info().and_then(|p| {
if p.file_id == file_id {
if let PhotoDownloadState::Downloaded(ref path) =
p.download_state
{
Some(path.clone())
} else {
None
}
} else {
None
}
})
});
if let (Some(path), Some(pending)) =
(downloaded_path, app.pending_image_open.take())
{
use crate::tdlib::ImageModalState;
app.image_modal = Some(ImageModalState {
message_id: pending.message_id,
photo_path: path,
photo_width: pending.photo_width,
photo_height: pending.photo_height,
});
app.status_message = None;
got_photos = true;
}
}
}
}
if got_photos {
@@ -345,76 +387,7 @@ async fn run_app<B: ratatui::backend::Backend, T: tdlib::TdClientTrait>(
// Process pending chat initialization (reply info, pinned, photos)
if let Some(chat_id) = app.pending_chat_init.take() {
// Загружаем недостающие reply info (игнорируем ошибки)
with_timeout_ignore(Duration::from_secs(5), app.td_client.fetch_missing_reply_info())
.await;
// Загружаем последнее закреплённое сообщение (игнорируем ошибки)
with_timeout_ignore(
Duration::from_secs(2),
app.td_client.load_current_pinned_message(chat_id),
)
.await;
// Авто-загрузка фото — неблокирующая фоновая задача (до 5 фото параллельно)
#[cfg(feature = "images")]
{
use crate::tdlib::PhotoDownloadState;
if app.config().images.auto_download_images && app.config().images.show_images {
let photo_file_ids: Vec<i32> = app
.td_client
.current_chat_messages()
.iter()
.rev()
.take(5)
.filter_map(|msg| {
msg.photo_info().and_then(|p| {
matches!(p.download_state, PhotoDownloadState::NotDownloaded)
.then_some(p.file_id)
})
})
.collect();
if !photo_file_ids.is_empty() {
let client_id = app.td_client.client_id();
let (tx, rx) =
tokio::sync::mpsc::unbounded_channel::<(i32, Result<String, String>)>();
app.photo_download_rx = Some(rx);
for file_id in photo_file_ids {
let tx = tx.clone();
tokio::spawn(async move {
let result = tokio::time::timeout(Duration::from_secs(5), async {
match tdlib_rs::functions::download_file(
file_id, 1, 0, 0, true, client_id,
)
.await
{
Ok(tdlib_rs::enums::File::File(file))
if file.local.is_downloading_completed
&& !file.local.path.is_empty() =>
{
Ok(file.local.path)
}
Ok(_) => Err("Файл не скачан".to_string()),
Err(e) => Err(format!("{:?}", e)),
}
})
.await;
let result = match result {
Ok(r) => r,
Err(_) => Err("Таймаут загрузки".to_string()),
};
let _ = tx.send((file_id, result));
});
}
}
}
}
app.needs_redraw = true;
process_pending_chat_init(app, chat_id).await;
}
// Check pending account switch

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &mut App<T>) {
let search_style = if app.is_searching {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::DarkGray)
Style::default().fg(Color::Rgb(160, 160, 160))
};
let search = Paragraph::new(search_text)
.block(Block::default().borders(Borders::ALL))

View File

@@ -221,9 +221,9 @@ pub fn render_message_bubble(
let mut lines = Vec::new();
let is_selected = selected_msg_id == Some(msg.id());
// Маркер выбора
let selection_marker = if is_selected { "" } else { "" };
let marker_len = selection_marker.chars().count();
// Маркер выбора (всегда резервируем место для ▶, чтобы текст не сдвигался)
let selection_marker = if is_selected { "" } else { " " };
let marker_len = 2;
// Цвет сообщения
let msg_color = if is_selected {
@@ -306,16 +306,16 @@ pub fn render_message_bubble(
let full_len = line_len + time_mark_len + marker_len;
let padding = content_width.saturating_sub(full_len + 1);
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
if is_selected && i == 0 {
// Одна строка — маркер на ней
if i == 0 {
// Первая (или единственная) строка — маркер
line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
} else if is_selected {
// Последняя строка multi-line — пробелы вместо маркера
} else {
// Остальные строки multi-line — пробелы вместо маркера
line_spans.push(Span::raw(" ".repeat(marker_len)));
}
line_spans.extend(formatted_spans);
@@ -327,14 +327,14 @@ pub fn render_message_bubble(
} else {
let padding = content_width.saturating_sub(line_len + marker_len + 1);
let mut line_spans = vec![Span::raw(" ".repeat(padding))];
if i == 0 && is_selected {
if i == 0 {
line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
} else if is_selected {
} else {
// Средние строки multi-line — пробелы вместо маркера
line_spans.push(Span::raw(" ".repeat(marker_len)));
}
@@ -364,14 +364,12 @@ pub fn render_message_bubble(
if i == 0 {
let mut line_spans = vec![];
if is_selected {
line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
}
line_spans.push(Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
));
line_spans
.push(Span::styled(format!(" {}", time_str), Style::default().fg(Color::Gray)));
line_spans.push(Span::raw(" "));
@@ -548,8 +546,8 @@ pub fn render_album_bubble(
let is_selected = messages.iter().any(|m| selected_msg_id == Some(m.id()));
let is_outgoing = messages.first().is_some_and(|m| m.is_outgoing());
// Selection marker
let selection_marker = if is_selected { "" } else { "" };
// Selection marker (всегда резервируем место)
let selection_marker = if is_selected { "" } else { " " };
// Фильтруем фото
let photos: Vec<&MessageInfo> = messages.iter().filter(|m| m.has_photo()).collect();
@@ -567,15 +565,13 @@ pub fn render_album_bubble(
let cols = photo_count.min(ALBUM_GRID_MAX_COLS);
let rows = photo_count.div_ceil(cols);
// Добавляем маркер выбора на первую строку
if is_selected {
lines.push(Line::from(vec![Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)]));
}
// Добавляем маркер выбора на первую строку (всегда — для постоянного отступа)
lines.push(Line::from(vec![Span::styled(
selection_marker,
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)]));
let grid_start_line = lines.len();

View File

@@ -19,12 +19,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
NetworkState::Updating => "⏳ Обновление... | ",
};
// Account indicator (shown if not "default")
let account_indicator = if app.current_account_name != "default" {
format!("[{}] ", app.current_account_name)
} else {
String::new()
};
let account_indicator = format!("[{}] ", app.current_account_name);
let status = if let Some(msg) = &app.status_message {
format!(" {}{}{} ", account_indicator, network_indicator, msg)
@@ -57,7 +52,7 @@ pub fn render<T: TdClientTrait>(f: &mut Frame, area: Rect, app: &App<T>) {
} else if app.status_message.is_some() {
Style::default().fg(Color::Yellow)
} else {
Style::default().fg(Color::DarkGray)
Style::default().fg(Color::Rgb(160, 160, 160))
};
let footer = Paragraph::new(status).style(style);

View File

@@ -1,5 +1,6 @@
---
source: tests/footer.rs
assertion_line: 22
expression: output
---
Инициализация TDLib...
[default] Инициализация TDLib...

View File

@@ -1,5 +1,6 @@
---
source: tests/footer.rs
assertion_line: 90
expression: output
---
⏳ Подключение... | Инициализация TDLib...
[default] ⏳ Подключение... | Инициализация TDLib...

View File

@@ -1,5 +1,6 @@
---
source: tests/footer.rs
assertion_line: 73
expression: output
---
⏳ Прокси... | Инициализация TDLib...
[default] ⏳ Прокси... | Инициализация TDLib...

View File

@@ -1,5 +1,6 @@
---
source: tests/footer.rs
assertion_line: 56
expression: output
---
⚠ Нет сети | Инициализация TDLib...
[default] ⚠ Нет сети | Инициализация TDLib...

View File

@@ -1,5 +1,6 @@
---
source: tests/footer.rs
assertion_line: 39
expression: output
---
Инициализация TDLib...
[default] Инициализация TDLib...

View File

@@ -1,5 +1,6 @@
---
source: tests/footer.rs
assertion_line: 107
expression: output
---
Инициализация TDLib...
[default] Инициализация TDLib...

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│Mom ──────────────── │
│ (14:33) What do you think about this?
(14:33) What do you think about this? │
│ │
│ │
│ │

View File

@@ -9,9 +9,9 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│Alice ──────────────── │
│ (14:33) 📷 [Фото]
│ (14:33) Caption for album
│ (14:33) 📷 [Фото]
(14:33) 📷 [Фото] │
(14:33) Caption for album │
(14:33) 📷 [Фото] │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│Alice ──────────────── │
│ (14:33) 📷 [Фото]
(14:33) 📷 [Фото] │
│▶ (14:33) 📷 [Фото] │
│ │
│ │

View File

@@ -9,10 +9,10 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│Alice ──────────────── │
│ (14:33) Regular message before
│ (14:33) 📷 [Фото]
│ (14:33) Album caption
│ (14:33) Regular message after
(14:33) Regular message before │
(14:33) 📷 [Фото] │
(14:33) Album caption │
(14:33) Regular message after │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Message from the past
(14:33) Message from the past │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33 ✎) Edited text
(14:33 ✎) Edited text │
│ │
│ │
│ │

View File

@@ -10,7 +10,7 @@ expression: output
│ │
│User ──────────────── │
│↪ Переслано от Alice │
│ (14:33) Forwarded content
(14:33) Forwarded content │
│ │
│ │
│ │

View File

@@ -9,9 +9,9 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) This is a very long message that should wrap across multiple lines
│ when rendered in the terminal UI. Let's make it even longer to
│ ensure we test the wrapping behavior properly.
(14:33) This is a very long message that should wrap across multiple lines │
when rendered in the terminal UI. Let's make it even longer to │
ensure we test the wrapping behavior properly. │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) **bold** *italic* `code`
(14:33) **bold** *italic* `code` │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Check [this](https://example.com) and @username
(14:33) Check [this](https://example.com) and @username │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Spoiler: ||hidden text||
(14:33) Spoiler: ||hidden text|| │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) [Фото]
(14:33) [Фото] │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Popular message
(14:33) Popular message │
│[👍 ] 5 👎 3 │
│ │
│ │

View File

@@ -10,7 +10,7 @@ expression: output
│ │
│User ──────────────── │
│┌ Mom: Original message text │
│ (14:33) This is a reply
(14:33) This is a reply │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Selected message
(14:33) Selected message │
│ │
│ │
│ │

View File

@@ -9,11 +9,11 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│Alice ──────────────── │
│ (14:33) First message
│ (14:33) Second message
(14:33) First message │
(14:33) Second message │
│ │
│Bob ──────────────── │
│ (14:33) Third message
(14:33) Third message │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│Mom ──────────────── │
│ (14:33) Hello there!
(14:33) Hello there! │
│ │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Great!
(14:33) Great! │
│[👍 ] │
│ │
│ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) React to this
(14:33) React to this │
│ │
│ ┌ Выбери реакцию ────────────────────────────────┐ │
│ │ │ │

View File

@@ -9,7 +9,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) React to this
(14:33) React to this │
│ │
│ ┌ Выбери реакцию ────────────────────────────────┐ │
│ │ │ │

View File

@@ -10,7 +10,7 @@ expression: output
│ ──────── 02.01.2022 ──────── │
│ │
│User ──────────────── │
│ (14:33) Regular message
(14:33) Regular message │
│ │
│ │
│ │