@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 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.0...HEAD
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.0...HEAD
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 ("… is typing");
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emaxe/tuigram",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Полнофункциональный TUI & CLI клиент Telegram на Node.js на базе MTProto (teleproto)",
5
5
  "type": "module",
6
6
  "exports": {
@@ -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
  }
@@ -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 (!id || !entity) return;
110
- const key = idToString(id);
111
- this.cache.set(key, entity);
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
- this.cache.set(`@${entity.username.toLowerCase()}`, entity);
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 (!idOrUsername) return null;
119
- const key = String(idOrUsername).toLowerCase();
120
- return this.cache.get(key) || this.cache.get(idToString(idOrUsername)) || null;
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) {
@@ -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
- try {
45
- const sender = await event.getSender?.();
46
- if (sender) {
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
- const normalized = normalizeMessage(msg);
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,
@@ -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
- ? (photo?.id ? `photo_${photo.id}` : (doc?.id ? `doc_${doc.id}` : `msg_${rawMessage.id}`))
37
- : undefined;
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
- let senderName = message.out ? "Вы" : "Собеседник";
58
- const cachedSender = entityCache.get(fromId);
59
- if (cachedSender) {
60
- senderName = getEntityDisplayName(cachedSender);
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
- messages.push(normalizeMessage(msg));
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
- state.setTyping(chatId, "Собеседник");
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,