@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 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.1.0...HEAD
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.1.0...HEAD
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.1.0",
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
  /**
@@ -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
- const statusBar = createStatusBar(screen, theme);
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
- chatView.setMessages(messages, !isPrepend);
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();