whatsapp-pi 1.0.54 → 1.0.56
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/README.md +1 -0
- package/package.json +1 -1
- package/src/i18n.ts +28 -0
- package/src/services/recents.service.ts +10 -3
- package/src/services/session.manager.ts +17 -1
- package/src/services/whatsapp.service.ts +29 -9
- package/src/ui/menu.handler.ts +167 -71
package/README.md
CHANGED
|
@@ -169,3 +169,4 @@ npm test
|
|
|
169
169
|
- **Session Handling**: Saved state, allow list, and startup reconnects are restored automatically when available.
|
|
170
170
|
- **Intelligent Message Filtering**: Messages ending with `π` are ignored to prevent bot loops.
|
|
171
171
|
- **Storage Management**: Persistent data lives under `.pi-data/` plus the recents store in the user home directory.
|
|
172
|
+
- **Improved Test Coverage (v1.0.56)**: Added unit tests for the `message_end` auto-reply handler, covering the happy path, disconnected guard, role guard, send failure, thrown exceptions, and the `send_wa_message` dedup flag. Fixed a Windows path separator bug in the recents service test suite.
|
package/package.json
CHANGED
package/src/i18n.ts
CHANGED
|
@@ -80,6 +80,11 @@ const fallback = {
|
|
|
80
80
|
"menu.allowed.contact.addAlias": "Add Alias",
|
|
81
81
|
"menu.allowed.contact.removeNumber": "Remove Contact",
|
|
82
82
|
"menu.allowed.contact.back": "Back",
|
|
83
|
+
"menu.allowed.contact.addNumber": "Add Number",
|
|
84
|
+
"menu.allowed.contact.removeSendNumber": "Remove Number",
|
|
85
|
+
"menu.allowed.contact.enterNumber": "Enter send number for {displayName}:",
|
|
86
|
+
"menu.allowed.contact.numberAdded": "Send number added for {displayName}",
|
|
87
|
+
"menu.allowed.contact.numberRemoved": "Send number removed for {displayName}",
|
|
83
88
|
"menu.allowed.enterAlias": "Enter alias for {number}:",
|
|
84
89
|
"menu.allowed.pleaseEnterAlias": "Please enter an alias.",
|
|
85
90
|
"menu.allowed.aliasAdded": "Alias added for {number}",
|
|
@@ -138,6 +143,8 @@ const fallback = {
|
|
|
138
143
|
"menu.recents.history.received": "Received",
|
|
139
144
|
"menu.recents.history.noText": "[No text]",
|
|
140
145
|
"menu.recents.history.messageTitle": "Message • {displayName}",
|
|
146
|
+
"menu.recents.grouped.contacts": "({count} contacts)",
|
|
147
|
+
"menu.recents.grouped.title": "Recents • ({count} contacts)",
|
|
141
148
|
"message.detail.defaultTitle": "Message Details",
|
|
142
149
|
"message.detail.noReadableText": "[No readable text available]",
|
|
143
150
|
"message.detail.messageId": "Message ID",
|
|
@@ -289,6 +296,11 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
289
296
|
"menu.allowed.contact.addAlias": "Adicionar apelido",
|
|
290
297
|
"menu.allowed.contact.removeNumber": "Remover contato",
|
|
291
298
|
"menu.allowed.contact.back": "Voltar",
|
|
299
|
+
"menu.allowed.contact.addNumber": "Adicionar número",
|
|
300
|
+
"menu.allowed.contact.removeSendNumber": "Remover número",
|
|
301
|
+
"menu.allowed.contact.enterNumber": "Digite o número de envio para {displayName}:",
|
|
302
|
+
"menu.allowed.contact.numberAdded": "Número de envio adicionado para {displayName}",
|
|
303
|
+
"menu.allowed.contact.numberRemoved": "Número de envio removido para {displayName}",
|
|
292
304
|
"menu.allowed.enterAlias": "Digite apelido para {number}:",
|
|
293
305
|
"menu.allowed.pleaseEnterAlias": "Digite um apelido.",
|
|
294
306
|
"menu.allowed.aliasAdded": "Apelido adicionado para {number}",
|
|
@@ -347,6 +359,8 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
347
359
|
"menu.recents.history.received": "Recebida",
|
|
348
360
|
"menu.recents.history.noText": "[Sem texto]",
|
|
349
361
|
"menu.recents.history.messageTitle": "Mensagem • {displayName}",
|
|
362
|
+
"menu.recents.grouped.contacts": "({count} contactos)",
|
|
363
|
+
"menu.recents.grouped.title": "Recentes • ({count} contactos)",
|
|
350
364
|
"message.detail.defaultTitle": "Detalhes da mensagem",
|
|
351
365
|
"message.detail.noReadableText": "[No readable text available]",
|
|
352
366
|
"message.detail.messageId": "ID da mensagem",
|
|
@@ -461,6 +475,11 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
461
475
|
"menu.allowed.contact.addAlias": "Agregar alias",
|
|
462
476
|
"menu.allowed.contact.removeNumber": "Quitar contacto",
|
|
463
477
|
"menu.allowed.contact.back": "Volver",
|
|
478
|
+
"menu.allowed.contact.addNumber": "Agregar número",
|
|
479
|
+
"menu.allowed.contact.removeSendNumber": "Quitar número",
|
|
480
|
+
"menu.allowed.contact.enterNumber": "Ingresa el número de envío para {displayName}:",
|
|
481
|
+
"menu.allowed.contact.numberAdded": "Número de envío agregado para {displayName}",
|
|
482
|
+
"menu.allowed.contact.numberRemoved": "Número de envío eliminado para {displayName}",
|
|
464
483
|
"menu.allowed.enterAlias": "Ingresa alias para {number}:",
|
|
465
484
|
"menu.allowed.pleaseEnterAlias": "Ingresa un alias.",
|
|
466
485
|
"menu.allowed.aliasAdded": "Alias agregado para {number}",
|
|
@@ -491,6 +510,8 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
491
510
|
"menu.recents.history.received": "Recibido",
|
|
492
511
|
"menu.recents.history.noText": "[Sin texto]",
|
|
493
512
|
"menu.recents.history.messageTitle": "Mensaje • {displayName}",
|
|
513
|
+
"menu.recents.grouped.contacts": "({count} contactos)",
|
|
514
|
+
"menu.recents.grouped.title": "Recientes • ({count} contactos)",
|
|
494
515
|
"message.detail.defaultTitle": "Detalles del mensaje",
|
|
495
516
|
"message.detail.noReadableText": "[No readable text available]",
|
|
496
517
|
"message.detail.messageId": "ID del mensaje",
|
|
@@ -588,6 +609,11 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
588
609
|
"menu.allowed.contact.addAlias": "Ajouter un alias",
|
|
589
610
|
"menu.allowed.contact.removeNumber": "Supprimer le contact",
|
|
590
611
|
"menu.allowed.contact.back": "Retour",
|
|
612
|
+
"menu.allowed.contact.addNumber": "Ajouter un numéro",
|
|
613
|
+
"menu.allowed.contact.removeSendNumber": "Supprimer le numéro",
|
|
614
|
+
"menu.allowed.contact.enterNumber": "Entrez le numéro d'envoi pour {displayName} :",
|
|
615
|
+
"menu.allowed.contact.numberAdded": "Numéro d'envoi ajouté pour {displayName}",
|
|
616
|
+
"menu.allowed.contact.numberRemoved": "Numéro d'envoi supprimé pour {displayName}",
|
|
591
617
|
"menu.allowed.enterAlias": "Entrez un alias pour {number} :",
|
|
592
618
|
"menu.allowed.pleaseEnterAlias": "Veuillez saisir un alias.",
|
|
593
619
|
"menu.allowed.aliasAdded": "Alias ajouté pour {number}",
|
|
@@ -618,6 +644,8 @@ const translations: Record<Locale, Partial<Record<Key, string>>> = {
|
|
|
618
644
|
"menu.recents.history.received": "Reçu",
|
|
619
645
|
"menu.recents.history.noText": "[Sans texte]",
|
|
620
646
|
"menu.recents.history.messageTitle": "Message • {displayName}",
|
|
647
|
+
"menu.recents.grouped.contacts": "({count} contacts)",
|
|
648
|
+
"menu.recents.grouped.title": "Récents • ({count} contacts)",
|
|
621
649
|
"message.detail.defaultTitle": "Détails du message",
|
|
622
650
|
"message.detail.noReadableText": "[No readable text available]",
|
|
623
651
|
"message.detail.messageId": "ID du message",
|
|
@@ -125,8 +125,15 @@ export class RecentsService {
|
|
|
125
125
|
});
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
private stripSpecialCharacters(text: string): string {
|
|
129
|
+
return text
|
|
130
|
+
.replace(/[\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Regional_Indicator}\u200D\uFE0F]/gu, '')
|
|
131
|
+
.replace(/\s+/g, ' ')
|
|
132
|
+
.trim();
|
|
133
|
+
}
|
|
134
|
+
|
|
128
135
|
private buildPreview(text: string): string {
|
|
129
|
-
const normalized =
|
|
136
|
+
const normalized = this.stripSpecialCharacters(text);
|
|
130
137
|
if (normalized.length <= 80) return normalized;
|
|
131
138
|
return `${normalized.slice(0, 77)}...`;
|
|
132
139
|
}
|
|
@@ -158,7 +165,7 @@ export class RecentsService {
|
|
|
158
165
|
if (!senderNumber) return;
|
|
159
166
|
|
|
160
167
|
const normalizedTimestamp = this.normalizeTimestamp(input.timestamp);
|
|
161
|
-
const normalizedText = input.text
|
|
168
|
+
const normalizedText = this.stripSpecialCharacters(input.text);
|
|
162
169
|
if (!normalizedText) return;
|
|
163
170
|
|
|
164
171
|
const existing = this.store.messagesBySender[senderNumber] ?? [];
|
|
@@ -181,7 +188,7 @@ export class RecentsService {
|
|
|
181
188
|
const summary: RecentConversationSummary = {
|
|
182
189
|
senderNumber,
|
|
183
190
|
senderName: input.senderName ?? existingConversation?.senderName,
|
|
184
|
-
lastMessagePreview: this.buildPreview(
|
|
191
|
+
lastMessagePreview: this.buildPreview(normalizedText),
|
|
185
192
|
lastMessageTime: normalizedTimestamp,
|
|
186
193
|
lastMessageDirection: input.direction,
|
|
187
194
|
messageCount: this.store.messagesBySender[senderNumber].length,
|
|
@@ -8,6 +8,7 @@ import { t } from '../i18n.js';
|
|
|
8
8
|
export interface Contact {
|
|
9
9
|
number: string;
|
|
10
10
|
name?: string;
|
|
11
|
+
sendNumber?: string;
|
|
11
12
|
reactionMode?: ReactionMode;
|
|
12
13
|
}
|
|
13
14
|
|
|
@@ -76,7 +77,8 @@ export class SessionManager {
|
|
|
76
77
|
const reactionMode = item.reactionMode === 'active' || item.reactionMode === 'passive'
|
|
77
78
|
? item.reactionMode
|
|
78
79
|
: undefined;
|
|
79
|
-
|
|
80
|
+
const sendNumber = typeof item.sendNumber === 'string' ? item.sendNumber : undefined;
|
|
81
|
+
return { number: num, name: item.name, sendNumber, reactionMode };
|
|
80
82
|
}
|
|
81
83
|
}
|
|
82
84
|
return null;
|
|
@@ -293,6 +295,20 @@ export class SessionManager {
|
|
|
293
295
|
await this.saveConfig();
|
|
294
296
|
}
|
|
295
297
|
|
|
298
|
+
async setContactSendNumber(number: string, sendNumber: string) {
|
|
299
|
+
const contact = this.getAllowedContact(number);
|
|
300
|
+
if (!contact) return;
|
|
301
|
+
contact.sendNumber = sendNumber.trim();
|
|
302
|
+
await this.saveConfig();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async removeContactSendNumber(number: string) {
|
|
306
|
+
const contact = this.getAllowedContact(number);
|
|
307
|
+
if (!contact) return;
|
|
308
|
+
delete contact.sendNumber;
|
|
309
|
+
await this.saveConfig();
|
|
310
|
+
}
|
|
311
|
+
|
|
296
312
|
async setAllowedGroupAlias(groupJid: string, alias: string) {
|
|
297
313
|
const trimmedAlias = alias.trim();
|
|
298
314
|
if (!trimmedAlias) {
|
|
@@ -107,6 +107,7 @@ export class WhatsAppService {
|
|
|
107
107
|
private saveCreds?: () => Promise<void>;
|
|
108
108
|
private restoreBaileysConsoleFilter?: () => void;
|
|
109
109
|
private reconnectTimeout?: ReturnType<typeof setTimeout>;
|
|
110
|
+
private intentionalStop = false;
|
|
110
111
|
private onQRCode?: (qr: string) => void;
|
|
111
112
|
private onMessage?: (m: MessagesUpsertEvent) => void;
|
|
112
113
|
private onStatusUpdate?: (status: string) => void;
|
|
@@ -254,6 +255,26 @@ export class WhatsAppService {
|
|
|
254
255
|
return Math.min(delay, WhatsAppService.MAX_RECONNECT_DELAY_MS);
|
|
255
256
|
}
|
|
256
257
|
|
|
258
|
+
private scheduleReconnect(options: WhatsAppStartOptions) {
|
|
259
|
+
if (this.intentionalStop) return;
|
|
260
|
+
this.isReconnecting = true;
|
|
261
|
+
this.reconnectAttempts++;
|
|
262
|
+
const delay = this.getReconnectDelayMs();
|
|
263
|
+
this.onStatusUpdate?.(t('service.whatsapp.reconnecting'));
|
|
264
|
+
this.clearReconnectTimeout();
|
|
265
|
+
this.reconnectTimeout = setTimeout(async () => {
|
|
266
|
+
this.isReconnecting = false;
|
|
267
|
+
if (this.intentionalStop) return;
|
|
268
|
+
try {
|
|
269
|
+
await this.start(options);
|
|
270
|
+
} catch {
|
|
271
|
+
if (!this.intentionalStop) {
|
|
272
|
+
this.scheduleReconnect(options);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}, delay);
|
|
276
|
+
}
|
|
277
|
+
|
|
257
278
|
private cleanupSocket() {
|
|
258
279
|
this.clearReconnectTimeout();
|
|
259
280
|
|
|
@@ -322,6 +343,7 @@ export class WhatsAppService {
|
|
|
322
343
|
}
|
|
323
344
|
|
|
324
345
|
async start(options: WhatsAppStartOptions = {}) {
|
|
346
|
+
this.intentionalStop = false;
|
|
325
347
|
if (this.isReconnecting) return;
|
|
326
348
|
this.onStatusUpdate?.(t('service.whatsapp.connecting'));
|
|
327
349
|
|
|
@@ -424,6 +446,10 @@ export class WhatsAppService {
|
|
|
424
446
|
const isAuthRejected = this.isAuthRejected(statusCode, errorMessage);
|
|
425
447
|
const shouldTreatAsLoggedOut = isBadMac || isAuthRejected;
|
|
426
448
|
|
|
449
|
+
if (this.intentionalStop) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
427
453
|
if (this.verboseMode) {
|
|
428
454
|
console.error(t('service.whatsapp.connectionClosed', { statusCode: statusCode ?? 'unknown', shouldReconnect: String(shouldReconnect) }));
|
|
429
455
|
}
|
|
@@ -464,17 +490,9 @@ export class WhatsAppService {
|
|
|
464
490
|
}
|
|
465
491
|
|
|
466
492
|
if (shouldReconnect && !this.isReconnecting) {
|
|
467
|
-
this.isReconnecting = true;
|
|
468
|
-
this.reconnectAttempts++;
|
|
469
|
-
const reconnectDelayMs = this.getReconnectDelayMs();
|
|
470
|
-
this.onStatusUpdate?.(t('service.whatsapp.reconnecting'));
|
|
471
|
-
this.clearReconnectTimeout();
|
|
472
493
|
await this.saveCreds?.();
|
|
473
494
|
this.cleanupSocket();
|
|
474
|
-
this.
|
|
475
|
-
this.isReconnecting = false;
|
|
476
|
-
void this.start(options);
|
|
477
|
-
}, reconnectDelayMs);
|
|
495
|
+
this.scheduleReconnect(options);
|
|
478
496
|
} else if (!shouldReconnect) {
|
|
479
497
|
this.reconnectAttempts = 0;
|
|
480
498
|
this.sessionManager.setStatus('logged-out');
|
|
@@ -706,11 +724,13 @@ export class WhatsAppService {
|
|
|
706
724
|
}
|
|
707
725
|
|
|
708
726
|
async logout() {
|
|
727
|
+
this.intentionalStop = true;
|
|
709
728
|
await this.socket?.logout();
|
|
710
729
|
await this.sessionManager.deleteAuthState();
|
|
711
730
|
}
|
|
712
731
|
|
|
713
732
|
async stop() {
|
|
733
|
+
this.intentionalStop = true;
|
|
714
734
|
try {
|
|
715
735
|
await this.saveCreds?.();
|
|
716
736
|
} catch (error) {
|
package/src/ui/menu.handler.ts
CHANGED
|
@@ -13,6 +13,12 @@ interface HistoryOptionEntry {
|
|
|
13
13
|
message: RecentConversationMessage;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
interface GroupedRecentEntry {
|
|
17
|
+
conversations: RecentConversationSummary[];
|
|
18
|
+
sharedPreview: string;
|
|
19
|
+
sharedTime: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
16
22
|
export class MenuHandler {
|
|
17
23
|
private readonly printedAllowedContacts: string[] = [];
|
|
18
24
|
private readonly printedAllowedGroups: string[] = [];
|
|
@@ -137,8 +143,17 @@ export class MenuHandler {
|
|
|
137
143
|
const removeAliasLabel = t('menu.allowed.contact.removeAlias');
|
|
138
144
|
const addAliasLabel = t('menu.allowed.contact.addAlias');
|
|
139
145
|
const removeNumberLabel = t('menu.allowed.contact.removeNumber');
|
|
146
|
+
const addSendNumberLabel = t('menu.allowed.contact.addNumber');
|
|
147
|
+
const removeSendNumberLabel = t('menu.allowed.contact.removeSendNumber');
|
|
140
148
|
const backLabel = t('menu.allowed.contact.back');
|
|
141
|
-
const options = [historyLabel
|
|
149
|
+
const options = [historyLabel];
|
|
150
|
+
if (contact.sendNumber) {
|
|
151
|
+
options.push(sendMessageLabel);
|
|
152
|
+
options.push(removeSendNumberLabel);
|
|
153
|
+
} else {
|
|
154
|
+
options.push(addSendNumberLabel);
|
|
155
|
+
}
|
|
156
|
+
options.push(printNumberLabel);
|
|
142
157
|
if (contact.name) {
|
|
143
158
|
options.push(removeAliasLabel);
|
|
144
159
|
} else {
|
|
@@ -148,6 +163,27 @@ export class MenuHandler {
|
|
|
148
163
|
|
|
149
164
|
const choice = await ctx.ui.select(title, options);
|
|
150
165
|
|
|
166
|
+
if (choice === addSendNumberLabel) {
|
|
167
|
+
const input = await ctx.ui.input(t('menu.allowed.contact.enterNumber', { displayName }));
|
|
168
|
+
const trimmed = input?.trim() || '';
|
|
169
|
+
if (!validatePhoneNumber(trimmed)) {
|
|
170
|
+
ctx.ui.notify(t('menu.allowed.invalidNumber'), 'error');
|
|
171
|
+
await this.manageAllowedContact(ctx, contact);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
await this.sessionManager.setContactSendNumber(contact.number, trimmed);
|
|
175
|
+
ctx.ui.notify(t('menu.allowed.contact.numberAdded', { displayName }), 'info');
|
|
176
|
+
await this.manageAllowedContact(ctx, { ...contact, sendNumber: trimmed });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (choice === removeSendNumberLabel) {
|
|
181
|
+
await this.sessionManager.removeContactSendNumber(contact.number);
|
|
182
|
+
ctx.ui.notify(t('menu.allowed.contact.numberRemoved', { displayName }), 'info');
|
|
183
|
+
await this.manageAllowedContact(ctx, { ...contact, sendNumber: undefined });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
151
187
|
if (choice === sendMessageLabel) {
|
|
152
188
|
await this.sendMessageToAllowedContact(ctx, contact);
|
|
153
189
|
await this.manageAllowedContact(ctx, contact);
|
|
@@ -344,36 +380,56 @@ export class MenuHandler {
|
|
|
344
380
|
|
|
345
381
|
private async manageRecents(ctx: ExtensionCommandContext) {
|
|
346
382
|
const recentConversations = await this.recentsService.getRecentConversations();
|
|
383
|
+
const groupedEntries = this.groupRecentConversations(recentConversations);
|
|
347
384
|
const title = t('menu.recents.title');
|
|
348
385
|
const backLabel = t('menu.root.back');
|
|
386
|
+
const nextLabel = 'Next';
|
|
387
|
+
const previousLabel = 'Previous';
|
|
388
|
+
const pageSize = 10;
|
|
349
389
|
|
|
350
|
-
if (
|
|
390
|
+
if (groupedEntries.length === 0) {
|
|
351
391
|
ctx.ui.notify(t('menu.recents.empty'), 'info');
|
|
352
392
|
await this.handleCommand(ctx);
|
|
353
393
|
return;
|
|
354
394
|
}
|
|
355
395
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
396
|
+
for (let page = 0; page * pageSize < groupedEntries.length;) {
|
|
397
|
+
const start = page * pageSize;
|
|
398
|
+
const pageEntries = groupedEntries.slice(start, start + pageSize);
|
|
399
|
+
const options = [
|
|
400
|
+
...pageEntries.map(entry => this.formatGroupedRecentOption(entry)),
|
|
401
|
+
...(page > 0 ? [previousLabel] : []),
|
|
402
|
+
...(start + pageSize < groupedEntries.length ? [nextLabel] : []),
|
|
403
|
+
backLabel
|
|
404
|
+
];
|
|
360
405
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
406
|
+
const choice = await ctx.ui.select(title, options);
|
|
407
|
+
if (!choice || choice === backLabel) {
|
|
408
|
+
await this.handleCommand(ctx);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
366
411
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
412
|
+
if (choice === nextLabel) {
|
|
413
|
+
page += 1;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (choice === previousLabel) {
|
|
418
|
+
page = Math.max(0, page - 1);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const selectedEntry = pageEntries.find(entry =>
|
|
423
|
+
this.formatGroupedRecentOption(entry) === choice
|
|
424
|
+
);
|
|
370
425
|
|
|
371
|
-
|
|
372
|
-
|
|
426
|
+
if (!selectedEntry) {
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
await this.manageRecentConversation(ctx, selectedEntry.conversations[0]);
|
|
373
431
|
return;
|
|
374
432
|
}
|
|
375
|
-
|
|
376
|
-
await this.manageRecentConversation(ctx, selectedConversation);
|
|
377
433
|
}
|
|
378
434
|
|
|
379
435
|
private async manageRecentConversation(ctx: ExtensionCommandContext, conversation: RecentConversationSummary) {
|
|
@@ -387,7 +443,6 @@ export class MenuHandler {
|
|
|
387
443
|
const allowContactLabel = isGroup
|
|
388
444
|
? t('menu.recents.contact.allowGroup')
|
|
389
445
|
: t('menu.recents.contact.allowNumber');
|
|
390
|
-
const sendMessageLabel = t('menu.recents.contact.sendMessage');
|
|
391
446
|
const removeAliasLabel = t('menu.recents.contact.removeAlias');
|
|
392
447
|
const backLabel = t('menu.recents.contact.back');
|
|
393
448
|
const options: string[] = [historyLabel];
|
|
@@ -396,8 +451,6 @@ export class MenuHandler {
|
|
|
396
451
|
options.push(allowContactLabel);
|
|
397
452
|
}
|
|
398
453
|
|
|
399
|
-
options.push(sendMessageLabel);
|
|
400
|
-
|
|
401
454
|
if (allowedContact?.name) {
|
|
402
455
|
options.push(removeAliasLabel);
|
|
403
456
|
}
|
|
@@ -434,12 +487,6 @@ export class MenuHandler {
|
|
|
434
487
|
return;
|
|
435
488
|
}
|
|
436
489
|
|
|
437
|
-
if (choice === sendMessageLabel) {
|
|
438
|
-
await this.sendMessageFromRecents(ctx, conversation);
|
|
439
|
-
await this.manageRecentConversation(ctx, conversation);
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
|
|
443
490
|
if (choice === historyLabel) {
|
|
444
491
|
await this.showConversationHistory(ctx, conversation);
|
|
445
492
|
await this.manageRecentConversation(ctx, conversation);
|
|
@@ -449,19 +496,10 @@ export class MenuHandler {
|
|
|
449
496
|
await this.manageRecents(ctx);
|
|
450
497
|
}
|
|
451
498
|
|
|
452
|
-
private async sendMessageFromRecents(ctx: ExtensionCommandContext, conversation: RecentConversationSummary) {
|
|
453
|
-
await this.sendPromptedMenuMessage(ctx, {
|
|
454
|
-
displayName: this.getConversationDisplayName(conversation),
|
|
455
|
-
senderNumber: conversation.senderNumber,
|
|
456
|
-
senderName: conversation.senderName,
|
|
457
|
-
appendPiSuffix: false
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
|
|
461
499
|
private async sendMessageToAllowedContact(ctx: ExtensionCommandContext, contact: Contact) {
|
|
462
500
|
await this.sendPromptedMenuMessage(ctx, {
|
|
463
501
|
displayName: this.formatAllowedContactOption(contact),
|
|
464
|
-
senderNumber: contact.
|
|
502
|
+
senderNumber: contact.sendNumber!,
|
|
465
503
|
senderName: contact.name,
|
|
466
504
|
appendPiSuffix: true
|
|
467
505
|
});
|
|
@@ -559,44 +597,66 @@ export class MenuHandler {
|
|
|
559
597
|
return;
|
|
560
598
|
}
|
|
561
599
|
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
600
|
+
const sortedHistory = this.sortHistoryByMostRecent(history);
|
|
601
|
+
const pageSize = 10;
|
|
602
|
+
const backLabel = t('menu.root.back');
|
|
603
|
+
const nextLabel = 'Next';
|
|
604
|
+
const previousLabel = 'Previous';
|
|
605
|
+
|
|
606
|
+
for (let page = 0; page * pageSize < sortedHistory.length;) {
|
|
607
|
+
const start = page * pageSize;
|
|
608
|
+
const pageHistory = sortedHistory.slice(start, start + pageSize);
|
|
609
|
+
const historyOptions = this.buildHistoryOptions(pageHistory);
|
|
610
|
+
const choice = await ctx.ui.select(t('menu.recents.history.title', { displayName }), [
|
|
611
|
+
...historyOptions.map(option => option.label),
|
|
612
|
+
...(page > 0 ? [previousLabel] : []),
|
|
613
|
+
...(start + pageSize < sortedHistory.length ? [nextLabel] : []),
|
|
614
|
+
backLabel
|
|
615
|
+
]);
|
|
616
|
+
|
|
617
|
+
if (!choice || choice === backLabel) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
567
620
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
621
|
+
if (choice === nextLabel) {
|
|
622
|
+
page += 1;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
571
625
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
626
|
+
if (choice === previousLabel) {
|
|
627
|
+
page = Math.max(0, page - 1);
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
576
630
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
senderName,
|
|
582
|
-
text: selectedMessage.text,
|
|
583
|
-
direction: selectedMessage.direction,
|
|
584
|
-
timestamp: selectedMessage.timestamp
|
|
585
|
-
});
|
|
631
|
+
const selectedMessage = this.resolveHistorySelection(choice, historyOptions);
|
|
632
|
+
if (!selectedMessage) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
586
635
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
timestamp: selectedMessage.timestamp
|
|
596
|
-
},
|
|
597
|
-
whatsappService: this.whatsappService,
|
|
598
|
-
recentsService: this.recentsService
|
|
636
|
+
const detailAction = await showMessageDetailView(ctx, {
|
|
637
|
+
title: t('menu.recents.history.messageTitle', { displayName }),
|
|
638
|
+
messageId: selectedMessage.messageId,
|
|
639
|
+
senderNumber: selectedMessage.senderNumber,
|
|
640
|
+
senderName,
|
|
641
|
+
text: selectedMessage.text,
|
|
642
|
+
direction: selectedMessage.direction,
|
|
643
|
+
timestamp: selectedMessage.timestamp
|
|
599
644
|
});
|
|
645
|
+
|
|
646
|
+
if (detailAction === 'reply') {
|
|
647
|
+
await showMessageReplyView(ctx, {
|
|
648
|
+
selectedMessage: {
|
|
649
|
+
messageId: selectedMessage.messageId,
|
|
650
|
+
senderNumber: selectedMessage.senderNumber,
|
|
651
|
+
senderName,
|
|
652
|
+
text: selectedMessage.text,
|
|
653
|
+
direction: selectedMessage.direction,
|
|
654
|
+
timestamp: selectedMessage.timestamp
|
|
655
|
+
},
|
|
656
|
+
whatsappService: this.whatsappService,
|
|
657
|
+
recentsService: this.recentsService
|
|
658
|
+
});
|
|
659
|
+
}
|
|
600
660
|
}
|
|
601
661
|
}
|
|
602
662
|
|
|
@@ -611,6 +671,41 @@ export class MenuHandler {
|
|
|
611
671
|
return options.find(option => option.label === choice)?.message;
|
|
612
672
|
}
|
|
613
673
|
|
|
674
|
+
private getRecentsGroupKey(conversation: RecentConversationSummary): string {
|
|
675
|
+
if (conversation.lastMessageDirection === 'outgoing') {
|
|
676
|
+
return `outgoing::${conversation.senderNumber}`;
|
|
677
|
+
}
|
|
678
|
+
const d = new Date(conversation.lastMessageTime);
|
|
679
|
+
const minuteKey = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}-${d.getHours()}-${d.getMinutes()}`;
|
|
680
|
+
return `${conversation.lastMessagePreview}::${minuteKey}`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
private groupRecentConversations(conversations: RecentConversationSummary[]): GroupedRecentEntry[] {
|
|
684
|
+
const groups = new Map<string, RecentConversationSummary[]>();
|
|
685
|
+
for (const conversation of conversations) {
|
|
686
|
+
const key = this.getRecentsGroupKey(conversation);
|
|
687
|
+
const existing = groups.get(key) ?? [];
|
|
688
|
+
existing.push(conversation);
|
|
689
|
+
groups.set(key, existing);
|
|
690
|
+
}
|
|
691
|
+
return Array.from(groups.values()).map(members => ({
|
|
692
|
+
conversations: members,
|
|
693
|
+
sharedPreview: members[0].lastMessagePreview,
|
|
694
|
+
sharedTime: members[0].lastMessageTime
|
|
695
|
+
}));
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
private formatGroupedRecentOption(entry: GroupedRecentEntry): string {
|
|
699
|
+
if (entry.conversations.length === 1) {
|
|
700
|
+
return this.formatRecentConversationOption(entry.conversations[0]);
|
|
701
|
+
}
|
|
702
|
+
const time = this.formatDateTime(entry.sharedTime);
|
|
703
|
+
const identifiers = entry.conversations
|
|
704
|
+
.map(c => c.senderName ? `[${c.senderNumber}] ${c.senderName}` : `(${c.senderNumber})`)
|
|
705
|
+
.join(' ');
|
|
706
|
+
return `${identifiers} • ${time} • ${entry.sharedPreview}`;
|
|
707
|
+
}
|
|
708
|
+
|
|
614
709
|
private formatRecentConversationOption(conversation: RecentConversationSummary): string {
|
|
615
710
|
const displayName = this.getConversationDisplayName(conversation);
|
|
616
711
|
const time = this.formatDateTime(conversation.lastMessageTime);
|
|
@@ -618,7 +713,8 @@ export class MenuHandler {
|
|
|
618
713
|
}
|
|
619
714
|
|
|
620
715
|
private formatAllowedContactOption(contact: Contact): string {
|
|
621
|
-
|
|
716
|
+
const base = contact.name ? `${contact.name} [${contact.number}]` : contact.number;
|
|
717
|
+
return contact.sendNumber ? `${base} (${contact.sendNumber})` : base;
|
|
622
718
|
}
|
|
623
719
|
|
|
624
720
|
private formatAllowedGroupOption(group: Contact): string {
|