@emaxe/tuigram 1.3.0 → 1.4.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/CHANGELOG.md +43 -1
- package/CHANGELOG.ru.md +47 -1
- package/README.md +50 -14
- package/README.ru.md +51 -14
- package/package.json +1 -1
- package/src/telegram/messages.js +98 -19
- package/src/ui/app.js +95 -40
- package/src/ui/components/chatList.js +4 -2
- package/src/ui/components/chatView.js +225 -61
- package/src/ui/components/header.js +2 -1
- package/src/ui/components/inputBox.js +2 -1
- package/src/ui/components/modals/actionModal.js +6 -14
- package/src/ui/components/modals/chatInfoModal.js +3 -13
- package/src/ui/components/modals/confirmModal.js +3 -13
- package/src/ui/components/modals/fileModal.js +6 -13
- package/src/ui/components/modals/filePickerModal.js +3 -13
- package/src/ui/components/modals/helpModal.js +13 -18
- package/src/ui/components/modals/imageViewerModal.js +172 -0
- package/src/ui/components/statusBar.js +2 -1
- package/src/ui/modalMouse.js +43 -0
- package/src/ui/screen.js +60 -20
- package/src/utils/image.js +8 -8
- package/src/utils/mouse.js +61 -74
package/src/utils/image.js
CHANGED
|
@@ -294,8 +294,11 @@ export function rgbaToHalfBlockBlessed(data, width, height) {
|
|
|
294
294
|
* @returns {string}
|
|
295
295
|
*/
|
|
296
296
|
export function renderImageBuffer(buffer, { mimeType = "", maxWidth = 36, maxHeight = 14, cacheKey } = {}) {
|
|
297
|
-
|
|
298
|
-
|
|
297
|
+
// Размер входит в ключ: одно и то же фото рисуется и миниатюрой в ленте, и на весь
|
|
298
|
+
// экран в просмотрщике. Без размера полноэкранный рендер подменял бы превью в ленте.
|
|
299
|
+
const key = cacheKey ? `${cacheKey}@${maxWidth}x${maxHeight}` : null;
|
|
300
|
+
if (key && imagePreviewCache.has(key)) {
|
|
301
|
+
return imagePreviewCache.get(key);
|
|
299
302
|
}
|
|
300
303
|
|
|
301
304
|
try {
|
|
@@ -304,12 +307,12 @@ export function renderImageBuffer(buffer, { mimeType = "", maxWidth = 36, maxHei
|
|
|
304
307
|
const resized = resizeRgba(decoded.data, decoded.width, decoded.height, dstW, dstH);
|
|
305
308
|
const blessedText = rgbaToHalfBlockBlessed(resized, dstW, dstH);
|
|
306
309
|
|
|
307
|
-
if (
|
|
310
|
+
if (key) {
|
|
308
311
|
if (imagePreviewCache.size >= MAX_CACHE_SIZE) {
|
|
309
312
|
const firstKey = imagePreviewCache.keys().next().value;
|
|
310
313
|
imagePreviewCache.delete(firstKey);
|
|
311
314
|
}
|
|
312
|
-
imagePreviewCache.set(
|
|
315
|
+
imagePreviewCache.set(key, blessedText);
|
|
313
316
|
}
|
|
314
317
|
|
|
315
318
|
return blessedText;
|
|
@@ -330,10 +333,7 @@ export function renderImageBuffer(buffer, { mimeType = "", maxWidth = 36, maxHei
|
|
|
330
333
|
export function renderStrippedThumbnail(strippedBytes, { maxWidth = 36, maxHeight = 14, cacheKey } = {}) {
|
|
331
334
|
if (!strippedBytes || strippedBytes.length < 3) return "";
|
|
332
335
|
|
|
333
|
-
|
|
334
|
-
return imagePreviewCache.get(cacheKey);
|
|
335
|
-
}
|
|
336
|
-
|
|
336
|
+
// Кэш проверяет renderImageBuffer: только он знает итоговый ключ с размером
|
|
337
337
|
try {
|
|
338
338
|
const jpgBuf = strippedPhotoToJpg(strippedBytes);
|
|
339
339
|
return renderImageBuffer(jpgBuf, { mimeType: "image/jpeg", maxWidth, maxHeight, cacheKey });
|
package/src/utils/mouse.js
CHANGED
|
@@ -24,6 +24,17 @@ export function stringCellWidth(text) {
|
|
|
24
24
|
return width;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Отличает клик правой кнопкой от остальных.
|
|
29
|
+
* neo-blessed эмитит одно и то же событие "click" для любой кнопки мыши,
|
|
30
|
+
* поэтому компоненты, где правый клик не имеет смысла, обязаны его отсеивать.
|
|
31
|
+
* @param {{ button?: string }} [data] данные события мыши blessed
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
export function isRightClick(data) {
|
|
35
|
+
return data?.button === "right";
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
/**
|
|
28
39
|
* Проверяет, попадают ли координаты (x, y) внутрь прямоугольной области.
|
|
29
40
|
* @param {number} x абсолютная или относительная X-координата
|
|
@@ -64,82 +75,43 @@ export function getTabByCoordinate(relativeX, tabKeys = DEFAULT_TAB_KEYS, tabNam
|
|
|
64
75
|
}
|
|
65
76
|
|
|
66
77
|
/**
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
* @param {
|
|
70
|
-
* @
|
|
78
|
+
* Находит сообщение в ленте по номеру отображаемой строки с учётом текущей прокрутки.
|
|
79
|
+
* Карту строк строит ChatView при отрисовке ленты — только он знает реальную вёрстку.
|
|
80
|
+
* @param {number} lineIndex индекс строки в буфере ленты (0-based)
|
|
81
|
+
* @param {Array<{ message: object, startLine: number, endLine: number }>} ranges
|
|
82
|
+
* @returns {object|null}
|
|
71
83
|
*/
|
|
72
|
-
export function
|
|
73
|
-
if (!
|
|
74
|
-
|
|
75
|
-
const ranges = [];
|
|
76
|
-
let currentLine = 0;
|
|
77
|
-
let lastDateString = "";
|
|
78
|
-
|
|
79
|
-
for (const msg of messages) {
|
|
80
|
-
// 1. Проверяем разделитель дат (если дата изменилась)
|
|
81
|
-
const dateDivider = msg.date ? new Date(msg.date).toLocaleDateString("ru-RU") : "";
|
|
82
|
-
if (dateDivider && dateDivider !== lastDateString) {
|
|
83
|
-
// Форматтер добавляет: \n ─────── дата ─────── \n\n (3 строки)
|
|
84
|
-
currentLine += 3;
|
|
85
|
-
lastDateString = dateDivider;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const startLine = currentLine;
|
|
89
|
-
|
|
90
|
-
// 2. Строка автора/времени
|
|
91
|
-
currentLine += 1;
|
|
92
|
-
|
|
93
|
-
// 3. Блок ответа (Reply), если есть
|
|
94
|
-
if (msg.replyToMsgId) {
|
|
95
|
-
currentLine += 1;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// 4. Тело сообщения (текст + медиа-описание + превью)
|
|
99
|
-
let bodyLinesCount = 1;
|
|
100
|
-
const parts = [];
|
|
101
|
-
if (msg.mediaDescription) parts.push(msg.mediaDescription);
|
|
102
|
-
if (msg.imagePreview) parts.push(msg.imagePreview);
|
|
103
|
-
if (msg.text) parts.push(msg.text);
|
|
104
|
-
|
|
105
|
-
if (parts.length > 0) {
|
|
106
|
-
const fullBody = parts.join("\n");
|
|
107
|
-
bodyLinesCount = fullBody.split("\n").length;
|
|
108
|
-
}
|
|
109
|
-
currentLine += bodyLinesCount;
|
|
84
|
+
export function getMessageAtLine(lineIndex, ranges) {
|
|
85
|
+
if (!ranges || ranges.length === 0 || lineIndex < 0) return null;
|
|
110
86
|
|
|
111
|
-
|
|
112
|
-
if (
|
|
113
|
-
|
|
87
|
+
for (const item of ranges) {
|
|
88
|
+
if (lineIndex >= item.startLine && lineIndex <= item.endLine) {
|
|
89
|
+
return item.message;
|
|
114
90
|
}
|
|
115
|
-
|
|
116
|
-
// 6. Замыкающий отступ между сообщениями (\n\n)
|
|
117
|
-
const endLine = currentLine;
|
|
118
|
-
currentLine += 2;
|
|
119
|
-
|
|
120
|
-
ranges.push({
|
|
121
|
-
message: msg,
|
|
122
|
-
startLine,
|
|
123
|
-
endLine,
|
|
124
|
-
});
|
|
125
91
|
}
|
|
126
92
|
|
|
127
|
-
return
|
|
93
|
+
return null;
|
|
128
94
|
}
|
|
129
95
|
|
|
130
96
|
/**
|
|
131
|
-
*
|
|
97
|
+
* Определяет, в какую часть сообщения попал клик: в превью изображения или в остальной текст.
|
|
132
98
|
* @param {number} lineIndex индекс строки в буфере ленты (0-based)
|
|
133
|
-
* @param {
|
|
134
|
-
* @
|
|
99
|
+
* @param {number} relativeX смещение по X от левого края ленты (в ячейках)
|
|
100
|
+
* @param {Array<{ message: object, startLine: number, endLine: number, image?: object|null }>} ranges
|
|
101
|
+
* @returns {{ message: object, part: "image"|"body" }|null}
|
|
135
102
|
*/
|
|
136
|
-
export function
|
|
103
|
+
export function getMessagePartAtPoint(lineIndex, relativeX, ranges) {
|
|
137
104
|
if (!ranges || ranges.length === 0 || lineIndex < 0) return null;
|
|
138
105
|
|
|
139
106
|
for (const item of ranges) {
|
|
140
|
-
if (lineIndex
|
|
141
|
-
|
|
142
|
-
|
|
107
|
+
if (lineIndex < item.startLine || lineIndex > item.endLine) continue;
|
|
108
|
+
|
|
109
|
+
const image = item.image;
|
|
110
|
+
const insideImage = Boolean(image)
|
|
111
|
+
&& lineIndex >= image.startLine && lineIndex <= image.endLine
|
|
112
|
+
&& relativeX >= image.left && relativeX < image.right;
|
|
113
|
+
|
|
114
|
+
return { message: item.message, part: insideImage ? "image" : "body" };
|
|
143
115
|
}
|
|
144
116
|
|
|
145
117
|
return null;
|
|
@@ -179,6 +151,9 @@ export function getStatusBarActionAt(relativeX, totalWidth = 120) {
|
|
|
179
151
|
return null;
|
|
180
152
|
}
|
|
181
153
|
|
|
154
|
+
/** Ширина логотипа « 🚀 TuiGram» в ячейках — кликабельная зона вызова справки. */
|
|
155
|
+
const HEADER_LOGO_WIDTH = stringCellWidth(" 🚀 TuiGram");
|
|
156
|
+
|
|
182
157
|
/**
|
|
183
158
|
* Определяет действие при клике на верхнюю шапку приложения.
|
|
184
159
|
* @param {number} relativeX смещение по X от левого края шапки
|
|
@@ -190,7 +165,8 @@ export function getStatusBarActionAt(relativeX, totalWidth = 120) {
|
|
|
190
165
|
export function getHeaderActionAt(relativeX, relativeY, { hasActiveChat = false } = {}) {
|
|
191
166
|
// Внутренняя строка 1 (верхняя линия контента): логотип TuiGram, имя пользователя, статус
|
|
192
167
|
if (relativeY === 1) {
|
|
193
|
-
|
|
168
|
+
// Контент шапки начинается после рамки, поэтому логотип занимает ячейки 1..HEADER_LOGO
|
|
169
|
+
if (relativeX >= 1 && relativeX <= HEADER_LOGO_WIDTH) {
|
|
194
170
|
return "help";
|
|
195
171
|
}
|
|
196
172
|
return "status";
|
|
@@ -206,6 +182,20 @@ export function getHeaderActionAt(relativeX, relativeY, { hasActiveChat = false
|
|
|
206
182
|
return null;
|
|
207
183
|
}
|
|
208
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Сегменты подсказок контекстной строки ввода в том порядке и с теми подписями,
|
|
187
|
+
* какими их рисует inputBox.renderContext. Ширины считаются из самих подписей —
|
|
188
|
+
* зашитые вручную координаты разъезжались при любой правке текста.
|
|
189
|
+
*/
|
|
190
|
+
const INPUT_HINT_SEGMENTS = [
|
|
191
|
+
{ id: null, label: " Введите сообщение... " },
|
|
192
|
+
{ id: null, label: "[Enter] Отправить " },
|
|
193
|
+
{ id: null, label: "[Ctrl+J] Новая строка " },
|
|
194
|
+
{ id: "reply", label: "[Ctrl+R] Ответ " },
|
|
195
|
+
{ id: "edit", label: "[Ctrl+E] Правка " },
|
|
196
|
+
{ id: "commands", label: "[/] Команды" },
|
|
197
|
+
];
|
|
198
|
+
|
|
209
199
|
/**
|
|
210
200
|
* Определяет действие при клике на контекстную строку поля ввода.
|
|
211
201
|
* @param {number} relativeX смещение по X от левого края контекстной строки
|
|
@@ -220,16 +210,13 @@ export function getInputContextActionAt(relativeX, mode) {
|
|
|
220
210
|
|
|
221
211
|
if (relativeX < 0) return null;
|
|
222
212
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
if (relativeX >= 91) {
|
|
232
|
-
return "commands";
|
|
213
|
+
let currentX = 0;
|
|
214
|
+
for (const seg of INPUT_HINT_SEGMENTS) {
|
|
215
|
+
const segWidth = stringCellWidth(seg.label);
|
|
216
|
+
if (relativeX >= currentX && relativeX < currentX + segWidth) {
|
|
217
|
+
return seg.id;
|
|
218
|
+
}
|
|
219
|
+
currentX += segWidth;
|
|
233
220
|
}
|
|
234
221
|
|
|
235
222
|
return null;
|