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

ringcentral / ringcentral-js-widgets / 9984535799

18 Jul 2024 02:28AM UTC coverage: 63.055% (+1.7%) from 61.322%
9984535799

push

github

web-flow
misc: sync features and bugfixes from f39b7a45 (#1747)

* misc: sync features and bugfixes from ba8d789

* misc: add i18n-dayjs and react-hooks packages

* version to 0.15.0

* chore: update crius

* misc: sync from f39b7a45

* chore: fix tests

* chore: fix tests

* chore: run test with --updateSnapshot

9782 of 17002 branches covered (57.53%)

Branch coverage included in aggregate %.

2206 of 3368 new or added lines in 501 files covered. (65.5%)

219 existing lines in 52 files now uncovered.

16601 of 24839 relevant lines covered (66.83%)

178566.16 hits per line

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

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

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

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

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

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

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

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

76
  protected _conversationLoadLength =
77
    this._deps.messageStoreOptions?.conversationLoadLength ??
342!
78
    DEFAULT_CONVERSATION_LOAD_LENGTH;
79

80
  protected _messagesFilter =
81
    this._deps.messageStoreOptions?.messagesFilter ?? DEFAULT_MESSAGES_FILTER;
342✔
82

83
  protected _daySpan =
84
    this._deps.messageStoreOptions?.daySpan ?? DEFAULT_DAY_SPAN;
342!
85

86
  protected _eventEmitter = new EventEmitter();
342✔
87

88
  protected _dispatchedMessageIds: DispatchedMessageIds = [];
342✔
89

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

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

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

122
  override onInit() {
123
    if (this._hasPermission) {
345!
124
      this._deps.subscription.subscribe([
345✔
125
        subscriptionFilters.messageStore,
126
        subscriptionFilters.instantMessage,
127
      ]);
128
    }
129
  }
130

131
  override onInitOnce() {
132
    if (this._deps.connectivityMonitor) {
341!
133
      watch(
341✔
134
        this,
135
        () => this._deps.connectivityMonitor.connectivity,
122,759✔
136
        (newValue) => {
137
          if (this.ready && this._deps.connectivityMonitor.ready && newValue) {
51✔
138
            this._deps.dataFetcherV2.fetchData(this._source);
20✔
139
          }
140
        },
141
      );
142
    }
143
    watch(
341✔
144
      this,
145
      () => this._deps.subscription.message,
122,759✔
146
      async (newValue) => {
147
        if (
155✔
148
          !this.ready ||
335✔
149
          (this._deps.tabManager && !this._deps.tabManager.active)
150
        ) {
151
          return;
65✔
152
        }
153
        const messageStoreEvent = /\/message-store$/;
90✔
154
        const instantMessageEvent = /\/message-store\/instant\?type=SMS$/;
90✔
155
        if (
90✔
156
          messageStoreEvent.test(newValue?.event!) &&
100✔
157
          newValue.body?.changes
158
        ) {
159
          try {
10✔
160
            await this.fetchData({ passive: true });
10✔
161
          } catch (ex) {
NEW
162
            console.error('[MessageStore] > subscription > fetchData', ex);
×
163
          }
164
        } else if (instantMessageEvent.test(newValue?.event!)) {
80!
NEW
165
          this.pushMessage(messageHelper.normalizeInstantEvent(newValue));
×
166
        }
167
      },
168
    );
169
  }
170

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

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

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

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

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

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

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

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

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

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

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

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

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

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

551
  sliceConversations() {
552
    const conversationIds = Object.keys(this.conversationStore);
×
553
    const messages = conversationIds.reduce(
×
554
      (acc, id) => acc.concat(this.conversationStore[id]),
×
555
      [] as Messages,
556
    );
557
    const messageIds = this._messagesFilter(messages).map(
×
558
      (item: Message) => item.id,
×
559
    );
560
    const conversationList = (this.data?.conversationList ?? []).filter(
×
561
      ({ messageId }) => messageIds.indexOf(messageId) > -1,
×
562
    );
563
    const conversationStore = Object.keys(this.conversationStore).reduce(
×
564
      (acc, key) => {
565
        const messages = this.conversationStore[key];
×
566
        const persist = messages.filter(
×
567
          ({ id }) => messageIds.indexOf(id) > -1,
×
568
        );
569
        if (!persist.length) {
×
570
          return acc;
×
571
        }
572
        acc[key] = persist;
×
573
        return acc;
×
574
      },
575
      {} as Record<string, Messages>,
576
    );
577
    this._deps.dataFetcherV2.updateData(
×
578
      this._source,
579
      {
580
        ...this.data,
581
        conversationList,
582
        conversationStore,
583
      },
584
      // @ts-expect-error TS(2345): Argument of type 'number | null' is not assignable... Remove this comment to see the full error message
585
      this.timestamp,
586
    );
587
  }
588

589
  /**
590
   * Batch update messages status
591
   */
592
  async _batchUpdateMessagesApi(
593
    messageIds: Message['id'][],
594
    body: {
595
      body: {
596
        readStatus: Message['readStatus'];
597
      };
598
    }[],
599
  ) {
600
    // Not to request when there're no messages
601
    if (!messageIds || messageIds.length === 0) {
×
602
      return;
×
603
    }
604

605
    const ids = decodeURIComponent(messageIds.join(','));
×
606
    const platform = this._deps.client.service.platform();
×
607
    const responses: Response[] = await batchPutApi({
×
608
      platform,
609
      url: `/restapi/v1.0/account/~/extension/~/message-store/${ids}`,
610
      body,
611
    });
612
    return responses;
×
613
  }
614

615
  /**
616
   * Change messages' status to `READ` or `UNREAD`.
617
   * Update 20 messages per time with `_batchUpdateMessagesApi`,
618
   * or `_updateMessageApi` one by one in recursion.
619
   */
620
  async _updateMessagesApi(
621
    messageIds: Message['id'][],
622
    status: Message['readStatus'],
623
  ) {
624
    const allMessageIds = messageIds;
13✔
625
    if (!allMessageIds || allMessageIds.length === 0) {
13!
626
      return [];
×
627
    }
628

629
    const results: GetMessageInfoResponse[] = [];
13✔
630

631
    for (let index = 0; ; index++) {
13✔
632
      let nextLength = (index + 1) * UPDATE_MESSAGE_ONCE_COUNT;
13✔
633

634
      if (nextLength > allMessageIds.length) {
13!
635
        nextLength = allMessageIds.length - index * UPDATE_MESSAGE_ONCE_COUNT;
13✔
636
      } else {
637
        nextLength = UPDATE_MESSAGE_ONCE_COUNT;
×
638
      }
639

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

647
      const leftIds = allMessageIds.slice(
×
648
        index * UPDATE_MESSAGE_ONCE_COUNT,
649
        index * UPDATE_MESSAGE_ONCE_COUNT + nextLength,
650
      );
651

652
      const body = leftIds.map(() => ({ body: { readStatus: status } }));
×
653
      const responses = await this._batchUpdateMessagesApi(leftIds, body);
×
654
      await Promise.all(
×
655
        // @ts-expect-error TS(2532): Object is possibly 'undefined'.
656
        responses.map(async (res) => {
657
          if (res.status === 200) {
×
658
            const result = await res.json();
×
659
            results.push(result);
×
660
          }
661
        }),
662
      );
663

664
      const { ownerId } = this._deps.auth;
×
665
      if (allMessageIds.length > (index + 1) * UPDATE_MESSAGE_ONCE_COUNT) {
×
666
        await sleep(1300);
×
667
        // Check if owner ID has been changed. If it is, cancel this update.
668
        if (ownerId !== this._deps.auth.ownerId) {
×
669
          return [];
×
670
        }
671
      } else {
672
        break;
×
673
      }
674
    }
675

676
    return results;
×
677
  }
678

679
  /**
680
   * Set message status to `READ`.
681
   */
682
  @proxify
683
  async readMessages(conversationId: Message['conversationId']) {
684
    this._debouncedSetConversationAsRead(conversationId);
84✔
685
  }
686

687
  _debouncedSetConversationAsRead = debounce({
342✔
688
    fn: this._setConversationAsRead,
689
    threshold: 500,
690
    leading: true,
691
  });
692

693
  async _setConversationAsRead(conversationId: Message['conversationId']) {
694
    // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
695
    const messageList = this.conversationStore[conversationId];
57✔
696
    if (!messageList || messageList.length === 0) {
57✔
697
      return;
23✔
698
    }
699
    const unreadMessageIds = messageList
34✔
700
      .filter(messageHelper.messageIsUnread)
701
      .map((m: any) => m.id);
13✔
702
    if (unreadMessageIds.length === 0) {
34✔
703
      return;
21✔
704
    }
705
    try {
13✔
706
      const { ownerId } = this._deps.auth;
13✔
707
      const updatedMessages = await this._updateMessagesApi(
13✔
708
        unreadMessageIds,
709
        'Read',
710
      );
711

712
      if (ownerId !== this._deps.auth.ownerId) {
13!
713
        return;
×
714
      }
715

716
      this.pushMessages(updatedMessages);
13✔
717
    } catch (error: any /** TODO: confirm with instanceof */) {
718
      console.error(error);
×
719

720
      if (
×
721
        !this._deps.availabilityMonitor ||
×
722
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
723
      ) {
724
        this._deps.alert.warning({ message: messageStoreErrors.readFailed });
×
725
      }
726
    }
727
  }
728

729
  /**
730
   * Set message status to `UNREAD`.
731
   */
732
  @proxify
733
  async unreadMessage(messageId: string) {
734
    this.onUnmarkMessages();
4✔
735
    try {
4✔
736
      const message = await this._updateMessageApi(messageId, 'Unread');
4✔
737
      this.pushMessage(message);
4✔
738
    } catch (error: any /** TODO: confirm with instanceof */) {
739
      console.error(error);
×
740

741
      if (
×
742
        !this._deps.availabilityMonitor ||
×
743
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
744
      ) {
745
        this._deps.alert.warning({ message: messageStoreErrors.unreadFailed });
×
746
      }
747
    }
748
  }
749

750
  @track(trackEvents.flagVoicemail)
751
  @proxify
752
  async onUnmarkMessages() {
753
    //  for track mark message
754
  }
755

756
  @track((that: MessageStore, conversationId: Message['conversationId']) => {
757
    // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
758
    const [conversation] = that.conversationStore[conversationId] ?? [];
2!
759
    if (!conversation) return;
2!
760
    if (conversation.type === 'VoiceMail') {
2✔
761
      return [trackEvents.deleteVoicemail];
1✔
762
    }
763
    if (conversation.type === 'Fax') {
1!
764
      return [trackEvents.deleteFax];
1✔
765
    }
766
  })
767
  @proxify
768
  async onDeleteConversation(conversationId: Message['conversationId']) {
769
    //  for track delete message
770
  }
771

772
  _deleteConversationStore(conversationId: Message['conversationId']) {
773
    // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
774
    if (!this.conversationStore[conversationId]) {
2!
775
      return this.conversationStore;
×
776
    }
777
    const newState = { ...this.conversationStore };
2✔
778
    // @ts-expect-error TS(2538): Type 'undefined' cannot be used as an index type.
779
    delete newState[conversationId];
2✔
780
    return newState;
2✔
781
  }
782

783
  _deleteConversation(conversationId: Message['conversationId']) {
784
    const conversationList = (this.data?.conversationList ?? []).filter(
2!
785
      (c) => c.id !== conversationId,
3✔
786
    );
787
    this.onDeleteConversation(conversationId);
2✔
788
    const conversationStore = this._deleteConversationStore(conversationId);
2✔
789
    this._deps.dataFetcherV2.updateData(
2✔
790
      this._source,
791
      {
792
        ...this.data,
793
        conversationList,
794
        conversationStore,
795
      },
796
      // @ts-expect-error TS(2345): Argument of type 'number | null' is not assignable... Remove this comment to see the full error message
797
      this.timestamp,
798
    );
799
  }
800

801
  @proxify
802
  async deleteConversationMessages(conversationId: Message['conversationId']) {
803
    if (!conversationId) {
2!
804
      return;
×
805
    }
806
    const messageList = this.conversationStore[conversationId];
2✔
807
    if (!messageList || messageList.length === 0) {
2!
808
      return;
×
809
    }
810
    const messageId = messageList.map((m) => m.id).join(',');
2✔
811
    try {
2✔
812
      await this.deleteMessageApi(messageId);
2✔
813
      this._deleteConversation(conversationId);
2✔
814
    } catch (error: any /** TODO: confirm with instanceof */) {
815
      console.error(error);
×
816

817
      if (
×
818
        !this._deps.availabilityMonitor ||
×
819
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
820
      ) {
821
        this._deps.alert.warning({ message: messageStoreErrors.deleteFailed });
×
822
      }
823
    }
824
  }
825

826
  @proxify
827
  async deleteConversation(conversationId: Message['conversationId']) {
828
    if (!conversationId) {
×
829
      return;
×
830
    }
831
    try {
×
832
      await this._deps.client.account().extension().messageStore().delete({
×
833
        conversationId,
834
      });
835
      this._deleteConversation(conversationId);
×
836
    } catch (error: any /** TODO: confirm with instanceof */) {
837
      console.error(error);
×
838

839
      if (
×
840
        !this._deps.availabilityMonitor ||
×
841
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
842
      ) {
843
        this._deps.alert.warning({ message: messageStoreErrors.deleteFailed });
×
844
      }
845
    }
846
  }
847

848
  @track(trackEvents.clickToSMSVoicemailList)
849
  @proxify
850
  async onClickToSMS() {
851
    // for track click to sms in message list
852
  }
853

854
  @track((_: MessageStore, action: { fromType?: Message['type'] }) => {
855
    if (action.fromType === 'Pager' || action.fromType === 'SMS') {
4✔
856
      return [trackEvents.clickToDialTextList];
3✔
857
    }
858
    if (action.fromType === 'VoiceMail') {
1!
859
      return [trackEvents.clickToDialVoicemailList];
1✔
860
    }
861
  })
862
  @proxify
863
  async onClickToCall({ fromType = '' }) {
×
864
    // for track click to call in message list
865
    this.onClickToCallWithRingout();
4✔
866
  }
867

868
  @track((that: MessageStore) => {
869
    if (
4!
870
      // TODO: refactor for Analytics
871
      (that.parentModule as any).callingSettings?.callingMode ===
872
      callingModes.ringout
873
    ) {
874
      return [trackEvents.callPlaceRingOutCallSMSHistory];
×
875
    }
876
  })
877
  @proxify
878
  async onClickToCallWithRingout() {
879
    // for track click to call with Ringout in message list
880
  }
881

882
  override get data() {
883
    return this._deps.dataFetcherV2.getData(this._source);
149,510✔
884
  }
885

886
  get timestamp() {
887
    return this._deps.dataFetcherV2.getTimestamp(this._source);
114,140✔
888
  }
889

890
  get syncInfo() {
891
    return this.data?.syncInfo;
376✔
892
  }
893

894
  @computed((that: MessageStore) => [that.data?.conversationStore])
73,311✔
895
  get conversationStore() {
896
    return this.data?.conversationStore || {};
1,073✔
897
  }
898

899
  get _hasPermission() {
900
    return this._deps.appFeatures.hasReadMessagesPermission;
81,556✔
901
  }
902

903
  @computed((that: MessageStore) => [
72,974✔
904
    that.data?.conversationList,
905
    that.conversationStore,
906
  ])
907
  get allConversations(): MessageStoreConversations {
908
    const { conversationList = [] } = this.data ?? {};
908✔
909
    return conversationList.map((conversationItem) => {
908✔
910
      const messageList = this.conversationStore[conversationItem.id] || [];
2,285!
911
      return {
2,285✔
912
        ...messageList[0],
913
        unreadCounts: messageList.filter(messageHelper.messageIsUnread).length,
914
      };
915
    });
916
  }
917

918
  @computed((that: MessageStore) => [that.allConversations])
25,181✔
919
  get textConversations() {
920
    return this.allConversations.filter((conversation) =>
586✔
921
      messageHelper.messageIsTextMessage(conversation),
2,242✔
922
    );
923
  }
924

925
  @computed((that: MessageStore) => [that.textConversations])
25,181✔
926
  get textUnreadCounts() {
927
    return this.textConversations.reduce((a, b) => a + b.unreadCounts, 0);
864✔
928
  }
929

930
  @computed((that: MessageStore) => [that.allConversations])
25,181✔
931
  get faxMessages() {
932
    return this.allConversations.filter((conversation) =>
586✔
933
      messageHelper.messageIsFax(conversation),
2,242✔
934
    );
935
  }
936

937
  @computed((that: MessageStore) => [that.faxMessages])
25,181✔
938
  get faxUnreadCounts() {
939
    return this.faxMessages.reduce((a, b) => a + b.unreadCounts, 0);
960✔
940
  }
941

942
  @computed((that: MessageStore) => [that.allConversations])
25,181✔
943
  get voicemailMessages() {
944
    return this.allConversations.filter((conversation) =>
586✔
945
      messageHelper.messageIsVoicemail(conversation),
2,242✔
946
    );
947
  }
948

949
  @computed((that: MessageStore) => [that.voicemailMessages])
25,181✔
950
  get voiceUnreadCounts() {
951
    return this.voicemailMessages.reduce((a, b) => a + b.unreadCounts, 0);
586✔
952
  }
953

954
  @computed((that: MessageStore) => [
25,181✔
955
    that.voiceUnreadCounts,
956
    that.textUnreadCounts,
957
    that.faxUnreadCounts,
958
  ])
959
  get unreadCounts() {
960
    let unreadCounts = 0;
442✔
961
    if (this._deps.appFeatures.hasReadTextPermission) {
442!
962
      unreadCounts += this.textUnreadCounts;
442✔
963
    }
964
    if (this._deps.appFeatures.hasVoicemailPermission) {
442!
965
      unreadCounts += this.voiceUnreadCounts;
442✔
966
    }
967
    if (this._deps.appFeatures.hasReadFaxPermission) {
442!
968
      unreadCounts += this.faxUnreadCounts;
442✔
969
    }
970
    return unreadCounts;
442✔
971
  }
972
}
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