Files
telegram-tui/CONTEXT.md
Mikhail Kilin 78fe09bf11 feat: implement photo albums (media groups) and persist account selection
Group photos with shared media_album_id into single album bubbles with
grid layout (up to 3x cols). Album navigation treats grouped photos as
one unit (j/k skip entire album). Persist selected account to
accounts.toml so it survives app restart.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 16:18:04 +03:00

20 KiB
Raw Blame History

Текущий контекст проекта

Статус: Фаза 14 — Мультиаккаунт (IN PROGRESS)

Photo Albums (Media Groups) — DONE

Фото-альбомы (несколько фото в одном сообщении) теперь группируются в один пузырь с сеткой фото.

Проблема: TDLib отправляет альбомы как отдельные Message с общим media_album_id: i64. Ранее проект это поле игнорировал — каждое фото отображалось как отдельный пузырь.

Решение:

  1. Data Modelmedia_album_id: i64 в MessageMetadata, MessageBuilder, getter MessageInfo::media_album_id(). Оба конвертера (async + sync) передают поле из TDLib.

  2. Message Grouping — новый вариант MessageGroup::Album(Vec<MessageInfo>). Сообщения с одинаковым media_album_id != 0 группируются; одиночное сообщение с album_id остаётся Message.

  3. Album Grid ConstantsALBUM_PHOTO_WIDTH: 16, ALBUM_PHOTO_HEIGHT: 8, ALBUM_PHOTO_GAP: 1, ALBUM_GRID_MAX_COLS: 3 (3×16 + 2×1 = 50 = INLINE_IMAGE_MAX_WIDTH).

  4. render_album_bubble() — сетка фото (до 3 в ряд), DeferredImageRender с x_offset для каждого фото, общая подпись и timestamp, индикация выбора, статусы загрузки.

  5. IntegrationAlbum arm в render_message_list, x_offset в second pass. Без feature images — fallback через отдельные bubble.

Модифицированные файлы:

  • src/tdlib/types.rsmedia_album_id в MessageMetadata, MessageBuilder, getter
  • src/tdlib/messages/convert.rs — передача media_album_id в builder
  • src/tdlib/message_converter.rs — передача media_album_id в builder
  • src/message_grouping.rsAlbum variant + album detection + 4 новых теста
  • src/constants.rs — album grid constants
  • src/ui/components/message_bubble.rsx_offset в DeferredImageRender, render_album_bubble()
  • src/ui/components/mod.rs — export render_album_bubble
  • src/ui/messages.rsAlbum arm + x_offset в second pass
  1. Навигация j/k по альбомам — альбом обрабатывается как одно сообщение. select_previous_message() / select_next_message() перескакивают через все сообщения альбома. start_message_selection() встаёт на первый элемент альбома если последнее сообщение — часть альбома.

  2. Тесты — 4 unit-теста в message_grouping.rs, 5 snapshot-тестов в tests/messages.rs, 3 теста навигации в tests/input_navigation.rs.

Дополнительно модифицированные файлы:

  • src/app/methods/messages.rs — навигация перескакивает альбомы
  • tests/helpers/test_data.rsTestMessageBuilder::media_album_id()
  • tests/messages.rs — 5 snapshot-тестов для альбомов
  • tests/input_navigation.rs — 3 теста навигации по альбомам

Что НЕ меняется: image modal (v), auto-download, одиночные фото.


Оптимизация: Ленивая загрузка сообщений при открытии чата (DONE)

Чат открывается мгновенно (< 1 сек) вместо 5-30 сек для больших чатов.

Проблема: open_chat_and_load_data() блокировал UI до полной загрузки ВСЕХ сообщений (get_chat_history(chat_id, i32::MAX)). Для чата с 500+ сообщениями это 10+ запросов к TDLib.

Решение:

  • Загрузка только 50 последних сообщений (один запрос) → чат виден сразу
  • Фоновые задачи (reply info, pinned, photos) — на следующем тике main loop через pending_chat_init
  • Старые сообщения подгружаются при скролле вверх (существующий load_older_messages_if_needed)

Модифицированные файлы:

  • src/app/mod.rs — поле pending_chat_init: Option<ChatId>
  • src/input/handlers/chat_list.rsopen_chat_and_load_data(): 50 сообщений + pending_chat_init
  • src/main.rs — обработка pending_chat_init в main loop (reply info, pinned, photos)
  • src/app/methods/navigation.rsсброс pending_chat_init в close_chat()

Bugfix: Авто-загрузка фото в чате (DONE)

Фото не отображались — отсутствовал код загрузки файлов после открытия чата.

Проблема: extract_media_info() создавал PhotoInfo с PhotoDownloadState::NotDownloaded, но никакой код не инициировал download_file(). Фото оставались в состоянии "📷 [Фото]" без inline превью.

Исправление:

  • Авто-загрузка при открытии чата: после загрузки истории сообщений скачиваются фото из последних 30 сообщений (если auto_download_images = true и show_images = true). Каждый файл — с таймаутом 5 сек.
  • Загрузка по v: вместо "Фото не загружено" — скачивание + открытие модалки. Также повторная попытка при Error.
  • Обновление PhotoDownloadState в сообщении после успешной/неуспешной загрузки.

Модифицированные файлы:

  • src/input/handlers/chat_list.rs — авто-загрузка фото в open_chat_and_load_data()
  • src/input/handlers/chat.rshandle_view_image(): download on NotDownloaded + retry on Error

Этап 2+3: Account Switcher Modal + Переключение аккаунтов (DONE)

Реализована модалка переключения аккаунтов и механизм переключения:

  • Модалка Ctrl+A: глобальный оверлей поверх любого экрана (Loading/Auth/Main)
  • Навигация: j/k по списку, Enter выбор, a добавление, Esc закрытие
  • Переключение: close TDLib → recreate_client(new_db_path) → auth flow
  • Добавление аккаунта: ввод имени в модалке → валидация → add_account() → переключение
  • Footer индикатор: [account_name] если не "default"
  • AccountSwitcherState: enum SelectAccount / AddAccount — глобальный оверлей в App<T>
  • recreate_client(): новый метод в TdClientTrait — close old → new TdClient → spawn set_tdlib_parameters

Новые файлы:

  • src/ui/modals/account_switcher.rs — UI рендеринг (SelectAccount + AddAccount)
  • tests/account_switcher.rs — 12 тестов

Модифицированные файлы:

  • src/app/mod.rsAccountSwitcherState enum, 3 поля (account_switcher, current_account_name, pending_account_switch), 8 методов
  • src/accounts/manager.rsadd_account() (validate + save + ensure_dir)
  • src/accounts/mod.rs — re-export add_account
  • src/tdlib/trait.rsrecreate_client(&mut self, db_path) в TdClientTrait
  • src/tdlib/client.rs — реализация recreate_client (close → new → set_tdlib_parameters)
  • src/tdlib/client_impl.rs — trait impl делегирование
  • tests/helpers/fake_tdclient_impl.rs — no-op recreate_client
  • src/input/main_input.rs — account_switcher роутинг (highest priority)
  • src/input/handlers/global.rsCtrl+A → open_account_switcher
  • src/input/handlers/modal.rshandle_account_switcher() (SelectAccount + AddAccount input)
  • src/ui/modals/mod.rspub mod account_switcher;
  • src/ui/mod.rs — overlay поверх любого экрана
  • src/ui/footer.rs[account_name] индикатор
  • src/main.rspending_account_switch check в run_app, Ctrl+A из любого экрана

Этап 1: Инфраструктура профилей аккаунтов (DONE)

Реализована инфраструктура для мультиаккаунта:

  • Модуль accounts/: profile.rs (типы + валидация) + manager.rs (загрузка/сохранение/миграция)
  • accounts.toml: конфиг списка аккаунтов в ~/.config/tele-tui/accounts.toml
  • XDG data dir: БД TDLib хранится в ~/.local/share/tele-tui/accounts/{name}/tdlib_data/
  • Автомиграция: ./tdlib_data/ → XDG path при первом запуске
  • CLI флаг --account <name>: выбор аккаунта при запуске
  • Параметризация db_path: TdClient::new(db_path), App::new(config, db_path)

Предыдущий статус: Multiline Message Display (DONE)

Multiline в сообщениях

  • Multiline в сообщениях: \n корректно отображается в пузырях сообщений (split по \n + word wrap)
  • Маркер выделения: ▶ показывается только на первой строке multiline-сообщения
  • Перенос строки в инпуте отключён (Shift+Enter/Alt+Enter/Ctrl+J не вставляют \n)

Файлы изменены:

  • ui/components/message_bubble.rswrap_text_with_offsets() split по \n + wrap_paragraph() + selection marker fix

Vim Normal/Insert Mode (DONE)

Реализован Vim-like режим работы с двумя состояниями:

  • Normal mode (по умолчанию при открытии чата): навигация j/k, команды d/r/f/y, автоматический вход в MessageSelection
  • Insert mode (нажать i/ш): набор текста, Esc возвращает в Normal
  • Автопереключение в Insert при Reply (r) и Edit (Enter)
  • Визуальные индикаторы: [NORMAL]/[INSERT] в footer, зелёная/серая рамка compose bar
  • В Insert mode блокируются все команды кроме текстового ввода и Esc

Файлы изменены:

  • app/chat_state.rs — enum InputMode
  • app/mod.rs — поле input_mode в App<T>
  • config/keybindings.rsCommand::EnterInsertMode + keybinding i/ш
  • app/methods/navigation.rsclose_chat() сбрасывает input_mode
  • input/main_input.rs — главный роутер Insert/Normal
  • input/handlers/chat.rs — EnterInsertMode, auto-Insert при Reply/Edit
  • input/handlers/chat_list.rs — auto-MessageSelection при открытии чата
  • ui/footer.rs — mode indicator
  • ui/compose_bar.rs — visual mode differentiation
  • tests/ — обновлены для нового поведения

Предыдущий статус: Фаза 12 — Прослушивание голосовых сообщений (DONE)

Завершённые фазы (краткий итог)

Фаза Описание Статус
1 Базовая инфраструктура (ratatui + crossterm, vim-навигация) DONE
2 TDLib интеграция (авторизация, чаты, сообщения) DONE
3 Улучшение UX (отправка, поиск, скролл, realtime) DONE
4 Папки и фильтрация (загрузка папок, переключение 1-9) DONE
5 Расширенный функционал (онлайн-статус, медиа-заглушки, muted) DONE
6 Полировка (60 FPS, память, graceful shutdown, динамический инпут) DONE
7 Рефакторинг памяти (единый источник данных, LRU-кэш) DONE
8 Дополнительные фичи (markdown, edit/delete, reply/forward, блочный курсор) DONE
9 Расширенные возможности (typing, pinned, поиск в чате, черновики, профиль, копирование, реакции, конфиг) DONE
10 Desktop уведомления (notify-rust, muted фильтр, mentions, медиа) DONE (83%)
11 Inline просмотр фото (ratatui-image, кэш, загрузка) DONE
12 Прослушивание голосовых сообщений (ffplay, play/pause, seek, ticker, cache, config) DONE
13 Глубокий рефакторинг архитектуры (7 этапов) DONE

Фаза 11: Inline фото + оптимизации (подробности)

Feature-gated (images), 2-tier архитектура:

Базовая реализация:

  1. Типы: MediaInfo, PhotoInfo, PhotoDownloadState, ImageModalState, ImagesConfig
  2. Зависимости: ratatui-image 8.1, image 0.25 (feature-gated)
  3. Media модуль: ImageCache (LRU), dual ImageRenderer (inline + modal)
  4. UX: Always-show inline preview (фикс. ширина 50 chars) + полноэкранная модалка на v/м
  5. Метаданные: extract_media_info() из TDLib MessagePhoto; auto-download visible photos

Оптимизации производительности:

  1. Dual protocol strategy:
    • inline_image_renderer: Halfblocks → быстро (Unicode блоки), для навигации
    • modal_image_renderer: iTerm2/Sixel → медленно (high quality), для просмотра
  2. Frame throttling: inline images 15 FPS (66ms), текст 60 FPS
  3. Lazy loading: загрузка только видимых изображений (не все сразу)
  4. LRU кэш: max 100 протоколов, eviction старых
  5. Loading indicator: " Загрузка..." в модалке при первом открытии
  6. Navigation hotkeys: / между фото, Esc/q закрыть модалку

UI рендеринг:

  • message_bubble.rs: photo status (Downloading/Error/placeholder), inline preview
  • messages.rs: второй проход с render_images() + throttling + только видимые
  • modals/image_viewer.rs: fullscreen modal с aspect ratio + loading state

Фаза 13: Рефакторинг (подробности)

Разбиты 5 монолитных файлов (4582 строк) на модульную архитектуру:

  • input/main_input.rs (1199→164): чистый роутер + 5 handler модулей в handlers/
  • app/mod.rs (1015→371): 5 trait модулей в methods/ (Navigation, Message, Compose, Search, Modal)
  • ui/messages.rs (893→365): модули modals/ (search, pinned, delete, reactions) + compose_bar.rs
  • tdlib/messages.rs (836→3 файла): messages/ (mod, convert, operations)
  • config/mod.rs (642→3 файла): validation.rs, loader.rs
  • Очистка дублей: ~220 строк удалено (shared components, format_user_status, scroll_to_message)
  • Документация: PROJECT_STRUCTURE.md переписан, 16 файлов получили //! docs

Фаза 12: Голосовые сообщения (подробности)

Реализовано:

  • AudioPlayer на ffplay (subprocess): play, pause (SIGSTOP), resume (SIGCONT), stop
  • VoiceCache: LRU кэш OGG файлов в ~/.cache/tele-tui/voice/ (max 100 MB)
  • Типы: VoiceInfo, VoiceDownloadState, PlaybackState, PlaybackStatus
  • TDLib интеграция: download_voice_note(), конвертация MessageVoiceNote
  • Хоткеи: Space (play/pause), ←/→ (seek ±5s via ffplay restart с -ss)
  • Автостоп: при навигации на другое сообщение воспроизведение останавливается
  • Ticker: last_playback_tick в App + обновление position в event loop (1 FPS redraw)
  • VoiceCache: проверка кэша перед загрузкой, кэширование после download
  • AudioConfig: [audio] секция в config.toml (cache_size_mb, auto_download_voice)
  • UI: progress bar (━●─) + waveform (▁▂▃▄▅▆▇█) + иконки статуса в message_bubble.rs
  • Race condition fixes: starting flag + pid ownership guard в потоках AudioPlayer
  • Seek: resume_from() перезапускает ffplay с -ss offset; MoveLeft/MoveRight как alias для SeekBackward/SeekForward
  • Resume with rewind: пауза→продолжение откатывает на 1 секунду назад
  • Graceful shutdown: stop_playback() + Drop impl для AudioPlayer

Ключевая архитектура

main.rs → event loop (16ms poll)
├── input/ → роутер + handlers/ (chat, chat_list, compose, modal, search)
├── app/ → App<T: TdClientTrait> + methods/ (5 traits, 67 методов)
├── ui/ → рендеринг (messages, chat_list, modals/, compose_bar, components/)
├── audio/ → player.rs (ffplay), cache.rs (VoiceCache)
├── media/ → [feature=images] cache.rs, image_renderer.rs
└── tdlib/ → TDLib wrapper (client, auth, chats, messages/, users, reactions, types)

Тестирование

500+ тестов (0 failures):

  • Snapshot tests: 57 (UI компоненты)
  • Integration tests: 93 (user flows)
  • E2E tests: 12 (smoke + journey)
  • Utils tests: 18
  • Performance benchmarks: 8

Ключевые решения

  1. Неблокирующий receive: TDLib updates через mpsc::channel в отдельном потоке
  2. Trait-based App: методы разбиты на traits — требуют use import на call site
  3. FakeTdClient: mock для тестов без TDLib (реализует TdClientTrait)
  4. Оптимизация рендеринга: needs_redraw флаг, рендеринг только при изменениях
  5. Конфиг: TOML ~/.config/tele-tui/config.toml, credentials с приоритетом (XDG → .env)
  6. Feature-gated images: images feature flag для ratatui-image + image deps
  7. Dual renderer: inline (Halfblocks, 15 FPS) + modal (iTerm2/Sixel, high quality) для баланса скорости/качества
  8. Audio via ffplay: subprocess с SIGSTOP/SIGCONT для pause/resume, автостоп при навигации

Зависимости (основные)

ratatui = "0.29"           # TUI фреймворк
crossterm = "0.28"          # Терминальный backend
tdlib-rs = "1.1"            # Telegram TDLib binding
tokio = "1"                 # Async runtime
notify-rust = "4.11"        # Desktop уведомления (feature flag)
ratatui-image = "8.1"       # Inline images (feature flag)
image = "0.25"              # Image decoding (feature flag)

Полная структура проекта: см. PROJECT_STRUCTURE.md Подробный план: см. ROADMAP.md