• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ringcentral / ringcentral-js-widgets / 21017357600

15 Jan 2026 02:19AM UTC coverage: 61.994% (-1.1%) from 63.06%
21017357600

push

github

web-flow
misc: sync features and bugfixes from 8f9fbb5dfe (#1784)

* misc: sync crius

* chore: update babel-setting deps

* feat(core): update logger and add fromWatch

* misc: fix tsconfig

* misc: fix tsconfig

* misc: update eslint-settings and new eslint-plugin-crius package

* chore: remove ci and nx from package.json

* feat(i18n): new getAcceptLocaleMap and preudo string support

* chore: add preudo i18n

* misc(locale-loader): convert to ts, new format support

* misc(locale-settings): use ts

* chore: add format test in phone number lib

* feat(react-hooks): add more hooks

* chore: add comments

* misc: add more mock files

* misc: update test utils

* misc: update utils

* chore: update tsconfig

* misc: update i18n string, and convert to ts

* feat: update ui components, support emoji input, new video setting ui

* feat: new rcvideo v2 module

* feat: use new subscription register api

* misc(commons): update enums/interfaces

* misc: update Analytics lib

* misc: upgrade uuid and update import

* misc(commons): update formatDuration lib

* misc(test): add test steps

* chore: update tests and more feature tests

* misc: update demo project

* misc: update cli template

* misc: fix deps issue

* misc: remove glip widgets package

* misc: fix wrong import path

* misc: limit jest worker memory

* chore: use npm trusted-publishers

10285 of 18150 branches covered (56.67%)

Branch coverage included in aggregate %.

986 of 2186 new or added lines in 228 files covered. (45.11%)

44 existing lines in 23 files now uncovered.

17404 of 26514 relevant lines covered (65.64%)

167640.7 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

70.62
/packages/ringcentral-integration/modules/MessageStore/MessageStore.ts
1
import type GetMessageInfoResponse from '@rc-ex/core/lib/definitions/GetMessageInfoResponse';
2
import type MessageEvent from '@rc-ex/core/lib/definitions/MessageEvent';
3
import { computed, track, watch } from '@ringcentral-integration/core';
4
import { sleep } from '@ringcentral-integration/utils';
5
import type { ApiError } from '@ringcentral/sdk';
6
import { EventEmitter } from 'events';
7

8
import { subscriptionFilters } from '../../enums/subscriptionFilters';
9
import { trackEvents } from '../../enums/trackEvents';
10
import type {
11
  Message,
12
  Messages,
13
  MessageStoreModel,
14
  MessageSyncList,
15
} from '../../interfaces/MessageStore.model';
16
import { batchPutApi } from '../../lib/batchApiHelper';
17
import { debounce } from '../../lib/debounce-throttle';
18
import { Module } from '../../lib/di';
19
import * as messageHelper from '../../lib/messageHelper';
20
import { proxify } from '../../lib/proxy/proxify';
21
import { callingModes } from '../CallingSettings';
22
import { DataFetcherV2Consumer, DataSource } from '../DataFetcherV2';
23

24
import type {
25
  Deps,
26
  DispatchedMessageIds,
27
  MessageHandler,
28
  MessageStoreConversations,
29
  ProcessRawConversationListOptions,
30
  ProcessRawConversationStoreOptions,
31
  SyncFunctionOptions,
32
} from './MessageStore.interface';
33
import { messageStoreErrors } from './messageStoreErrors';
34
import { getSyncParams } from './messageStoreHelper';
35

36
const DEFAULT_CONVERSATIONS_LOAD_LENGTH = 10;
223✔
37
const DEFAULT_CONVERSATION_LOAD_LENGTH = 100;
223✔
38
const DEFAULT_POLLING_INTERVAL = 30 * 60 * 1000; // 30 min
223✔
39
const DEFAULT_TTL = 5 * 60 * 1000; // 5 min
223✔
40
const DEFAULT_RETRY = 62 * 1000; // 62 sec
223✔
41

42
const DEFAULT_DAY_SPAN = 7; // default to load 7 days messages
223✔
43
const DEFAULT_MESSAGES_FILTER = (list: Messages) => list;
373✔
44
const UPDATE_MESSAGE_ONCE_COUNT = 20; // Number of messages to be updated in one time
223✔
45

46
// reference: https://developers.ringcentral.com/api-reference/Message-Store/syncMessages
47
const INVALID_TOKEN_ERROR_CODES = ['CMN-101', 'MSG-333'];
223✔
48

49
/**
50
 * Messages data managing module
51
 * fetch conversations
52
 * handle new message subscription
53
 */
54
@Module({
55
  name: 'MessageStore',
56
  deps: [
57
    'Alert',
58
    'Auth',
59
    'Client',
60
    'DataFetcherV2',
61
    'Subscription',
62
    'ConnectivityMonitor',
63
    'AppFeatures',
64
    { dep: 'AvailabilityMonitor', optional: true },
65
    { dep: 'TabManager', optional: true },
66
    { dep: 'MessageStoreOptions', optional: true },
67
  ],
68
})
69
export class MessageStore<T extends Deps = Deps> extends DataFetcherV2Consumer<
70
  T,
71
  MessageStoreModel
72
> {
73
  protected _conversationsLoadLength =
74
    this._deps.messageStoreOptions?.conversationsLoadLength ??
344!
75
    DEFAULT_CONVERSATIONS_LOAD_LENGTH;
76

77
  protected _conversationLoadLength =
78
    this._deps.messageStoreOptions?.conversationLoadLength ??
344!
79
    DEFAULT_CONVERSATION_LOAD_LENGTH;
80

81
  protected _messagesFilter =
82
    this._deps.messageStoreOptions?.messagesFilter ?? DEFAULT_MESSAGES_FILTER;
344✔
83

84
  protected _daySpan =
85
    this._deps.messageStoreOptions?.daySpan ?? DEFAULT_DAY_SPAN;
344!
86

87
  protected _eventEmitter = new EventEmitter();
344✔
88

89
  protected _dispatchedMessageIds: DispatchedMessageIds = [];
344✔
90

91
  // @ts-expect-error TS(2322): Type 'null' is not assignable to type 'GetMessageI... Remove this comment to see the full error message
92
  protected _handledRecord: GetMessageInfoResponse[] = null;
344✔
93

94
  constructor(deps: T) {
95
    super({
344✔
96
      deps,
97
    });
98

99
    const {
100
      disableCache = false,
344✔
101
      polling = false,
344✔
102
      timeToRetry = DEFAULT_RETRY,
344✔
103
      pollingInterval = DEFAULT_POLLING_INTERVAL,
344✔
104
      ttl = DEFAULT_TTL,
344✔
105
    } = this._deps.messageStoreOptions ?? {};
344!
106
    // @ts-expect-error TS(2322): Type 'DataSource<{ conversationList: ConversationI... Remove this comment to see the full error message
107
    this._source = new DataSource({
344✔
108
      ...this._deps.messageStoreOptions,
109
      key: 'messageStore',
110
      disableCache,
111
      ttl,
112
      polling,
113
      timeToRetry,
114
      pollingInterval,
115
      cleanOnReset: true,
116
      permissionCheckFunction: () => this._hasPermission,
78,440✔
117
      readyCheckFunction: () => this._deps.appFeatures.ready,
98,739✔
118
      fetchFunction: async () => this._syncData(),
364✔
119
    });
120
    this._deps.dataFetcherV2.register(this._source);
344✔
121

122
    this._deps.subscription.register(this, {
344✔
123
      filters: [
124
        subscriptionFilters.messageStore,
125
        subscriptionFilters.instantMessage,
126
      ],
127
    });
128
  }
129

130
  override onInitOnce() {
131
    if (this._deps.connectivityMonitor) {
343!
132
      watch(
343✔
133
        this,
134
        () => this._deps.connectivityMonitor.connectivity,
121,474✔
135
        (newValue) => {
136
          if (this.ready && this._deps.connectivityMonitor.ready && newValue) {
40✔
137
            this._deps.dataFetcherV2.fetchData(this._source);
16✔
138
          }
139
        },
140
      );
141
    }
142
    watch(
343✔
143
      this,
144
      () => this._deps.subscription.message as MessageEvent | undefined,
121,474✔
145
      async (newValue) => {
146
        if (
153✔
147
          !newValue ||
509✔
148
          !this.ready ||
149
          !this._hasPermission ||
150
          (this._deps.tabManager && !this._deps.tabManager.active)
151
        ) {
152
          return;
64✔
153
        }
154
        const messageStoreEvent = /\/message-store$/;
89✔
155
        const instantMessageEvent = /\/message-store\/instant\?type=SMS$/;
89✔
156
        if (messageStoreEvent.test(newValue.event!) && newValue.body?.changes) {
89✔
157
          try {
10✔
158
            await this.fetchData({ passive: true });
10✔
159
          } catch (ex) {
160
            console.error('[MessageStore] > subscription > fetchData', ex);
×
161
          }
162
        } else if (instantMessageEvent.test(newValue.event!)) {
79!
163
          this.pushMessage(messageHelper.normalizeInstantEvent(newValue));
×
164
        }
165
      },
166
    );
167
  }
168

169
  @proxify
170
  async _updateData(data: any, timestamp = Date.now()) {
10✔
171
    this._deps.dataFetcherV2.updateData(this._source, data, timestamp);
10✔
172
  }
173

174
  _processRawConversationList({
175
    records,
176
    conversationStore,
177
    isFSyncSuccess,
178
  }: ProcessRawConversationListOptions) {
179
    const state = this.data?.conversationList || [];
411✔
180
    const newState: MessageStoreModel['conversationList'] = [];
411✔
181
    const stateMap: Record<string, { index: number }> = {};
411✔
182
    if (!isFSyncSuccess) {
411✔
183
      if (!records || records.length === 0) {
64!
184
        return state;
×
185
      }
186
      state.forEach((oldConversation) => {
64✔
187
        newState.push(oldConversation);
732✔
188
        stateMap[oldConversation.id] = {
732✔
189
          index: newState.length - 1,
190
        };
191
      });
192
    }
193
    records.forEach((record) => {
411✔
194
      const message = messageHelper.normalizeRecord(record);
1,986✔
195
      const id = message.conversationId;
1,986✔
196
      const newCreationTime = message.creationTime;
1,986✔
197
      const isDeleted = messageHelper.messageIsDeleted(message);
1,986✔
198
      // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
199
      if (stateMap[id]) {
1,986✔
200
        // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
201
        const oldConversation = newState[stateMap[id].index];
458✔
202
        const creationTime = oldConversation.creationTime;
458✔
203
        // @ts-expect-error TS(2532): Object is possibly 'undefined'.
204
        if (creationTime < newCreationTime && !isDeleted) {
458✔
205
          // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
206
          newState[stateMap[id].index] = {
14✔
207
            // @ts-expect-error TS(2322): Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
208
            id,
209
            // @ts-expect-error TS(2322): Type 'number | undefined' is not assignable to typ... Remove this comment to see the full error message
210
            creationTime: newCreationTime,
211
            type: message.type,
212
            messageId: message.id,
213
          };
214
        }
215
        // when user deleted a coversation message
216
        if (isDeleted && message.id === oldConversation.messageId) {
458!
217
          // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
218
          const oldMessageList = conversationStore[id] || [];
×
NEW
219
          const existedMessageList = oldMessageList.filter(
×
220
            (m: any) => m.id !== message.id,
×
221
          );
NEW
222
          if (existedMessageList.length > 0) {
×
223
            // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
224
            newState[stateMap[id].index] = {
×
225
              // @ts-expect-error TS(2322): Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
226
              id,
227
              creationTime: existedMessageList[0].creationTime,
228
              type: existedMessageList[0].type,
229
              messageId: existedMessageList[0].id,
230
            };
231
            return;
×
232
          }
233
          // when user delete conversation
234
          // @ts-expect-error TS(2322): Type 'null' is not assignable to type 'Conversatio... Remove this comment to see the full error message
235
          newState[stateMap[id].index] = null;
×
236
          // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
237
          delete stateMap[id];
×
238
        }
239
        return;
458✔
240
      }
241
      if (isDeleted || !messageHelper.messageIsAcceptable(message)) {
1,528!
242
        return;
×
243
      }
244
      newState.push({
1,528✔
245
        // @ts-expect-error TS(2322): Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
246
        id,
247
        // @ts-expect-error TS(2322): Type 'number | undefined' is not assignable to typ... Remove this comment to see the full error message
248
        creationTime: newCreationTime,
249
        type: message.type,
250
        messageId: message.id,
251
      });
252
      // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
253
      stateMap[id] = {
1,528✔
254
        index: newState.length - 1,
255
      };
256
    });
257
    return newState
411✔
258
      .filter((c) => !!c && typeof c.creationTime === 'number')
2,260✔
259
      .sort(messageHelper.sortByCreationTime);
260
  }
261

262
  _processRawConversationStore({
263
    records,
264
    isFSyncSuccess,
265
  }: ProcessRawConversationStoreOptions) {
266
    const state = this.data?.conversationStore ?? {};
411✔
267
    let newState: MessageStoreModel['conversationStore'] = {};
411✔
268
    const updatedConversations: Record<string, number> = {};
411✔
269
    if (!isFSyncSuccess) {
411✔
270
      if (!records || records.length === 0) {
64!
271
        return state;
×
272
      }
273
      newState = {
64✔
274
        ...state,
275
      };
276
    }
277
    records.forEach((record) => {
411✔
278
      const message = messageHelper.normalizeRecord(record);
1,986✔
279
      const id = message.conversationId;
1,986✔
280
      // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
281
      const newMessages = newState[id] ? [].concat(newState[id]) : [];
1,986✔
282
      // @ts-expect-error TS(2339): Property 'id' does not exist on type 'never'.
283
      const oldMessageIndex = newMessages.findIndex((r) => r.id === record.id);
19,668✔
284
      if (messageHelper.messageIsDeleted(message)) {
1,986!
285
        // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
286
        newState[id] = newMessages.filter((m) => m.id !== message.id);
×
287
        // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
288
        if (newState[id].length === 0) {
×
289
          // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
290
          delete newState[id];
×
291
        }
292
        return;
×
293
      }
294
      if (oldMessageIndex > -1) {
1,986✔
295
        if (
258✔
296
          // @ts-expect-error TS(2339): Property 'lastModifiedTime' does not exist on type... Remove this comment to see the full error message
297
          newMessages[oldMessageIndex].lastModifiedTime <
298
          // @ts-expect-error TS(2532): Object is possibly 'undefined'.
299
          message.lastModifiedTime
300
        ) {
301
          // @ts-expect-error TS(2322): Type 'Message' is not assignable to type 'never'.
302
          newMessages[oldMessageIndex] = message;
25✔
303
        }
304
      } else if (messageHelper.messageIsAcceptable(message)) {
1,728!
305
        // @ts-expect-error TS(2345): Argument of type 'Message' is not assignable to pa... Remove this comment to see the full error message
306
        newMessages.push(message);
1,728✔
307
      }
308
      // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
309
      updatedConversations[id] = 1;
1,986✔
310
      // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
311
      newState[id] = newMessages;
1,986✔
312
    });
313
    Object.keys(updatedConversations).forEach((id) => {
411✔
314
      const noSorted = newState[id];
1,582✔
315
      newState[id] = noSorted.sort(messageHelper.sortByCreationTime);
1,582✔
316
    });
317
    return newState;
411✔
318
  }
319

320
  async _syncFunction({
321
    recordCount,
322
    conversationLoadLength,
323
    dateFrom,
324
    dateTo,
325
    syncToken,
326
    receivedRecordsLength = 0,
378✔
327
  }: SyncFunctionOptions): Promise<MessageSyncList> {
328
    const params = getSyncParams({
378✔
329
      recordCount,
330
      conversationLoadLength,
331
      dateFrom,
332
      dateTo,
333
      syncToken,
334
    });
335
    const { records, syncInfo = {} }: MessageSyncList = await this._deps.client
378✔
336
      .account()
337
      .extension()
338
      .messageSync()
339
      .list(params);
340
    receivedRecordsLength += records.length;
373✔
341
    // @ts-expect-error TS(2532): Object is possibly 'undefined'.
342
    if (!syncInfo.olderRecordsExist || receivedRecordsLength >= recordCount) {
373!
343
      return { records, syncInfo };
373✔
344
    }
345
    await sleep(500);
×
346
    // @ts-expect-error TS(2769): No overload matches this call.
347
    const olderDateTo = new Date(records[records.length - 1].creationTime);
×
348
    const olderRecordResult = await this._syncFunction({
×
349
      conversationLoadLength,
350
      dateFrom,
351
      dateTo: olderDateTo,
352
    });
353
    return {
×
354
      records: records.concat(olderRecordResult.records),
355
      syncInfo,
356
    };
357
  }
358

359
  // @ts-expect-error TS(2352): Conversion of type 'null' to type 'Date' may be a ... Remove this comment to see the full error message
360
  async _syncData({ dateTo = null as Date, passive = false } = {}) {
1,102✔
361
    const conversationsLoadLength = this._conversationsLoadLength;
374✔
362
    const conversationLoadLength = this._conversationLoadLength;
374✔
363
    const { ownerId } = this._deps.auth;
374✔
364
    try {
374✔
365
      const dateFrom = new Date();
374✔
366
      dateFrom.setDate(dateFrom.getDate() - this._daySpan);
374✔
367
      let syncToken = dateTo ? undefined : this.syncInfo?.syncToken;
374!
368
      const recordCount = conversationsLoadLength * conversationLoadLength;
374✔
369
      let data: MessageSyncList;
370
      try {
374✔
371
        data = await this._syncFunction({
374✔
372
          recordCount,
373
          conversationLoadLength,
374
          dateFrom,
375
          syncToken,
376
          dateTo,
377
        });
378
      } catch (e: unknown) {
379
        const error = e as ApiError;
5✔
380
        const responseResult = await error.response?.clone().json();
5✔
381
        if (
5✔
382
          responseResult?.errors?.some(({ errorCode = '' } = {}) =>
×
383
            INVALID_TOKEN_ERROR_CODES.includes(errorCode),
5✔
384
          )
385
        ) {
386
          data = await this._syncFunction({
4✔
387
            recordCount,
388
            conversationLoadLength,
389
            dateFrom,
390
            syncToken: undefined,
391
            dateTo,
392
          });
393
          syncToken = undefined;
4✔
394
        } else {
395
          throw error;
1✔
396
        }
397
      }
398
      if (this._deps.auth.ownerId === ownerId) {
373!
399
        const records = this._messagesFilter(data!.records);
373✔
400
        const isFSyncSuccess = !syncToken;
373✔
401
        // this is only executed in passive sync mode (aka. invoked by subscription)
402
        if (passive) {
373✔
403
          this._handledRecord = records;
10✔
404
        }
405
        return {
373✔
406
          conversationList: this._processRawConversationList({
407
            records,
408
            conversationStore: this.conversationStore,
409
            isFSyncSuccess,
410
          }),
411
          conversationStore: this._processRawConversationStore({
412
            records,
413
            isFSyncSuccess,
414
          }),
415
          syncInfo: data!.syncInfo,
416
        };
417
      }
418
    } catch (error: any /** TODO: confirm with instanceof */) {
419
      if (this._deps.auth.ownerId === ownerId) {
1!
420
        console.error('[MessageStore] > _syncData', error);
1✔
421
        throw error;
1✔
422
      }
423
    }
424
  }
425

426
  @proxify
427
  override async fetchData({ passive = false } = {}) {
×
428
    const data = await this._syncData({ passive });
10✔
429
    this._updateData(data);
10✔
430
    if (passive && this._handledRecord) {
10!
431
      this._dispatchMessageHandlers(this._handledRecord);
10✔
432
      // @ts-expect-error TS(2322): Type 'null' is not assignable to type 'GetMessageI... Remove this comment to see the full error message
433
      this._handledRecord = null;
10✔
434
    }
435
  }
436

437
  onNewInboundMessage(handler: MessageHandler) {
438
    if (typeof handler === 'function') {
×
439
      this._eventEmitter.on('newInboundMessageNotification', handler);
×
440
    }
441
  }
442

443
  onMessageUpdated(handler: MessageHandler) {
444
    if (typeof handler === 'function') {
×
445
      this._eventEmitter.on('messageUpdated', handler);
×
446
    }
447
  }
448

449
  /**
450
   * Dispatch events to different handlers
451
   */
452
  _dispatchMessageHandlers(records: GetMessageInfoResponse[]) {
453
    // Sort all records by creation time
454
    records = records.slice().sort(
10✔
455
      (a, b) =>
456
        // @ts-expect-error TS(2769): No overload matches this call.
457
        new Date(a.creationTime).getTime() -
4✔
458
        // @ts-expect-error TS(2769): No overload matches this call.
459
        new Date(b.creationTime).getTime(),
460
    );
461
    for (const record of records) {
10✔
462
      const {
463
        id,
464
        direction,
465
        availability,
466
        messageStatus,
467
        readStatus,
468
        lastModifiedTime,
469
        creationTime,
470
      } = record || {};
14!
471
      // Notify when new message incoming
472
      // fix mix old messages and new messages logic error.
473
      if (!this._messageDispatched(record)) {
14!
474
        // Mark last 10 messages that dispatched
475
        // To present dispatching same record twice
476
        // @ts-expect-error TS(2322): Type '{ id: number | undefined; lastModifiedTime: ... Remove this comment to see the full error message
477
        this._dispatchedMessageIds = [{ id, lastModifiedTime }]
14✔
478
          .concat(this._dispatchedMessageIds)
479
          .slice(0, 20);
480
        this._eventEmitter.emit('messageUpdated', record);
14✔
481
        // For new inbound message notification
482
        if (
14✔
483
          direction === 'Inbound' &&
19✔
484
          readStatus === 'Unread' &&
485
          messageStatus === 'Received' &&
486
          availability === 'Alive' &&
487
          // @ts-expect-error TS(2769): No overload matches this call.
488
          new Date(creationTime).getTime() >
489
            // @ts-expect-error TS(2769): No overload matches this call.
490
            new Date(lastModifiedTime).getTime() - 600 * 1000
491
        ) {
492
          this._eventEmitter.emit('newInboundMessageNotification', record);
1✔
493
        }
494
      }
495
    }
496
  }
497

498
  _messageDispatched(message: GetMessageInfoResponse) {
499
    return this._dispatchedMessageIds.some(
14✔
500
      (m) =>
501
        m.id === message.id && m.lastModifiedTime === message.lastModifiedTime,
4!
502
    );
503
  }
504

505
  @proxify
506
  async pushMessages(records: GetMessageInfoResponse[]) {
507
    this._deps.dataFetcherV2.updateData(
38✔
508
      this._source,
509
      {
510
        ...this.data,
511
        conversationList: this._processRawConversationList({
512
          records,
513
          conversationStore: this.conversationStore,
514
        }),
515
        conversationStore: this._processRawConversationStore({
516
          records,
517
        }),
518
      },
519
      // @ts-expect-error TS(2345): Argument of type 'number | null' is not assignable... Remove this comment to see the full error message
520
      this.timestamp,
521
    );
522
  }
523

524
  pushMessage(record: GetMessageInfoResponse) {
525
    this.pushMessages([record]);
4✔
526
  }
527

528
  async _updateMessageApi(messageId: string, status: Message['readStatus']) {
529
    const body = {
17✔
530
      readStatus: status,
531
    };
532
    const updateRequest: GetMessageInfoResponse = await this._deps.client
17✔
533
      .account()
534
      .extension()
535
      .messageStore(messageId)
536
      .put(body);
537
    return updateRequest;
17✔
538
  }
539

540
  async deleteMessageApi(messageId: string) {
541
    const response: string = await this._deps.client
2✔
542
      .account()
543
      .extension()
544
      .messageStore(messageId)
545
      .delete();
546
    return response;
2✔
547
  }
548

549
  /**
550
   * Batch update messages status
551
   */
552
  async _batchUpdateMessagesApi(
553
    messageIds: Message['id'][],
554
    body: {
555
      body: {
556
        readStatus: Message['readStatus'];
557
      };
558
    }[],
559
  ) {
560
    // Not to request when there're no messages
561
    if (!messageIds || messageIds.length === 0) {
×
562
      return;
×
563
    }
564

565
    const ids = decodeURIComponent(messageIds.join(','));
×
566
    const platform = this._deps.client.service.platform();
×
567
    const responses: Response[] = await batchPutApi({
×
568
      platform,
569
      url: `/restapi/v1.0/account/~/extension/~/message-store/${ids}`,
570
      body,
571
    });
572
    return responses;
×
573
  }
574

575
  /**
576
   * Change messages' status to `READ` or `UNREAD`.
577
   * Update 20 messages per time with `_batchUpdateMessagesApi`,
578
   * or `_updateMessageApi` one by one in recursion.
579
   */
580
  async _updateMessagesApi(
581
    messageIds: Message['id'][],
582
    status: Message['readStatus'],
583
  ) {
584
    const allMessageIds = messageIds;
13✔
585
    if (!allMessageIds || allMessageIds.length === 0) {
13!
586
      return [];
×
587
    }
588

589
    const results: GetMessageInfoResponse[] = [];
13✔
590

591
    for (let index = 0; ; index++) {
13✔
592
      let nextLength = (index + 1) * UPDATE_MESSAGE_ONCE_COUNT;
13✔
593

594
      if (nextLength > allMessageIds.length) {
13!
595
        nextLength = allMessageIds.length - index * UPDATE_MESSAGE_ONCE_COUNT;
13✔
596
      } else {
597
        nextLength = UPDATE_MESSAGE_ONCE_COUNT;
×
598
      }
599

600
      // If there's only one message, use another api to update its status
601
      if (nextLength === 1) {
13!
602
        // @ts-expect-error TS(2345): Argument of type 'number | undefined' is not assig... Remove this comment to see the full error message
603
        const result = await this._updateMessageApi(messageIds[0], status);
13✔
604
        return [result];
13✔
605
      }
606

607
      const leftIds = allMessageIds.slice(
×
608
        index * UPDATE_MESSAGE_ONCE_COUNT,
609
        index * UPDATE_MESSAGE_ONCE_COUNT + nextLength,
610
      );
611

612
      const body = leftIds.map(() => ({ body: { readStatus: status } }));
×
613
      const responses = await this._batchUpdateMessagesApi(leftIds, body);
×
614
      await Promise.all(
×
615
        // @ts-expect-error TS(2532): Object is possibly 'undefined'.
616
        responses.map(async (res) => {
617
          if (res.status === 200) {
×
618
            const result = await res.json();
×
619
            results.push(result);
×
620
          }
621
        }),
622
      );
623

624
      const { ownerId } = this._deps.auth;
×
625
      if (allMessageIds.length > (index + 1) * UPDATE_MESSAGE_ONCE_COUNT) {
×
626
        await sleep(1300);
×
627
        // Check if owner ID has been changed. If it is, cancel this update.
628
        if (ownerId !== this._deps.auth.ownerId) {
×
629
          return [];
×
630
        }
631
      } else {
632
        break;
×
633
      }
634
    }
635

636
    return results;
×
637
  }
638

639
  /**
640
   * Set message status to `READ`.
641
   */
642
  @proxify
643
  async readMessages(conversationId: Message['conversationId']) {
644
    this._debouncedSetConversationAsRead(conversationId);
81✔
645
  }
646

647
  _debouncedSetConversationAsRead = debounce({
344✔
648
    fn: this._setConversationAsRead,
649
    threshold: 500,
650
    leading: true,
651
  });
652

653
  async _setConversationAsRead(conversationId: Message['conversationId']) {
654
    // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
655
    const messageList = this.conversationStore[conversationId];
58✔
656
    if (!messageList || messageList.length === 0) {
58✔
657
      return;
23✔
658
    }
659
    const unreadMessageIds = messageList
35✔
660
      .filter(messageHelper.messageIsUnread)
661
      .map((m: any) => m.id);
13✔
662
    if (unreadMessageIds.length === 0) {
35✔
663
      return;
22✔
664
    }
665
    try {
13✔
666
      const { ownerId } = this._deps.auth;
13✔
667
      const updatedMessages = await this._updateMessagesApi(
13✔
668
        unreadMessageIds,
669
        'Read',
670
      );
671

672
      if (ownerId !== this._deps.auth.ownerId) {
13!
673
        return;
×
674
      }
675

676
      this.pushMessages(updatedMessages);
13✔
677
    } catch (error: any /** TODO: confirm with instanceof */) {
678
      console.error(error);
×
679

680
      if (
×
681
        !this._deps.availabilityMonitor ||
×
682
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
683
      ) {
684
        this._deps.alert.warning({ message: messageStoreErrors.readFailed });
×
685
      }
686
    }
687
  }
688

689
  /**
690
   * Set message status to `UNREAD`.
691
   */
692
  @proxify
693
  async unreadMessage(messageId: string) {
694
    this.onUnmarkMessages();
4✔
695
    try {
4✔
696
      const message = await this._updateMessageApi(messageId, 'Unread');
4✔
697
      this.pushMessage(message);
4✔
698
    } catch (error: any /** TODO: confirm with instanceof */) {
699
      console.error(error);
×
700

701
      if (
×
702
        !this._deps.availabilityMonitor ||
×
703
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
704
      ) {
705
        this._deps.alert.warning({ message: messageStoreErrors.unreadFailed });
×
706
      }
707
    }
708
  }
709

710
  @track(trackEvents.flagVoicemail)
711
  @proxify
712
  async onUnmarkMessages() {
713
    //  for track mark message
714
  }
715

716
  @track((that: MessageStore, conversationId: Message['conversationId']) => {
717
    // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
718
    const [conversation] = that.conversationStore[conversationId] ?? [];
2!
719
    if (!conversation) return;
2!
720
    if (conversation.type === 'VoiceMail') {
2✔
721
      return [trackEvents.deleteVoicemail];
1✔
722
    }
723
    if (conversation.type === 'Fax') {
1!
724
      return [trackEvents.deleteFax];
1✔
725
    }
726
  })
727
  @proxify
728
  async onDeleteConversation(conversationId: Message['conversationId']) {
729
    //  for track delete message
730
  }
731

732
  _deleteConversationStore(conversationId: Message['conversationId']) {
733
    // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
734
    if (!this.conversationStore[conversationId]) {
2!
735
      return this.conversationStore;
×
736
    }
737
    const newState = { ...this.conversationStore };
2✔
738
    // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
739
    delete newState[conversationId];
2✔
740
    return newState;
2✔
741
  }
742

743
  _deleteConversation(conversationId: Message['conversationId']) {
744
    const conversationList = (this.data?.conversationList ?? []).filter(
2!
745
      (c) => c.id !== conversationId,
3✔
746
    );
747
    this.onDeleteConversation(conversationId);
2✔
748
    const conversationStore = this._deleteConversationStore(conversationId);
2✔
749
    this._deps.dataFetcherV2.updateData(
2✔
750
      this._source,
751
      {
752
        ...this.data,
753
        conversationList,
754
        conversationStore,
755
      },
756
      // @ts-expect-error TS(2345): Argument of type 'number | null' is not assignable... Remove this comment to see the full error message
757
      this.timestamp,
758
    );
759
  }
760

761
  @proxify
762
  async deleteConversationMessages(conversationId: Message['conversationId']) {
763
    if (!conversationId) {
2!
764
      return;
×
765
    }
766
    const messageList = this.conversationStore[conversationId];
2✔
767
    if (!messageList || messageList.length === 0) {
2!
768
      return;
×
769
    }
770
    const messageId = messageList.map((m) => m.id).join(',');
2✔
771
    try {
2✔
772
      await this.deleteMessageApi(messageId);
2✔
773
      this._deleteConversation(conversationId);
2✔
774
    } catch (error: any /** TODO: confirm with instanceof */) {
775
      console.error(error);
×
776

777
      if (
×
778
        !this._deps.availabilityMonitor ||
×
779
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
780
      ) {
781
        this._deps.alert.warning({ message: messageStoreErrors.deleteFailed });
×
782
      }
783
    }
784
  }
785

786
  @proxify
787
  async deleteConversation(conversationId: Message['conversationId']) {
788
    if (!conversationId) {
×
789
      return;
×
790
    }
791
    try {
×
792
      await this._deps.client.account().extension().messageStore().delete({
×
793
        conversationId,
794
      });
795
      this._deleteConversation(conversationId);
×
796
    } catch (error: any /** TODO: confirm with instanceof */) {
797
      console.error(error);
×
798

799
      if (
×
800
        !this._deps.availabilityMonitor ||
×
801
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
802
      ) {
803
        this._deps.alert.warning({ message: messageStoreErrors.deleteFailed });
×
804
      }
805
    }
806
  }
807

808
  @track(trackEvents.clickToSMSVoicemailList)
809
  @proxify
810
  async onClickToSMS() {
811
    // for track click to sms in message list
812
  }
813

814
  @track((_: MessageStore, action: { fromType?: Message['type'] }) => {
815
    if (action.fromType === 'Pager' || action.fromType === 'SMS') {
4✔
816
      return [trackEvents.clickToDialTextList];
3✔
817
    }
818
    if (action.fromType === 'VoiceMail') {
1!
819
      return [trackEvents.clickToDialVoicemailList];
1✔
820
    }
821
  })
822
  @proxify
823
  async onClickToCall({ fromType = '' }) {
×
824
    // for track click to call in message list
825
    this.onClickToCallWithRingout();
4✔
826
  }
827

828
  @track((that: MessageStore) => {
829
    if (
4!
830
      // TODO: refactor for Analytics
831
      (that.parentModule as any).callingSettings?.callingMode ===
832
      callingModes.ringout
833
    ) {
834
      return [trackEvents.callPlaceRingOutCallSMSHistory];
×
835
    }
836
  })
837
  @proxify
838
  async onClickToCallWithRingout() {
839
    // for track click to call with Ringout in message list
840
  }
841

842
  override get data() {
843
    return this._deps.dataFetcherV2.getData(this._source);
147,686✔
844
  }
845

846
  get timestamp() {
847
    return this._deps.dataFetcherV2.getTimestamp(this._source);
115,554✔
848
  }
849

850
  get syncInfo() {
851
    return this.data?.syncInfo;
374✔
852
  }
853

854
  @computed((that: MessageStore) => [that.data?.conversationStore])
72,401✔
855
  get conversationStore() {
856
    return this.data?.conversationStore || {};
1,075✔
857
  }
858

859
  get _hasPermission() {
860
    return this._deps.appFeatures.hasReadMessagesPermission;
78,529✔
861
  }
862

863
  @computed((that: MessageStore) => [
72,062✔
864
    that.data?.conversationList,
865
    that.conversationStore,
866
  ])
867
  get allConversations(): MessageStoreConversations {
868
    const { conversationList = [] } = this.data ?? {};
910✔
869
    return conversationList.map((conversationItem) => {
910✔
870
      const messageList = this.conversationStore[conversationItem.id] || [];
2,266!
871
      return {
2,266✔
872
        ...messageList[0],
873
        unreadCounts: messageList.filter(messageHelper.messageIsUnread).length,
874
      };
875
    });
876
  }
877

878
  @computed((that: MessageStore) => [that.allConversations])
24,554✔
879
  get textConversations() {
880
    return this.allConversations.filter((conversation) =>
590✔
881
      messageHelper.messageIsTextMessage(conversation),
2,244✔
882
    );
883
  }
884

885
  @computed((that: MessageStore) => [that.textConversations])
24,554✔
886
  get textUnreadCounts() {
887
    return this.textConversations.reduce((a, b) => a + b.unreadCounts, 0);
866✔
888
  }
889

890
  @computed((that: MessageStore) => [that.allConversations])
24,554✔
891
  get faxMessages() {
892
    return this.allConversations.filter((conversation) =>
590✔
893
      messageHelper.messageIsFax(conversation),
2,244✔
894
    );
895
  }
896

897
  @computed((that: MessageStore) => [that.faxMessages])
24,554✔
898
  get faxUnreadCounts() {
899
    return this.faxMessages.reduce((a, b) => a + b.unreadCounts, 0);
960✔
900
  }
901

902
  @computed((that: MessageStore) => [that.allConversations])
24,554✔
903
  get voicemailMessages() {
904
    return this.allConversations.filter((conversation) =>
590✔
905
      messageHelper.messageIsVoicemail(conversation),
2,244✔
906
    );
907
  }
908

909
  @computed((that: MessageStore) => [that.voicemailMessages])
24,554✔
910
  get voiceUnreadCounts() {
911
    return this.voicemailMessages.reduce((a, b) => a + b.unreadCounts, 0);
590✔
912
  }
913

914
  @computed((that: MessageStore) => [
24,554✔
915
    that.voiceUnreadCounts,
916
    that.textUnreadCounts,
917
    that.faxUnreadCounts,
918
  ])
919
  get unreadCounts() {
920
    let unreadCounts = 0;
444✔
921
    if (this._deps.appFeatures.hasReadTextPermission) {
444!
922
      unreadCounts += this.textUnreadCounts;
444✔
923
    }
924
    if (this._deps.appFeatures.hasVoicemailPermission) {
444!
925
      unreadCounts += this.voiceUnreadCounts;
444✔
926
    }
927
    if (this._deps.appFeatures.hasReadFaxPermission) {
444!
928
      unreadCounts += this.faxUnreadCounts;
444✔
929
    }
930
    return unreadCounts;
444✔
931
  }
932
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc