@emaxe/tuigram 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +7 -0
- package/CHANGELOG.md +27 -1
- package/CHANGELOG.ru.md +27 -1
- package/README.md +43 -0
- package/README.ru.md +43 -0
- package/package.json +3 -1
- package/src/config.js +3 -0
- package/src/state.js +4 -4
- package/src/telegram/messages.js +58 -0
- package/src/ui/app.js +169 -5
- package/src/ui/components/chatList.js +62 -0
- package/src/ui/components/chatView.js +116 -14
- package/src/ui/components/header.js +24 -1
- package/src/ui/components/inputBox.js +40 -1
- package/src/ui/components/modals/actionModal.js +31 -0
- package/src/ui/components/modals/chatInfoModal.js +19 -0
- package/src/ui/components/modals/confirmModal.js +18 -0
- package/src/ui/components/modals/fileModal.js +33 -1
- package/src/ui/components/modals/filePickerModal.js +19 -0
- package/src/ui/components/modals/helpModal.js +24 -0
- package/src/ui/components/statusBar.js +53 -1
- package/src/ui/screen.js +52 -3
- package/src/utils/image.js +343 -0
- package/src/utils/mouse.js +236 -0
package/.env.example
CHANGED
|
@@ -28,6 +28,13 @@ AUTO_SCROLL=true
|
|
|
28
28
|
# Показывать уведомления о наборе текста (true/false)
|
|
29
29
|
SHOW_TYPING=true
|
|
30
30
|
|
|
31
|
+
# Отображение цветных превью изображений в чате (true/false)
|
|
32
|
+
SHOW_IMAGES=true
|
|
33
|
+
|
|
34
|
+
# Максимальные размеры превью изображений в символах терминала (ширина x высота)
|
|
35
|
+
IMAGE_MAX_WIDTH=36
|
|
36
|
+
IMAGE_MAX_HEIGHT=14
|
|
37
|
+
|
|
31
38
|
# ── Proxy Settings ────────────────────────────────────────────────────────────
|
|
32
39
|
# Поддерживаются HTTP и SOCKS5 (как с авторизацией, так и без).
|
|
33
40
|
# Формат единого URL:
|
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
13
13
|
|
|
14
14
|
- English localization of the TUI and CLI interface strings.
|
|
15
15
|
|
|
16
|
+
## [1.3.0] — 2026-09-01
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- Comprehensive mouse support across the entire TUI:
|
|
21
|
+
- Direct panel focus on click (left panel for dialog list, middle panel for message feed, bottom panel for input box).
|
|
22
|
+
- Single-click dialog selection and immediate opening.
|
|
23
|
+
- Smooth mouse wheel scrolling for dialog list and message feed with automatic loading of older history when scrolling up past the top.
|
|
24
|
+
- Clicking on any message opens its contextual action menu (reactions, replies, edits, downloads, deletion).
|
|
25
|
+
- Clickable filter tabs (`1:Все`, `2:ЛС`, `3:Группы`, `4:Каналы`, `5:Боты`, `6:Непроч`) and search input focusing.
|
|
26
|
+
- Interactive status bar buttons (`[Tab]`, `[1-6]`, `[/]`, `[F1]`, `[Ctrl+A]`, `[Ctrl+P]`, `[Ctrl+Q]`).
|
|
27
|
+
- Interactive header elements: click logo for Help, click chat title for Chat Info, click status badge for network state.
|
|
28
|
+
- Interactive context bar in input box: click to cancel reply/edit mode or trigger shortcuts.
|
|
29
|
+
- Full mouse navigation inside modals (button clicks, checkbox toggles, file browsing) and dismissing any modal by clicking outside its boundary.
|
|
30
|
+
- Robust terminal mouse mode initialization (`\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?1015h`) supporting modern terminals (macOS Terminal.app, iTerm2, Alacritty, Kitty, Windows Terminal, xterm).
|
|
31
|
+
- Pure mouse coordinate calculation utilities (`src/utils/mouse.js`) with full test coverage in `test/unit.test.js`.
|
|
32
|
+
|
|
33
|
+
## [1.2.0] — 2026-09-01
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Display color image previews directly in chat messages using high-resolution Unicode half-block characters (`▀` `U+2580`) with instant rendering of Telegram stripped thumbnails (`PhotoStrippedSize`).
|
|
38
|
+
- Configuration options `SHOW_IMAGES`, `IMAGE_MAX_WIDTH`, and `IMAGE_MAX_HEIGHT` to control image preview rendering in `.env` and `config.js`.
|
|
39
|
+
|
|
16
40
|
## [1.1.0] — 2026-08-31
|
|
17
41
|
|
|
18
42
|
### Added
|
|
@@ -111,7 +135,9 @@ First public release.
|
|
|
111
135
|
- A WCAG contrast test (3:1 threshold) applied to every theme **after** conversion
|
|
112
136
|
to xterm-256 — the colors are checked exactly as the user sees them.
|
|
113
137
|
|
|
114
|
-
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.
|
|
138
|
+
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.3.0...HEAD
|
|
139
|
+
[1.3.0]: https://github.com/emaxe/tuigram/compare/v1.2.0...v1.3.0
|
|
140
|
+
[1.2.0]: https://github.com/emaxe/tuigram/compare/v1.1.0...v1.2.0
|
|
115
141
|
[1.1.0]: https://github.com/emaxe/tuigram/compare/v1.0.1...v1.1.0
|
|
116
142
|
[1.0.1]: https://github.com/emaxe/tuigram/compare/v1.0.0...v1.0.1
|
|
117
143
|
[1.0.0]: https://github.com/emaxe/tuigram/releases/tag/v1.0.0
|
package/CHANGELOG.ru.md
CHANGED
|
@@ -13,6 +13,30 @@
|
|
|
13
13
|
|
|
14
14
|
- Английская локализация строк интерфейса TUI и CLI.
|
|
15
15
|
|
|
16
|
+
## [1.3.0] — 2026-09-01
|
|
17
|
+
|
|
18
|
+
### Добавлено
|
|
19
|
+
|
|
20
|
+
- Полноценная поддержка управления мышью во всём TUI-интерфейсе:
|
|
21
|
+
- Фокусировка любой панели по клику (список чатов, лента сообщений, поле ввода).
|
|
22
|
+
- Открытие диалога в один клик по элементу списка.
|
|
23
|
+
- Плавная прокрутка списка чатов и ленты сообщений колесом мыши с автоматической подгрузкой старой истории при прокрутке вверх.
|
|
24
|
+
- Открытие меню действий над сообщением (реакции, ответ, редактирование, скачивание, удаление) по прямому клику на сообщение.
|
|
25
|
+
- Интерактивные вкладки фильтрации (`1:Все`, `2:ЛС`, `3:Группы`, `4:Каналы`, `5:Боты`, `6:Непроч`) и строка поиска в списке диалогов.
|
|
26
|
+
- Интерактивная нижняя строка состояния (`[Tab]`, `[1-6]`, `[/]`, `[F1]`, `[Ctrl+A]`, `[Ctrl+P]`, `[Ctrl+Q]`).
|
|
27
|
+
- Интерактивный верхний заголовок: клик по логотипу открывает справку, по названию активного чата — сведения о чате, по статусу — информацию о сети.
|
|
28
|
+
- Контекстная строка поля ввода: клик сбрасывает режим ответа/редактирования либо запускает команды.
|
|
29
|
+
- Поддержка мыши во всех модальных окнах (клики по кнопкам, чекбоксам, файлам в проводнике) и закрытие модалок по клику мимо окна.
|
|
30
|
+
- Надёжная инициализация режимов мыши терминала (`\x1b[?1000h\x1b[?1002h\x1b[?1006h\x1b[?1015h`) с поддержкой современных эмуляторов (macOS Terminal.app, iTerm2, Alacritty, Kitty, Windows Terminal, xterm).
|
|
31
|
+
- Чистые утилиты расчёта координат мыши (`src/utils/mouse.js`) и покрытие юнит-тестами в `test/unit.test.js`.
|
|
32
|
+
|
|
33
|
+
## [1.2.0] — 2026-09-01
|
|
34
|
+
|
|
35
|
+
### Добавлено
|
|
36
|
+
|
|
37
|
+
- Отображение цветных превью изображений прямо в ленте сообщений чата с помощью псевдографики высокого разрешения (Unicode Half-Block `▀` `U+2580`) и мгновенного рендеринга миниатюр `PhotoStrippedSize`.
|
|
38
|
+
- Параметры конфигурации `SHOW_IMAGES`, `IMAGE_MAX_WIDTH` и `IMAGE_MAX_HEIGHT` в `.env` и `config.js` для управления отображением превью и их максимальным размером.
|
|
39
|
+
|
|
16
40
|
## [1.1.0] — 2026-08-31
|
|
17
41
|
|
|
18
42
|
### Добавлено
|
|
@@ -113,7 +137,9 @@
|
|
|
113
137
|
- Тест контраста по WCAG (порог 3:1) для каждой темы — **после** конверсии в xterm-256,
|
|
114
138
|
то есть ровно в том виде, в каком цвет увидит пользователь.
|
|
115
139
|
|
|
116
|
-
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.
|
|
140
|
+
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.3.0...HEAD
|
|
141
|
+
[1.3.0]: https://github.com/emaxe/tuigram/compare/v1.2.0...v1.3.0
|
|
142
|
+
[1.2.0]: https://github.com/emaxe/tuigram/compare/v1.1.0...v1.2.0
|
|
117
143
|
[1.1.0]: https://github.com/emaxe/tuigram/compare/v1.0.1...v1.1.0
|
|
118
144
|
[1.0.1]: https://github.com/emaxe/tuigram/compare/v1.0.0...v1.0.1
|
|
119
145
|
[1.0.0]: https://github.com/emaxe/tuigram/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -52,9 +52,11 @@ TuiGram lets you use Telegram entirely from the terminal: browse your dialog lis
|
|
|
52
52
|
- [Where files are stored](#-where-files-are-stored)
|
|
53
53
|
- [Development](#-development)
|
|
54
54
|
- [Keyboard shortcuts](#️-keyboard-shortcuts)
|
|
55
|
+
- [Mouse controls](#-mouse-controls)
|
|
55
56
|
- [Slash commands](#-slash-commands-in-the-input-box)
|
|
56
57
|
- [Sending files and images](#-sending-files-and-images)
|
|
57
58
|
- [Themes](#-themes)
|
|
59
|
+
- [Inline image previews](#️-inline-image-previews)
|
|
58
60
|
- [Proxy configuration](#-proxy-configuration)
|
|
59
61
|
- [Command line usage (CLI)](#️-command-line-usage-cli)
|
|
60
62
|
- [Project structure](#-project-structure)
|
|
@@ -74,6 +76,7 @@ TuiGram lets you use Telegram entirely from the terminal: browse your dialog lis
|
|
|
74
76
|
- Instant search and filtering of chats by title and `@username` (`/`);
|
|
75
77
|
- Live typing indicator ("… is typing");
|
|
76
78
|
- Colored rendering of Telegram entities (bold, italic, monospace code, URLs, mentions, spoilers);
|
|
79
|
+
- High-resolution inline image previews (Unicode Half-Block `▀` pixel art);
|
|
77
80
|
- Media attachment indicators (photo, video, document, voice, sticker, poll);
|
|
78
81
|
- Message reactions (👍, 🔥, ❤️);
|
|
79
82
|
- Infinite upward pagination of message history (`PageUp` / `Ctrl+U`);
|
|
@@ -450,6 +453,28 @@ node scripts/check-package.js # verifies that .env and the session do not leak
|
|
|
450
453
|
|
|
451
454
|
---
|
|
452
455
|
|
|
456
|
+
## 🖱 Mouse controls
|
|
457
|
+
|
|
458
|
+
TuiGram fully supports mouse interaction in terminals that support mouse reporting (macOS Terminal, iTerm2, Alacritty, Kitty, Windows Terminal, Linux virtual terminals):
|
|
459
|
+
|
|
460
|
+
| Action | Target | Result |
|
|
461
|
+
|---|---|---|
|
|
462
|
+
| Click | Left / Middle / Bottom panel | Focus the clicked panel |
|
|
463
|
+
| Click | Chat item in dialog list | Immediately select and open the chat |
|
|
464
|
+
| Click | Filter tabs `1:Все` .. `6:Непроч` | Switch chat category filter |
|
|
465
|
+
| Click | Search bar `[/]` | Focus search box and clear placeholder |
|
|
466
|
+
| Wheel scroll | Dialog list / Message feed | Smooth scroll (scrolling up to the top loads older messages) |
|
|
467
|
+
| Click | Any message line | Open message action menu (reactions, reply, edit, download, delete) |
|
|
468
|
+
| Click | Header logo 🚀 | Open Help window |
|
|
469
|
+
| Click | Header active chat title | Open Chat Info window |
|
|
470
|
+
| Click | Header connection badge | Show active network status |
|
|
471
|
+
| Click | Input context bar | Cancel reply/edit mode or insert `/` |
|
|
472
|
+
| Click | Status bar items | Trigger corresponding action (`[Tab]`, `[F1]`, `[Ctrl+A]`, `[Ctrl+Q]`, etc.) |
|
|
473
|
+
| Click | Outside any modal window | Dismiss / close modal |
|
|
474
|
+
| Click | Buttons in modals | Click buttons (`[ Отправить ]`, `[ Отмена ]`, `[ Закрыть ]`, toggle checkboxes) |
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
453
478
|
## 💬 Slash commands in the input box
|
|
454
479
|
|
|
455
480
|
Quick commands are available in the message input box (they start with `/`):
|
|
@@ -512,6 +537,23 @@ The contrast of every "text on background" pair is verified by an automated WCAG
|
|
|
512
537
|
|
|
513
538
|
---
|
|
514
539
|
|
|
540
|
+
## 🖼️ Inline image previews
|
|
541
|
+
|
|
542
|
+
TuiGram displays color image previews directly in the chat history using high-resolution Unicode half-block characters (`▀` `U+2580`). Each character cell renders 2 vertical subpixels with 24-bit RGB colors, producing crisp, proportional pixel art that scrolls naturally with messages and works across all terminals.
|
|
543
|
+
|
|
544
|
+
Previews are enabled by default and can be configured in `.env`:
|
|
545
|
+
|
|
546
|
+
```env
|
|
547
|
+
# Enable/disable image previews in chat (true/false)
|
|
548
|
+
SHOW_IMAGES=true
|
|
549
|
+
|
|
550
|
+
# Maximum preview size in terminal characters (width x height)
|
|
551
|
+
IMAGE_MAX_WIDTH=36
|
|
552
|
+
IMAGE_MAX_HEIGHT=14
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
---
|
|
556
|
+
|
|
515
557
|
## 🌐 Proxy configuration
|
|
516
558
|
|
|
517
559
|
TuiGram supports routing MTProto connections through HTTP (including HTTPS CONNECT) and SOCKS5/SOCKS4 proxies — both with and without username/password authentication.
|
|
@@ -612,6 +654,7 @@ TuiGram/
|
|
|
612
654
|
│ │ ├── init.js # tuigram init / paths — setup and diagnostics
|
|
613
655
|
│ │ └── formatters.js # Console table and log formatters
|
|
614
656
|
│ └── utils/
|
|
657
|
+
│ ├── image.js # Image decoding, resizing and Half-Block ANSI rendering
|
|
615
658
|
│ ├── storage.js # File operations and session persistence
|
|
616
659
|
│ └── time.js # Time and date formatting
|
|
617
660
|
├── scripts/
|
package/README.ru.md
CHANGED
|
@@ -48,9 +48,11 @@
|
|
|
48
48
|
- [Где хранятся файлы](#-где-хранятся-файлы)
|
|
49
49
|
- [Разработка](#-разработка)
|
|
50
50
|
- [Горячие клавиши](#️-горячие-клавиши)
|
|
51
|
+
- [Управление мышью](#-управление-мышью)
|
|
51
52
|
- [Слэш-команды](#-слэш-команды-в-поле-ввода)
|
|
52
53
|
- [Отправка файлов и картинок](#-отправка-файлов-и-картинок)
|
|
53
54
|
- [Темы оформления](#-темы-оформления)
|
|
55
|
+
- [Превью изображений в сообщениях](#️-превью-изображений-в-сообщениях)
|
|
54
56
|
- [Прокси-сервер](#-прокси-сервер)
|
|
55
57
|
- [Использование через консоль (CLI)](#️-использование-через-консоль-cli)
|
|
56
58
|
- [Структура проекта](#-структура-проекта)
|
|
@@ -70,6 +72,7 @@
|
|
|
70
72
|
- Мгновенный поиск и фильтрация чатов по названию и `@username` (`/`);
|
|
71
73
|
- Отображение статуса набора текста («Собеседник печатает...») в реальном времени;
|
|
72
74
|
- Цветовое форматирование Telegram Entities (Bold, Italic, Monospace Code, URLs, Mentions, Spoilers);
|
|
75
|
+
- Цветные превью изображений высокого разрешения прямо в чате (Unicode Half-Block `▀` pixel art);
|
|
73
76
|
- Индикаторы медиа-вложений (фото, видео, документы, голосовые, стикеры, опросы);
|
|
74
77
|
- Реакции на сообщения (👍, 🔥, ❤️);
|
|
75
78
|
- Бесконечная пагинация истории сообщений вверх (`PageUp` / `Ctrl+U`);
|
|
@@ -442,6 +445,28 @@ node scripts/check-package.js # проверка, что в пакет не у
|
|
|
442
445
|
|
|
443
446
|
---
|
|
444
447
|
|
|
448
|
+
## 🖱 Управление мышью
|
|
449
|
+
|
|
450
|
+
TuiGram полноценно поддерживает управление мышью во всех современных терминалах (Terminal.app, iTerm2, Alacritty, Kitty, Windows Terminal, xterm):
|
|
451
|
+
|
|
452
|
+
| Действие | Где | Результат |
|
|
453
|
+
|---|---|---|
|
|
454
|
+
| Клик | Левая / Центральная / Нижняя панель | Переключение фокуса на выбранную панель |
|
|
455
|
+
| Клик | Диалог в списке чатов | Мгновенный выбор и открытие чата |
|
|
456
|
+
| Клик | Вкладки `1:Все` .. `6:Непроч` | Переключение фильтра категории чатов |
|
|
457
|
+
| Клик | Поле поиска `[/]` | Фокус в поиск с очисткой подсказки |
|
|
458
|
+
| Колесо мыши | Список чатов / Лента сообщений | Плавная прокрутка (прокрутка вверх в ленте подгружает историю) |
|
|
459
|
+
| Клик | Любое сообщение | Открытие меню действий (реакции, ответ, редактирование, удаление) |
|
|
460
|
+
| Клик | Логотип 🚀 в шапке | Открытие окна справки |
|
|
461
|
+
| Клик | Название активного чата в шапке | Открытие информации о текущем чате |
|
|
462
|
+
| Клик | Индикатор статуса в шапке | Просмотр статуса MTProto подключения |
|
|
463
|
+
| Клик | Контекстная плашка ввода | Сброс режима ответа/редактирования или вставка `/` |
|
|
464
|
+
| Клик | Кнопки строки состояния | Быстрый вызов действий (`[Tab]`, `[F1]`, `[Ctrl+A]`, `[Ctrl+Q]` и др.) |
|
|
465
|
+
| Клик | Мимо любого модального окна | Закрытие модального окна |
|
|
466
|
+
| Клик | Кнопки в модалках | Нажатие кнопки (`[ Отправить ]`, `[ Отмена ]`, `[ Закрыть ]`, чекбоксы) |
|
|
467
|
+
|
|
468
|
+
---
|
|
469
|
+
|
|
445
470
|
## 💬 Слэш-команды в поле ввода
|
|
446
471
|
|
|
447
472
|
В поле ввода сообщения доступны быстрые команды (начинаются с `/`):
|
|
@@ -506,6 +531,23 @@ TUI_THEME=light # светлая
|
|
|
506
531
|
|
|
507
532
|
---
|
|
508
533
|
|
|
534
|
+
## 🖼️ Превью изображений в сообщениях
|
|
535
|
+
|
|
536
|
+
TuiGram отображает цветные превью фотографий прямо в ленте сообщений чата с помощью псевдографики высокого разрешения (Unicode Half-Block `▀` `U+2580`). Каждое знакоместо терминала вмещает 2 вертикальных субпикселя с 24-битными цветами, обеспечивая чёткое пропорциональное изображение 1:1, которое плавно прокручивается вместе с сообщениями и поддерживается всеми терминалами.
|
|
537
|
+
|
|
538
|
+
Отображение превью включено по умолчанию и настраивается в `.env`:
|
|
539
|
+
|
|
540
|
+
```env
|
|
541
|
+
# Включение/отключение превью картинок в чате (true/false)
|
|
542
|
+
SHOW_IMAGES=true
|
|
543
|
+
|
|
544
|
+
# Максимальный размер превью в символах терминала (ширина x высота)
|
|
545
|
+
IMAGE_MAX_WIDTH=36
|
|
546
|
+
IMAGE_MAX_HEIGHT=14
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
509
551
|
## 🌐 Прокси-сервер
|
|
510
552
|
|
|
511
553
|
TuiGram поддерживает маршрутизацию MTProto-соединений через HTTP (включая HTTPS CONNECT) и SOCKS5/SOCKS4 прокси — как с авторизацией по логину и паролю, так и без неё.
|
|
@@ -606,6 +648,7 @@ TuiGram/
|
|
|
606
648
|
│ │ ├── init.js # tuigram init / paths — настройка и диагностика
|
|
607
649
|
│ │ └── formatters.js # Консольные форматтеры таблиц и логов
|
|
608
650
|
│ └── utils/
|
|
651
|
+
│ ├── image.js # Декодирование, масштабирование и Half-Block ANSI рендеринг изображений
|
|
609
652
|
│ ├── storage.js # Файловые операции и сохранение сессий
|
|
610
653
|
│ └── time.js # Форматирование времени и дат
|
|
611
654
|
├── scripts/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emaxe/tuigram",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Полнофункциональный TUI & CLI клиент Telegram на Node.js на базе MTProto (teleproto)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -70,7 +70,9 @@
|
|
|
70
70
|
"colorette": "^2.0.20",
|
|
71
71
|
"dotenv": "^16.4.7",
|
|
72
72
|
"input": "^1.0.1",
|
|
73
|
+
"jpeg-js": "^0.4.4",
|
|
73
74
|
"neo-blessed": "^0.2.0",
|
|
75
|
+
"pngjs": "^7.0.0",
|
|
74
76
|
"teleproto": "^1.229.0"
|
|
75
77
|
}
|
|
76
78
|
}
|
package/src/config.js
CHANGED
|
@@ -251,6 +251,9 @@ export const config = {
|
|
|
251
251
|
theme: process.env.TUI_THEME || "default",
|
|
252
252
|
autoScroll: String(process.env.AUTO_SCROLL || "true").toLowerCase() === "true",
|
|
253
253
|
showTyping: String(process.env.SHOW_TYPING || "true").toLowerCase() === "true",
|
|
254
|
+
showImages: String(process.env.SHOW_IMAGES || "true").toLowerCase() !== "false",
|
|
255
|
+
imageMaxWidth: parseInt(process.env.IMAGE_MAX_WIDTH || "36", 10) || 36,
|
|
256
|
+
imageMaxHeight: parseInt(process.env.IMAGE_MAX_HEIGHT || "14", 10) || 14,
|
|
254
257
|
|
|
255
258
|
proxy: parseProxyConfig(),
|
|
256
259
|
|
package/src/state.js
CHANGED
|
@@ -140,7 +140,7 @@ class AppState extends EventEmitter {
|
|
|
140
140
|
combined.sort((a, b) => (a.date || 0) - (b.date || 0) || (a.id - b.id));
|
|
141
141
|
|
|
142
142
|
this.messagesByChat.set(chatId, combined);
|
|
143
|
-
this.emit("messages_updated", { chatId, messages: combined, isPrepend: prepend });
|
|
143
|
+
this.emit("messages_updated", { chatId, messages: combined, isPrepend: prepend, isUpdate: false });
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
@@ -181,7 +181,7 @@ class AppState extends EventEmitter {
|
|
|
181
181
|
this.emit("dialogs_updated", this.getVisibleDialogs());
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
this.emit("messages_updated", { chatId, messages: list, isPrepend: false });
|
|
184
|
+
this.emit("messages_updated", { chatId, messages: list, isPrepend: false, isNewMessage: true });
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
/**
|
|
@@ -194,7 +194,7 @@ class AppState extends EventEmitter {
|
|
|
194
194
|
const index = list.findIndex((m) => m.id === updatedMessage.id);
|
|
195
195
|
if (index !== -1) {
|
|
196
196
|
list[index] = { ...list[index], ...updatedMessage };
|
|
197
|
-
this.emit("messages_updated", { chatId, messages: list, isPrepend: false });
|
|
197
|
+
this.emit("messages_updated", { chatId, messages: list, isPrepend: false, isUpdate: true });
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
|
|
@@ -208,7 +208,7 @@ class AppState extends EventEmitter {
|
|
|
208
208
|
const idSet = new Set(deletedIds);
|
|
209
209
|
const filtered = list.filter((m) => !idSet.has(m.id));
|
|
210
210
|
this.messagesByChat.set(chatId, filtered);
|
|
211
|
-
this.emit("messages_updated", { chatId, messages: filtered, isPrepend: false });
|
|
211
|
+
this.emit("messages_updated", { chatId, messages: filtered, isPrepend: false, isUpdate: true });
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
package/src/telegram/messages.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Api, errors } from "teleproto";
|
|
2
2
|
import { idToString, toMarkedId, getEntityDisplayName, entityCache, resolveEntity } from "./entities.js";
|
|
3
3
|
import { describeMedia } from "./formatter.js";
|
|
4
|
+
import { config } from "../config.js";
|
|
5
|
+
import { renderStrippedThumbnail, renderImageBuffer } from "../utils/image.js";
|
|
4
6
|
|
|
5
7
|
const { FloodWaitError } = errors;
|
|
6
8
|
|
|
@@ -38,6 +40,27 @@ export function normalizeMessage(message) {
|
|
|
38
40
|
}
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
// Извлечение и рендеринг PhotoStrippedSize в псевдографику
|
|
44
|
+
let imagePreview = null;
|
|
45
|
+
if (config.showImages && message.media) {
|
|
46
|
+
const media = message.media;
|
|
47
|
+
const photo = media.photo;
|
|
48
|
+
const doc = media.document;
|
|
49
|
+
const sizes = photo?.sizes || doc?.thumbs || [];
|
|
50
|
+
const stripped = sizes.find((s) => s?.className === "PhotoStrippedSize" || s?.type === "i" || (s?.bytes && s.bytes.length > 0));
|
|
51
|
+
|
|
52
|
+
if (stripped?.bytes) {
|
|
53
|
+
const cacheKey = photo?.id
|
|
54
|
+
? `photo_${photo.id}`
|
|
55
|
+
: (doc?.id ? `doc_${doc.id}` : `msg_${message.id}`);
|
|
56
|
+
imagePreview = renderStrippedThumbnail(stripped.bytes, {
|
|
57
|
+
maxWidth: config.imageMaxWidth,
|
|
58
|
+
maxHeight: config.imageMaxHeight,
|
|
59
|
+
cacheKey,
|
|
60
|
+
}) || null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
41
64
|
return {
|
|
42
65
|
id: message.id,
|
|
43
66
|
date: message.date ? message.date * 1000 : Date.now(),
|
|
@@ -53,6 +76,7 @@ export function normalizeMessage(message) {
|
|
|
53
76
|
forwards: message.forwards || null,
|
|
54
77
|
media: message.media || null,
|
|
55
78
|
mediaDescription: describeMedia(message.media),
|
|
79
|
+
imagePreview,
|
|
56
80
|
entities: message.entities || [],
|
|
57
81
|
reactions,
|
|
58
82
|
rawMessage: message,
|
|
@@ -263,3 +287,37 @@ export async function markAsRead(client, rawPeer, maxId = 0) {
|
|
|
263
287
|
// Игнорируем незначительные сетевые ошибки прочтения
|
|
264
288
|
}
|
|
265
289
|
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Асинхронно загружает и декодирует превью изображения сообщения через MTProto.
|
|
293
|
+
* @param {import("teleproto").TelegramClient} client
|
|
294
|
+
* @param {object} rawMessage
|
|
295
|
+
* @param {object} [options]
|
|
296
|
+
* @param {number} [options.maxWidth]
|
|
297
|
+
* @param {number} [options.maxHeight]
|
|
298
|
+
* @returns {Promise<string>}
|
|
299
|
+
*/
|
|
300
|
+
export async function loadMessageImagePreview(client, rawMessage, { maxWidth = config.imageMaxWidth, maxHeight = config.imageMaxHeight } = {}) {
|
|
301
|
+
if (!rawMessage?.media) return "";
|
|
302
|
+
const media = rawMessage.media;
|
|
303
|
+
const isPhoto = media.className === "MessageMediaPhoto";
|
|
304
|
+
const isDoc = media.className === "MessageMediaDocument";
|
|
305
|
+
if (!isPhoto && !isDoc) return "";
|
|
306
|
+
|
|
307
|
+
const cacheKey = isPhoto
|
|
308
|
+
? `photo_full_${media.photo?.id || rawMessage.id}`
|
|
309
|
+
: `doc_full_${media.document?.id || rawMessage.id}`;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
const thumbBuf = await client.downloadMedia(media, { thumb: 1 });
|
|
313
|
+
if (!thumbBuf || thumbBuf.length === 0) return "";
|
|
314
|
+
return renderImageBuffer(thumbBuf, {
|
|
315
|
+
maxWidth,
|
|
316
|
+
maxHeight,
|
|
317
|
+
cacheKey,
|
|
318
|
+
});
|
|
319
|
+
} catch {
|
|
320
|
+
return "";
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
package/src/ui/app.js
CHANGED
|
@@ -16,7 +16,7 @@ import { state } from "../state.js";
|
|
|
16
16
|
import { config } from "../config.js";
|
|
17
17
|
import { fetchDialogs } from "../telegram/dialogs.js";
|
|
18
18
|
import { setMessagePalette } from "../telegram/formatter.js";
|
|
19
|
-
import { fetchHistory, sendMessage, editMessage, deleteMessages, sendFiles, downloadMedia, sendReaction, markAsRead } from "../telegram/messages.js";
|
|
19
|
+
import { fetchHistory, sendMessage, editMessage, deleteMessages, sendFiles, downloadMedia, sendReaction, markAsRead, loadMessageImagePreview } from "../telegram/messages.js";
|
|
20
20
|
import { startTelegramListener } from "../telegram/listener.js";
|
|
21
21
|
import { logout } from "../telegram/auth.js";
|
|
22
22
|
import { ensureDir, inspectLocalFile } from "../utils/storage.js";
|
|
@@ -43,8 +43,79 @@ export async function startTui(client, me) {
|
|
|
43
43
|
state.setConnectionStatus("connected");
|
|
44
44
|
|
|
45
45
|
// 1. Создание визуальных компонентов
|
|
46
|
-
const header = createHeader(screen, theme
|
|
47
|
-
|
|
46
|
+
const header = createHeader(screen, theme, {
|
|
47
|
+
onHelp: () => {
|
|
48
|
+
releaseInputs();
|
|
49
|
+
helpModal.show();
|
|
50
|
+
},
|
|
51
|
+
onChatInfo: () => {
|
|
52
|
+
if (state.activeChat) {
|
|
53
|
+
releaseInputs();
|
|
54
|
+
chatInfoModal.show(state.activeChat);
|
|
55
|
+
} else {
|
|
56
|
+
statusBar.showMessage("Сначала выберите чат слева!", "warning");
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
onStatusClick: () => {
|
|
60
|
+
const statusText = state.connectionStatus === "connected"
|
|
61
|
+
? "Подключение активно: MTProto онлайн"
|
|
62
|
+
: `Статус сети: ${state.connectionStatus}`;
|
|
63
|
+
statusBar.showMessage(statusText, "info");
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const statusBar = createStatusBar(screen, theme, {
|
|
68
|
+
onFocusNext: () => moveFocus(1),
|
|
69
|
+
onSelectOrSubmit: () => {
|
|
70
|
+
const focused = detectFocus();
|
|
71
|
+
if (focused === "input") {
|
|
72
|
+
inputBox.textarea.emit("key enter");
|
|
73
|
+
} else if (focused === "chatList") {
|
|
74
|
+
chatList.list.emit("key enter");
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
onFilterTabs: () => {
|
|
78
|
+
releaseInputs();
|
|
79
|
+
chatList.focus();
|
|
80
|
+
statusBar.showMessage("Фильтры: нажмите 1-6 для выбора вкладки", "info");
|
|
81
|
+
},
|
|
82
|
+
onSearch: () => {
|
|
83
|
+
chatList.searchBox.setValue("");
|
|
84
|
+
chatList.searchBox.focus();
|
|
85
|
+
screen.render();
|
|
86
|
+
},
|
|
87
|
+
onHelp: () => {
|
|
88
|
+
releaseInputs();
|
|
89
|
+
helpModal.show();
|
|
90
|
+
},
|
|
91
|
+
onAction: () => {
|
|
92
|
+
if (!state.activeChat) {
|
|
93
|
+
statusBar.showMessage("Сначала выберите чат слева!", "warning");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const msgs = state.getMessages(state.activeChat.id);
|
|
97
|
+
if (msgs.length > 0) {
|
|
98
|
+
releaseInputs();
|
|
99
|
+
actionModal.show(msgs[msgs.length - 1]);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
onChatInfo: () => {
|
|
103
|
+
if (state.activeChat) {
|
|
104
|
+
releaseInputs();
|
|
105
|
+
chatInfoModal.show(state.activeChat);
|
|
106
|
+
} else {
|
|
107
|
+
statusBar.showMessage("Сначала выберите чат слева!", "warning");
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
onQuit: () => {
|
|
111
|
+
releaseInputs();
|
|
112
|
+
confirmModal.ask("Выйти из TuiGram?", () => {
|
|
113
|
+
listener?.stop();
|
|
114
|
+
screen.destroy();
|
|
115
|
+
process.exit(0);
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
});
|
|
48
119
|
|
|
49
120
|
// Модальные окна
|
|
50
121
|
const helpModal = createHelpModal(screen, theme);
|
|
@@ -128,6 +199,7 @@ export async function startTui(client, me) {
|
|
|
128
199
|
state.setMessages(dialog.id, history.messages);
|
|
129
200
|
markAsRead(client, dialog.peerId).catch(() => {});
|
|
130
201
|
statusBar.showMessage(`Чат: ${dialog.title}`, "info");
|
|
202
|
+
fetchMissingImagePreviews(history.messages, dialog.id);
|
|
131
203
|
} catch (err) {
|
|
132
204
|
statusBar.showMessage(`Ошибка загрузки: ${err.message}`, "error");
|
|
133
205
|
}
|
|
@@ -157,6 +229,7 @@ export async function startTui(client, me) {
|
|
|
157
229
|
if (older.messages.length > 0) {
|
|
158
230
|
state.setMessages(state.activeChat.id, older.messages, true);
|
|
159
231
|
statusBar.showMessage(`Загружено ${older.messages.length} предыдущих сообщений`, "info");
|
|
232
|
+
fetchMissingImagePreviews(older.messages, state.activeChat.id);
|
|
160
233
|
}
|
|
161
234
|
} catch (err) {
|
|
162
235
|
statusBar.showMessage(`Ошибка пагинации: ${err.message}`, "error");
|
|
@@ -203,6 +276,21 @@ export async function startTui(client, me) {
|
|
|
203
276
|
onCancelContext: () => {
|
|
204
277
|
statusBar.showMessage("Режим ответа/редактирования сброшен", "info", 2000);
|
|
205
278
|
},
|
|
279
|
+
onReplyLast: () => {
|
|
280
|
+
if (!state.activeChat) return;
|
|
281
|
+
const msgs = state.getMessages(state.activeChat.id);
|
|
282
|
+
if (msgs.length > 0) {
|
|
283
|
+
inputBox.setContext("reply", msgs[msgs.length - 1]);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
onEditLast: () => {
|
|
287
|
+
if (!state.activeChat) return;
|
|
288
|
+
const msgs = state.getMessages(state.activeChat.id);
|
|
289
|
+
const ownMsgs = msgs.filter((m) => m.out);
|
|
290
|
+
if (ownMsgs.length > 0) {
|
|
291
|
+
inputBox.setContext("edit", ownMsgs[ownMsgs.length - 1]);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
206
294
|
onSlashCommand: (cmd, args) => {
|
|
207
295
|
switch (cmd) {
|
|
208
296
|
case "help":
|
|
@@ -267,6 +355,33 @@ export async function startTui(client, me) {
|
|
|
267
355
|
},
|
|
268
356
|
});
|
|
269
357
|
|
|
358
|
+
// Переключение фокуса по клику мышью на любую из трех панелей
|
|
359
|
+
chatList.container.on("click", () => {
|
|
360
|
+
if (screen.focused !== chatList.list && screen.focused !== chatList.searchBox) {
|
|
361
|
+
releaseInputs();
|
|
362
|
+
chatList.focus();
|
|
363
|
+
statusBar.showMessage("Фокус: список чатов", "info", 2000);
|
|
364
|
+
screen.render();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
chatView.container.on("click", () => {
|
|
369
|
+
if (screen.focused !== chatView.scrollBox) {
|
|
370
|
+
releaseInputs();
|
|
371
|
+
chatView.focus();
|
|
372
|
+
statusBar.showMessage("Фокус: лента сообщений", "info", 2000);
|
|
373
|
+
screen.render();
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
inputBox.container.on("click", () => {
|
|
378
|
+
if (screen.focused !== inputBox.textarea) {
|
|
379
|
+
inputBox.focus();
|
|
380
|
+
statusBar.showMessage("Фокус: поле ввода", "info", 2000);
|
|
381
|
+
screen.render();
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
270
385
|
// 5. Реакция на события состояния
|
|
271
386
|
state.on("dialogs_updated", (dialogs) => {
|
|
272
387
|
chatList.setDialogs(dialogs);
|
|
@@ -291,9 +406,13 @@ export async function startTui(client, me) {
|
|
|
291
406
|
chatView.setMessages(msgs);
|
|
292
407
|
});
|
|
293
408
|
|
|
294
|
-
state.on("messages_updated", ({ chatId, messages, isPrepend }) => {
|
|
409
|
+
state.on("messages_updated", ({ chatId, messages, isPrepend, isUpdate, isNewMessage }) => {
|
|
295
410
|
if (state.activeChat?.id === chatId) {
|
|
296
|
-
|
|
411
|
+
// Скроллим в самый низ только если это новое сообщение или первая загрузка чата.
|
|
412
|
+
// При подгрузке старых сообщений (isPrepend) или фоновых обновлениях (isUpdate)
|
|
413
|
+
// позиция скролла сохраняется!
|
|
414
|
+
const shouldScrollToBottom = isNewMessage ? config.autoScroll : (!isPrepend && !isUpdate);
|
|
415
|
+
chatView.setMessages(messages, shouldScrollToBottom);
|
|
297
416
|
}
|
|
298
417
|
});
|
|
299
418
|
|
|
@@ -316,11 +435,41 @@ export async function startTui(client, me) {
|
|
|
316
435
|
}
|
|
317
436
|
});
|
|
318
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Фоново догружает превью изображений для сообщений, не имевших встроенного PhotoStrippedSize.
|
|
440
|
+
* @param {Array<object>} messages
|
|
441
|
+
* @param {string} chatId
|
|
442
|
+
*/
|
|
443
|
+
async function fetchMissingImagePreviews(messages, chatId) {
|
|
444
|
+
if (!config.showImages || !messages || messages.length === 0) return;
|
|
445
|
+
for (const msg of messages) {
|
|
446
|
+
if (state.activeChat?.id !== chatId) break;
|
|
447
|
+
if (msg.media && !msg.imagePreview && msg.rawMessage) {
|
|
448
|
+
const isPhoto = msg.media.className === "MessageMediaPhoto";
|
|
449
|
+
const isDoc = msg.media.className === "MessageMediaDocument";
|
|
450
|
+
if (isPhoto || isDoc) {
|
|
451
|
+
try {
|
|
452
|
+
const preview = await loadMessageImagePreview(client, msg.rawMessage);
|
|
453
|
+
if (preview && state.activeChat?.id === chatId) {
|
|
454
|
+
msg.imagePreview = preview;
|
|
455
|
+
state.updateMessage(chatId, msg);
|
|
456
|
+
}
|
|
457
|
+
} catch {
|
|
458
|
+
// Игнорируем сетевые ошибки фоновой загрузки превью
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
319
465
|
// 6. Подключение фонового слушателя MTProto событий
|
|
320
466
|
listener = startTelegramListener(client);
|
|
321
467
|
|
|
322
468
|
listener.on("new_message", ({ peerId, message }) => {
|
|
323
469
|
state.addMessage(peerId, message);
|
|
470
|
+
if (!message.imagePreview && (message.media?.className === "MessageMediaPhoto" || message.media?.className === "MessageMediaDocument")) {
|
|
471
|
+
fetchMissingImagePreviews([message], peerId);
|
|
472
|
+
}
|
|
324
473
|
|
|
325
474
|
if (state.activeChat?.id === peerId) {
|
|
326
475
|
markAsRead(client, state.activeChat.peerId).catch(() => {});
|
|
@@ -465,6 +614,21 @@ export async function startTui(client, me) {
|
|
|
465
614
|
screen.key(["tab"], () => moveFocus(1));
|
|
466
615
|
screen.key(["S-tab"], () => moveFocus(-1));
|
|
467
616
|
|
|
617
|
+
screen.key(["pageup", "C-u"], () => {
|
|
618
|
+
if (!state.activeChat) return;
|
|
619
|
+
chatView.scrollBox.scroll(-10);
|
|
620
|
+
if (chatView.scrollBox.getScroll() <= 0) {
|
|
621
|
+
chatView.loadMore?.();
|
|
622
|
+
}
|
|
623
|
+
screen.render();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
screen.key(["pagedown", "C-d"], () => {
|
|
627
|
+
if (!state.activeChat) return;
|
|
628
|
+
chatView.scrollBox.scroll(10);
|
|
629
|
+
screen.render();
|
|
630
|
+
});
|
|
631
|
+
|
|
468
632
|
screen.key(["f1", "?"], () => {
|
|
469
633
|
releaseInputs();
|
|
470
634
|
helpModal.show();
|