@emaxe/tuigram 1.5.0 → 1.5.1
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/CHANGELOG.md +12 -1
- package/CHANGELOG.ru.md +12 -1
- package/README.md +1 -1
- package/README.ru.md +1 -1
- package/package.json +1 -1
- package/src/cli/formatters.js +2 -2
- package/src/telegram/entities.js +62 -7
- package/src/telegram/listener.js +57 -11
- package/src/telegram/messages.js +103 -12
- package/src/ui/app.js +11 -2
- package/src/ui/components/chatView.js +2 -2
- package/src/ui/components/inputBox.js +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -13,6 +13,16 @@ 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.5.1] — 2026-09-03
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- Message sender attribution across TUI and CLI: channel posts now display the channel title (along with author signature if present) instead of generic "Собеседник" or marking owner posts as "Вы ✓✓".
|
|
21
|
+
- Sender names in direct 1-on-1 chats and groups are now properly resolved and cached from Telegram entities instead of falling back to "Собеседник".
|
|
22
|
+
- Live typing indicator in the active chat now displays the typing user's display name instead of generic "Собеседник".
|
|
23
|
+
- Contextual reply preview bar in the input box displays the channel name when replying to channel posts.
|
|
24
|
+
- Resolution of Telegram Peer objects and channel IDs in `entityCache` and `parsePeer`.
|
|
25
|
+
|
|
16
26
|
## [1.5.0] — 2026-09-02
|
|
17
27
|
|
|
18
28
|
### Added
|
|
@@ -195,7 +205,8 @@ First public release.
|
|
|
195
205
|
- A WCAG contrast test (3:1 threshold) applied to every theme **after** conversion
|
|
196
206
|
to xterm-256 — the colors are checked exactly as the user sees them.
|
|
197
207
|
|
|
198
|
-
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.5.
|
|
208
|
+
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.5.1...HEAD
|
|
209
|
+
[1.5.1]: https://github.com/emaxe/tuigram/compare/v1.5.0...v1.5.1
|
|
199
210
|
[1.5.0]: https://github.com/emaxe/tuigram/compare/v1.4.0...v1.5.0
|
|
200
211
|
[1.4.0]: https://github.com/emaxe/tuigram/compare/v1.3.0...v1.4.0
|
|
201
212
|
[1.3.0]: https://github.com/emaxe/tuigram/compare/v1.2.0...v1.3.0
|
package/CHANGELOG.ru.md
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
|
|
14
14
|
- Английская локализация строк интерфейса TUI и CLI.
|
|
15
15
|
|
|
16
|
+
## [1.5.1] — 2026-09-03
|
|
17
|
+
|
|
18
|
+
### Исправлено
|
|
19
|
+
|
|
20
|
+
- Отображение авторов сообщений в TUI и CLI: посты в каналах теперь выводятся от имени канала (с подписью автора, если она включена в канале) вместо подстановки «Собеседник» или ошибочной отметки «Вы ✓✓» для постов владельца канала.
|
|
21
|
+
- Определение и кэширование имён отправителей в личных диалогах и группах: при входящих сообщениях и загрузке истории имена резолвятся из сущностей Telegram, предотвращая фоллбэк на «Собеседник».
|
|
22
|
+
- Индикатор набора текста в активном чате теперь отображает имя конкретного пользователя (например, «Sam печатает...») вместо обобщённого «Собеседник».
|
|
23
|
+
- В строке контекста ответа (Reply) в поле ввода для сообщений каналов отображается название канала вместо «Собеседник».
|
|
24
|
+
- Улучшена работа кэша сущностей (`entityCache`) с поддержкой маркированных и числовых ID, юзернеймов с префиксом `@` и без него, а также объектов Peer в `parsePeer`.
|
|
25
|
+
|
|
16
26
|
## [1.5.0] — 2026-09-02
|
|
17
27
|
|
|
18
28
|
### Добавлено
|
|
@@ -201,7 +211,8 @@
|
|
|
201
211
|
- Тест контраста по WCAG (порог 3:1) для каждой темы — **после** конверсии в xterm-256,
|
|
202
212
|
то есть ровно в том виде, в каком цвет увидит пользователь.
|
|
203
213
|
|
|
204
|
-
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.5.
|
|
214
|
+
[Unreleased]: https://github.com/emaxe/tuigram/compare/v1.5.1...HEAD
|
|
215
|
+
[1.5.1]: https://github.com/emaxe/tuigram/compare/v1.5.0...v1.5.1
|
|
205
216
|
[1.5.0]: https://github.com/emaxe/tuigram/compare/v1.4.0...v1.5.0
|
|
206
217
|
[1.4.0]: https://github.com/emaxe/tuigram/compare/v1.3.0...v1.4.0
|
|
207
218
|
[1.3.0]: https://github.com/emaxe/tuigram/compare/v1.2.0...v1.3.0
|
package/README.md
CHANGED
|
@@ -76,7 +76,7 @@ TuiGram lets you use Telegram entirely from the terminal: browse your dialog lis
|
|
|
76
76
|
menu, clicking an image opens it full screen, clicking a video plays it, wheel scrolling);
|
|
77
77
|
- Chat categories: All, Direct messages, Groups, Channels, Bots, Unread;
|
|
78
78
|
- Instant search and filtering of chats by title and `@username` (`/`);
|
|
79
|
-
- Live typing indicator
|
|
79
|
+
- Live typing indicator with sender display name in real time;
|
|
80
80
|
- Colored rendering of Telegram entities (bold, italic, monospace code, URLs, mentions, spoilers);
|
|
81
81
|
- High-resolution inline image previews (Unicode Half-Block `▀` pixel art);
|
|
82
82
|
- Terminal video and video note playback (Unicode Half-Block + synchronized audio);
|
package/README.ru.md
CHANGED
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
правый — меню действий, клик по картинке — просмотр на весь экран, клик по видео — воспроизведение, колесо — прокрутка);
|
|
73
73
|
- Категории чатов: Все, Личные (ЛС), Группы, Каналы, Боты, Непрочитанные;
|
|
74
74
|
- Мгновенный поиск и фильтрация чатов по названию и `@username` (`/`);
|
|
75
|
-
- Отображение статуса набора текста
|
|
75
|
+
- Отображение статуса набора текста с именем собеседника в реальном времени;
|
|
76
76
|
- Цветовое форматирование Telegram Entities (Bold, Italic, Monospace Code, URLs, Mentions, Spoilers);
|
|
77
77
|
- Цветные превью изображений высокого разрешения прямо в чате (Unicode Half-Block `▀` pixel art);
|
|
78
78
|
- Воспроизведение видео и видеозаметок в псевдографике (Unicode Half-Block + синхронный звук);
|
package/package.json
CHANGED
package/src/cli/formatters.js
CHANGED
|
@@ -30,7 +30,7 @@ export function formatDialogRow(d) {
|
|
|
30
30
|
export function formatHistoryMessage(m) {
|
|
31
31
|
const time = gray(`[${formatFullDateTime(m.date)}]`);
|
|
32
32
|
const id = gray(`#${m.id}`);
|
|
33
|
-
const author = m.out ? green(bold("Вы:")) : cyan(bold(`${m.senderName || "Собеседник"}:`));
|
|
33
|
+
const author = (m.out && !m.post) ? green(bold("Вы:")) : cyan(bold(`${m.senderName || (m.post ? "Канал" : "Собеседник")}:`));
|
|
34
34
|
const text = m.text || (m.mediaDescription ? m.mediaDescription.replace(/\{[a-z0-9\-]+\}/gi, "") : "");
|
|
35
35
|
const reply = m.replyToMsgId ? gray(` (в ответ на #${m.replyToMsgId})`) : "";
|
|
36
36
|
const edited = m.editDate ? gray(" (изменено)") : "";
|
|
@@ -50,7 +50,7 @@ export function formatStreamEvent(kind, data) {
|
|
|
50
50
|
case "new_message": {
|
|
51
51
|
const m = data.message;
|
|
52
52
|
const peer = yellow(`[${data.peerId}]`);
|
|
53
|
-
const author = m.out ? green("Вы") : cyan(m.senderName || "Собеседник");
|
|
53
|
+
const author = (m.out && !m.post) ? green("Вы") : cyan(m.senderName || (m.post ? "Канал" : "Собеседник"));
|
|
54
54
|
const text = m.text || "[медиа]";
|
|
55
55
|
return `${time} ${green("+ НОВОЕ")} ${peer} ${author}: ${text}`;
|
|
56
56
|
}
|
package/src/telegram/entities.js
CHANGED
|
@@ -49,6 +49,12 @@ export function toMarkedId(peer) {
|
|
|
49
49
|
* @returns {string|bigint}
|
|
50
50
|
*/
|
|
51
51
|
export function parsePeer(raw) {
|
|
52
|
+
if (raw && typeof raw === "object") {
|
|
53
|
+
const marked = toMarkedId(raw);
|
|
54
|
+
if (marked && marked !== "[object Object]") {
|
|
55
|
+
return parsePeer(marked);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
52
58
|
const value = String(raw || "").trim();
|
|
53
59
|
if (!value) throw new Error("Не указан идентификатор чата (peer)");
|
|
54
60
|
if (value === "me" || value === "self") return "me";
|
|
@@ -106,18 +112,67 @@ class EntityCache {
|
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
set(id, entity) {
|
|
109
|
-
if (!
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
if (!entity) return;
|
|
116
|
+
if (id !== null && id !== undefined) {
|
|
117
|
+
const key = idToString(id).trim().toLowerCase();
|
|
118
|
+
if (key) this.cache.set(key, entity);
|
|
119
|
+
}
|
|
120
|
+
if (entity.id !== null && entity.id !== undefined) {
|
|
121
|
+
const rawId = idToString(entity.id).trim().toLowerCase();
|
|
122
|
+
if (rawId) {
|
|
123
|
+
this.cache.set(rawId, entity);
|
|
124
|
+
if (entity.className === "Channel" || entity.broadcast || entity.megagroup) {
|
|
125
|
+
this.cache.set(`-100${rawId}`, entity);
|
|
126
|
+
} else if (entity.className === "Chat") {
|
|
127
|
+
this.cache.set(`-${rawId}`, entity);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const marked = toMarkedId(entity);
|
|
132
|
+
if (marked && marked !== "[object Object]") {
|
|
133
|
+
this.cache.set(marked.toLowerCase(), entity);
|
|
134
|
+
}
|
|
112
135
|
if (entity.username) {
|
|
113
|
-
|
|
136
|
+
const u = entity.username.toLowerCase();
|
|
137
|
+
this.cache.set(`@${u}`, entity);
|
|
138
|
+
this.cache.set(u, entity);
|
|
114
139
|
}
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
get(idOrUsername) {
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
143
|
+
if (idOrUsername === null || idOrUsername === undefined) return null;
|
|
144
|
+
let key = "";
|
|
145
|
+
if (typeof idOrUsername === "object") {
|
|
146
|
+
const marked = toMarkedId(idOrUsername);
|
|
147
|
+
key = (marked && marked !== "[object Object]" ? marked : idToString(idOrUsername)).trim().toLowerCase();
|
|
148
|
+
} else {
|
|
149
|
+
key = String(idOrUsername).trim().toLowerCase();
|
|
150
|
+
}
|
|
151
|
+
if (!key) return null;
|
|
152
|
+
|
|
153
|
+
let found = this.cache.get(key);
|
|
154
|
+
if (found) return found;
|
|
155
|
+
|
|
156
|
+
if (key.startsWith("-100")) {
|
|
157
|
+
found = this.cache.get(key.slice(4));
|
|
158
|
+
if (found) return found;
|
|
159
|
+
} else if (key.startsWith("-")) {
|
|
160
|
+
found = this.cache.get(key.slice(1));
|
|
161
|
+
if (found) return found;
|
|
162
|
+
} else if (/^\d+$/.test(key)) {
|
|
163
|
+
found = this.cache.get(`-100${key}`) || this.cache.get(`-${key}`);
|
|
164
|
+
if (found) return found;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (key.startsWith("@")) {
|
|
168
|
+
found = this.cache.get(key.slice(1));
|
|
169
|
+
if (found) return found;
|
|
170
|
+
} else {
|
|
171
|
+
found = this.cache.get(`@${key}`);
|
|
172
|
+
if (found) return found;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return null;
|
|
121
176
|
}
|
|
122
177
|
|
|
123
178
|
has(idOrUsername) {
|
package/src/telegram/listener.js
CHANGED
|
@@ -2,7 +2,7 @@ import { EventEmitter } from "node:events";
|
|
|
2
2
|
import { Api } from "teleproto";
|
|
3
3
|
import { NewMessage, EditedMessage, DeletedMessage, Raw } from "teleproto/events/index.js";
|
|
4
4
|
import { normalizeMessage } from "./messages.js";
|
|
5
|
-
import { idToString, toMarkedId, entityCache } from "./entities.js";
|
|
5
|
+
import { idToString, toMarkedId, entityCache, getEntityDisplayName } from "./entities.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Приводит идентификатор чата из сырого апдейта к маркированному виду (как dialog.id).
|
|
@@ -36,21 +36,39 @@ export function startTelegramListener(client) {
|
|
|
36
36
|
try {
|
|
37
37
|
const msg = event.message;
|
|
38
38
|
if (!msg) return;
|
|
39
|
-
const normalized = normalizeMessage(msg);
|
|
40
|
-
const peerId = normalized.peerId;
|
|
41
|
-
const fromId = normalized.fromId;
|
|
42
39
|
|
|
43
|
-
// Кэшируем
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
entityCache.set(sender.id, sender);
|
|
48
|
-
normalized.senderName = sender.title || [sender.firstName, sender.lastName].filter(Boolean).join(" ") || normalized.senderName;
|
|
40
|
+
// Кэшируем сущности события, если они пришли в апдейте
|
|
41
|
+
if (event.originalUpdate?._entities && typeof event.originalUpdate._entities.values === "function") {
|
|
42
|
+
for (const ent of event.originalUpdate._entities.values()) {
|
|
43
|
+
if (ent?.id) entityCache.set(ent.id, ent);
|
|
49
44
|
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let sender = null;
|
|
48
|
+
try {
|
|
49
|
+
sender = await event.getSender?.();
|
|
50
|
+
if (sender) entityCache.set(sender.id, sender);
|
|
51
|
+
} catch {
|
|
52
|
+
// Игнорируем
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let chat = null;
|
|
56
|
+
try {
|
|
57
|
+
chat = await event.getChat?.();
|
|
58
|
+
if (chat) entityCache.set(chat.id, chat);
|
|
50
59
|
} catch {
|
|
51
60
|
// Игнорируем
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
const normalized = normalizeMessage(msg, chat || sender);
|
|
64
|
+
if (sender && (!normalized.senderName || normalized.senderName === "Собеседник")) {
|
|
65
|
+
const baseName = getEntityDisplayName(sender);
|
|
66
|
+
normalized.senderName = msg.postAuthor ? `${baseName} (${msg.postAuthor})` : baseName;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const peerId = normalized.peerId;
|
|
70
|
+
const fromId = normalized.fromId;
|
|
71
|
+
|
|
54
72
|
bus.emit("new_message", {
|
|
55
73
|
peerId,
|
|
56
74
|
fromId,
|
|
@@ -67,7 +85,35 @@ export function startTelegramListener(client) {
|
|
|
67
85
|
try {
|
|
68
86
|
const msg = event.message;
|
|
69
87
|
if (!msg) return;
|
|
70
|
-
|
|
88
|
+
|
|
89
|
+
if (event.originalUpdate?._entities && typeof event.originalUpdate._entities.values === "function") {
|
|
90
|
+
for (const ent of event.originalUpdate._entities.values()) {
|
|
91
|
+
if (ent?.id) entityCache.set(ent.id, ent);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let sender = null;
|
|
96
|
+
try {
|
|
97
|
+
sender = await event.getSender?.();
|
|
98
|
+
if (sender) entityCache.set(sender.id, sender);
|
|
99
|
+
} catch {
|
|
100
|
+
// Игнорируем
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let chat = null;
|
|
104
|
+
try {
|
|
105
|
+
chat = await event.getChat?.();
|
|
106
|
+
if (chat) entityCache.set(chat.id, chat);
|
|
107
|
+
} catch {
|
|
108
|
+
// Игнорируем
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const normalized = normalizeMessage(msg, chat || sender);
|
|
112
|
+
if (sender && (!normalized.senderName || normalized.senderName === "Собеседник")) {
|
|
113
|
+
const baseName = getEntityDisplayName(sender);
|
|
114
|
+
normalized.senderName = msg.postAuthor ? `${baseName} (${msg.postAuthor})` : baseName;
|
|
115
|
+
}
|
|
116
|
+
|
|
71
117
|
bus.emit("edited_message", {
|
|
72
118
|
peerId: normalized.peerId,
|
|
73
119
|
message: normalized,
|
package/src/telegram/messages.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Api, errors } from "teleproto";
|
|
2
|
-
import { idToString, toMarkedId, getEntityDisplayName, entityCache, resolveEntity } from "./entities.js";
|
|
2
|
+
import { idToString, toMarkedId, detectChatType, getEntityDisplayName, entityCache, resolveEntity } from "./entities.js";
|
|
3
3
|
import { describeMedia } from "./formatter.js";
|
|
4
4
|
import { config } from "../config.js";
|
|
5
5
|
import { renderStrippedThumbnail, renderImageBuffer, renderMediaPreloader, getCachedImagePreview, isPreviewableMedia } from "../utils/image.js";
|
|
@@ -33,31 +33,77 @@ export function renderMessageThumbnail(rawMessage, {
|
|
|
33
33
|
|
|
34
34
|
// Полноэкранные рендеры не кэшируем: одна такая строка весит сотни килобайт
|
|
35
35
|
const cacheKey = useCache
|
|
36
|
-
? (
|
|
37
|
-
:
|
|
36
|
+
? (rawMessage.id ? `thumb_${rawMessage.id}@${maxWidth}x${maxHeight}` : null)
|
|
37
|
+
: null;
|
|
38
38
|
|
|
39
|
-
return renderStrippedThumbnail(stripped.bytes, { maxWidth, maxHeight, cacheKey })
|
|
39
|
+
return renderStrippedThumbnail(stripped.bytes, { maxWidth, maxHeight, cacheKey });
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Преобразует объект Message из MTProto в нормализованный объект для TUI.
|
|
44
44
|
* @param {object} message
|
|
45
|
+
* @param {object|null} [chatEntity=null] сущность чата (канал, пользователь, группа)
|
|
45
46
|
* @returns {object}
|
|
46
47
|
*/
|
|
47
|
-
export function normalizeMessage(message) {
|
|
48
|
+
export function normalizeMessage(message, chatEntity = null) {
|
|
48
49
|
if (!message) return null;
|
|
49
50
|
|
|
50
51
|
// fromId остаётся немаркированным: по нему ищется сущность в entityCache,
|
|
51
52
|
// куда объекты кладутся по entity.id (тоже без маркера).
|
|
52
|
-
const fromId = idToString(message.fromId?.userId || message.fromId?.channelId || message.fromId?.chatId);
|
|
53
|
+
const fromId = idToString(message.fromId?.userId || message.fromId?.channelId || message.fromId?.chatId || message.senderId);
|
|
53
54
|
// peerId маркируется, чтобы совпадать с dialog.id (см. toMarkedId).
|
|
54
55
|
const peerId = toMarkedId(message.peerId);
|
|
55
56
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
const postAuthor = message.postAuthor ? String(message.postAuthor).trim() : null;
|
|
58
|
+
const isPost = Boolean(
|
|
59
|
+
message.post ||
|
|
60
|
+
chatEntity?.broadcast ||
|
|
61
|
+
(chatEntity && detectChatType({ entity: chatEntity }) === "channel")
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// Ищем сущность отправителя:
|
|
65
|
+
// 1. Уже прикреплённый teleproto sender
|
|
66
|
+
// 2. Вложенный кэш сущностей сообщения message._entities
|
|
67
|
+
// 3. Глобальный entityCache
|
|
68
|
+
let senderEntity = message.sender || null;
|
|
69
|
+
if (!senderEntity && message._entities && typeof message._entities.get === "function") {
|
|
70
|
+
senderEntity = (fromId && message._entities.get(fromId)) ||
|
|
71
|
+
(message.senderId && message._entities.get(idToString(message.senderId))) ||
|
|
72
|
+
(message.fromId && message._entities.get(toMarkedId(message.fromId))) ||
|
|
73
|
+
null;
|
|
74
|
+
}
|
|
75
|
+
if (!senderEntity) {
|
|
76
|
+
senderEntity = (fromId && entityCache.get(fromId)) ||
|
|
77
|
+
(message.senderId && entityCache.get(message.senderId)) ||
|
|
78
|
+
(message.fromId && entityCache.get(message.fromId)) ||
|
|
79
|
+
null;
|
|
80
|
+
}
|
|
81
|
+
if (senderEntity && senderEntity.id) {
|
|
82
|
+
entityCache.set(senderEntity.id, senderEntity);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let senderName = "";
|
|
86
|
+
if (isPost) {
|
|
87
|
+
// В вещательных каналах автор — сам канал (или подпись автора)
|
|
88
|
+
const channelEntity = senderEntity || chatEntity || message.chat || entityCache.get(peerId) || null;
|
|
89
|
+
const channelTitle = channelEntity ? getEntityDisplayName(channelEntity) : (chatEntity?.title || "");
|
|
90
|
+
if (postAuthor) {
|
|
91
|
+
senderName = channelTitle ? `${channelTitle} (${postAuthor})` : postAuthor;
|
|
92
|
+
} else {
|
|
93
|
+
senderName = channelTitle || "Канал";
|
|
94
|
+
}
|
|
95
|
+
} else if (message.out) {
|
|
96
|
+
senderName = "Вы";
|
|
97
|
+
} else if (senderEntity) {
|
|
98
|
+
const baseName = getEntityDisplayName(senderEntity);
|
|
99
|
+
senderName = postAuthor ? `${baseName} (${postAuthor})` : baseName;
|
|
100
|
+
} else if (chatEntity && (chatEntity.className === "User" || detectChatType({ entity: chatEntity }) === "user")) {
|
|
101
|
+
// В личном диалоге (1-на-1) входящее сообщение всегда от собеседника чата
|
|
102
|
+
senderName = getEntityDisplayName(chatEntity);
|
|
103
|
+
} else if (postAuthor) {
|
|
104
|
+
senderName = postAuthor;
|
|
105
|
+
} else {
|
|
106
|
+
senderName = "Собеседник";
|
|
61
107
|
}
|
|
62
108
|
|
|
63
109
|
// Реакции
|
|
@@ -105,6 +151,8 @@ export function normalizeMessage(message) {
|
|
|
105
151
|
date: message.date ? message.date * 1000 : Date.now(),
|
|
106
152
|
editDate: message.editDate ? message.editDate * 1000 : null,
|
|
107
153
|
out: Boolean(message.out),
|
|
154
|
+
post: isPost,
|
|
155
|
+
postAuthor,
|
|
108
156
|
text: message.message || "",
|
|
109
157
|
fromId,
|
|
110
158
|
senderName,
|
|
@@ -140,7 +188,15 @@ export async function fetchHistory(client, rawPeer, { limit = 40, offsetId = 0,
|
|
|
140
188
|
const load = async () => {
|
|
141
189
|
for await (const msg of client.iterMessages(entity, { limit, offsetId, reverse })) {
|
|
142
190
|
if (msg && msg.className !== "MessageEmpty") {
|
|
143
|
-
|
|
191
|
+
// Сохраняем сущности, которые Telegram прислал вместе с сообщениями
|
|
192
|
+
if (msg._entities && typeof msg._entities.values === "function") {
|
|
193
|
+
for (const ent of msg._entities.values()) {
|
|
194
|
+
if (ent?.id) {
|
|
195
|
+
entityCache.set(ent.id, ent);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
messages.push(normalizeMessage(msg, entity));
|
|
144
200
|
}
|
|
145
201
|
}
|
|
146
202
|
if (!reverse) {
|
|
@@ -161,6 +217,41 @@ export async function fetchHistory(client, rawPeer, { limit = 40, offsetId = 0,
|
|
|
161
217
|
}
|
|
162
218
|
}
|
|
163
219
|
|
|
220
|
+
// Резолвим отправителей, которых ещё нет в кэше сущностей,
|
|
221
|
+
// чтобы в диалоге отображались реальные имена, а не «Собеседник».
|
|
222
|
+
for (const msg of messages) {
|
|
223
|
+
if (!msg.out && (!msg.senderName || msg.senderName === "Собеседник")) {
|
|
224
|
+
// 1. Попытка через rawMessage.getSender()
|
|
225
|
+
if (msg.rawMessage?.getSender) {
|
|
226
|
+
try {
|
|
227
|
+
const sender = await msg.rawMessage.getSender();
|
|
228
|
+
if (sender) {
|
|
229
|
+
entityCache.set(sender.id, sender);
|
|
230
|
+
const baseName = getEntityDisplayName(sender);
|
|
231
|
+
msg.senderName = msg.postAuthor ? `${baseName} (${msg.postAuthor})` : baseName;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Игнорируем
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2. Попытка через resolveEntity по targetPeer
|
|
240
|
+
const targetPeer = msg.rawMessage?.fromId || msg.fromId || msg.rawMessage?.peerId || msg.peerId;
|
|
241
|
+
if (targetPeer) {
|
|
242
|
+
try {
|
|
243
|
+
const sender = await resolveEntity(client, targetPeer);
|
|
244
|
+
if (sender) {
|
|
245
|
+
const baseName = getEntityDisplayName(sender);
|
|
246
|
+
msg.senderName = msg.postAuthor ? `${baseName} (${msg.postAuthor})` : baseName;
|
|
247
|
+
}
|
|
248
|
+
} catch {
|
|
249
|
+
// Имя не резолвится — останется fallback
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
164
255
|
return {
|
|
165
256
|
peer: entity,
|
|
166
257
|
messages,
|
package/src/ui/app.js
CHANGED
|
@@ -16,6 +16,7 @@ import { createImageViewerModal } from "./components/modals/imageViewerModal.js"
|
|
|
16
16
|
import { createVideoPlayerModal } from "./components/modals/videoPlayerModal.js";
|
|
17
17
|
import { state } from "../state.js";
|
|
18
18
|
import { config } from "../config.js";
|
|
19
|
+
import { entityCache, getEntityDisplayName, resolveEntity } from "../telegram/entities.js";
|
|
19
20
|
import { fetchDialogs } from "../telegram/dialogs.js";
|
|
20
21
|
import { setMessagePalette } from "../telegram/formatter.js";
|
|
21
22
|
import { fetchHistory, sendMessage, editMessage, deleteMessages, sendFiles, downloadMedia, sendReaction, markAsRead, loadMessageImagePreview, downloadImageBuffer, renderMessageThumbnail, findFirstUnreadMessage, calculateRemainingUnreadCount } from "../telegram/messages.js";
|
|
@@ -668,8 +669,16 @@ export async function startTui(client, me) {
|
|
|
668
669
|
state.removeMessages(peerId, deletedIds);
|
|
669
670
|
});
|
|
670
671
|
|
|
671
|
-
listener.on("typing", ({ chatId, userId }) => {
|
|
672
|
-
|
|
672
|
+
listener.on("typing", async ({ chatId, userId }) => {
|
|
673
|
+
let cached = entityCache.get(userId);
|
|
674
|
+
if (!cached && userId) {
|
|
675
|
+
try {
|
|
676
|
+
cached = await resolveEntity(client, userId);
|
|
677
|
+
} catch {
|
|
678
|
+
// Фоллбэк
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
state.setTyping(chatId, cached ? getEntityDisplayName(cached) : "Собеседник");
|
|
673
682
|
});
|
|
674
683
|
|
|
675
684
|
/**
|
|
@@ -123,11 +123,11 @@ export function createChatView(screen, theme, {
|
|
|
123
123
|
|
|
124
124
|
// Отправитель
|
|
125
125
|
let authorTag = "";
|
|
126
|
-
if (msg.out) {
|
|
126
|
+
if (msg.out && !msg.post) {
|
|
127
127
|
const readCheck = fg(theme.chatView.outgoingName, "✓✓");
|
|
128
128
|
authorTag = `${fg(theme.chatView.outgoingName, "{bold}Вы{/bold}")} ${timeTag} ${readCheck}`;
|
|
129
129
|
} else {
|
|
130
|
-
const name = escapeBlessed(msg.senderName || "Собеседник");
|
|
130
|
+
const name = escapeBlessed(msg.senderName || (msg.post ? "Канал" : "Собеседник"));
|
|
131
131
|
authorTag = `${fg(theme.chatView.incomingName, `{bold}${name}{/bold}`)} ${timeTag}`;
|
|
132
132
|
}
|
|
133
133
|
|
|
@@ -97,7 +97,7 @@ export function createInputBox(screen, theme, {
|
|
|
97
97
|
|
|
98
98
|
function renderContext() {
|
|
99
99
|
if (currentMode === "reply" && currentTarget) {
|
|
100
|
-
const author = escapeBlessed(currentTarget.senderName || "Собеседник");
|
|
100
|
+
const author = escapeBlessed(currentTarget.senderName || (currentTarget.post ? "Канал" : "Собеседник"));
|
|
101
101
|
const preview = escapeBlessed((currentTarget.text || "").slice(0, 30));
|
|
102
102
|
contextBar.setContent(
|
|
103
103
|
badge(theme.input.replyBg, theme.input.replyFg,
|