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

ringcentral / ringcentral-js-widgets / 5607299609

pending completion
5607299609

push

github

web-flow
sync features and bugfixs from dd3f20d7 (#1730)

8800 of 15728 branches covered (55.95%)

Branch coverage included in aggregate %.

881 of 1290 new or added lines in 248 files covered. (68.29%)

13 existing lines in 7 files now uncovered.

14708 of 22609 relevant lines covered (65.05%)

142084.43 hits per line

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

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

7
import { subscriptionFilters } from '../../enums/subscriptionFilters';
8
import type {
9
  Message,
10
  Messages,
11
  MessageStoreModel,
12
  MessageSyncList,
13
} from '../../interfaces/MessageStore.model';
14
import { batchPutApi } from '../../lib/batchApiHelper';
15
import { debounce } from '../../lib/debounce-throttle';
16
import { Module } from '../../lib/di';
17
import * as messageHelper from '../../lib/messageHelper';
18
import { proxify } from '../../lib/proxy/proxify';
19
import { trackEvents } from '../../enums/trackEvents';
20
import { callingModes } from '../CallingSettings';
21
import { DataFetcherV2Consumer, DataSource } from '../DataFetcherV2';
22
import type {
23
  Deps,
24
  DispatchedMessageIds,
25
  MessageHandler,
26
  MessageStoreConversations,
27
  ProcessRawConversationListOptions,
28
  ProcessRawConversationStoreOptions,
29
  SyncFunctionOptions,
30
} from './MessageStore.interface';
31
import { messageStoreErrors } from './messageStoreErrors';
32
import { getSyncParams } from './messageStoreHelper';
33

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

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

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

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

75
  protected _conversationLoadLength =
76
    this._deps.messageStoreOptions?.conversationLoadLength ??
263!
77
    DEFAULT_CONVERSATION_LOAD_LENGTH;
78

79
  protected _messagesFilter =
80
    this._deps.messageStoreOptions?.messagesFilter ?? DEFAULT_MESSAGES_FILTER;
263✔
81

82
  protected _daySpan =
83
    this._deps.messageStoreOptions?.daySpan ?? DEFAULT_DAY_SPAN;
263!
84

85
  protected _eventEmitter = new EventEmitter();
263✔
86

87
  protected _dispatchedMessageIds: DispatchedMessageIds = [];
263✔
88

89
  // @ts-expect-error
90
  protected _handledRecord: GetMessageInfoResponse[] = null;
263✔
91

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

97
    const {
98
      disableCache = false,
263✔
99
      polling = false,
263✔
100
      timeToRetry = DEFAULT_RETRY,
263✔
101
      pollingInterval = DEFAULT_POLLING_INTERVAL,
263✔
102
      ttl = DEFAULT_TTL,
263✔
103
    } = this._deps.messageStoreOptions ?? {};
263!
104
    // @ts-expect-error
105
    this._source = new DataSource({
263✔
106
      ...this._deps.messageStoreOptions,
107
      key: 'messageStore',
108
      disableCache,
109
      ttl,
110
      polling,
111
      timeToRetry,
112
      pollingInterval,
113
      cleanOnReset: true,
114
      permissionCheckFunction: () => this._hasPermission,
60,486✔
115
      readyCheckFunction: () => this._deps.appFeatures.ready,
75,912✔
116
      fetchFunction: async () => this._syncData(),
269✔
117
    });
118
    this._deps.dataFetcherV2.register(this._source);
263✔
119
  }
120

121
  override onInit() {
122
    if (this._hasPermission) {
265!
123
      this._deps.subscription.subscribe([subscriptionFilters.messageStore]);
265✔
124
    }
125
  }
126

127
  override onInitOnce() {
128
    if (this._deps.connectivityMonitor) {
263!
129
      watch(
263✔
130
        this,
131
        () => this._deps.connectivityMonitor.connectivity,
89,232✔
132
        (newValue) => {
133
          if (this.ready && this._deps.connectivityMonitor.ready && newValue) {
16✔
134
            this._deps.dataFetcherV2.fetchData(this._source);
4✔
135
          }
136
        },
137
      );
138
    }
139
    watch(
263✔
140
      this,
141
      () => this._deps.subscription.message,
89,232✔
142
      (newValue) => {
143
        if (
140✔
144
          !this.ready ||
298✔
145
          (this._deps.tabManager && !this._deps.tabManager.active)
146
        ) {
147
          return;
61✔
148
        }
149
        const accountExtensionEndPoint = /\/message-store$/;
79✔
150
        if (
79✔
151
          newValue &&
168✔
152
          // @ts-expect-error
153
          accountExtensionEndPoint.test(newValue.event) &&
154
          newValue.body?.changes
155
        ) {
156
          this.fetchData({ passive: true });
10✔
157
        }
158
      },
159
    );
160
  }
161

162
  @proxify
163
  async _updateData(data: any, timestamp = Date.now()) {
10✔
164
    this._deps.dataFetcherV2.updateData(this._source, data, timestamp);
10✔
165
  }
166

167
  _processRawConversationList({
168
    records,
169
    conversationStore,
170
    isFSyncSuccess,
171
  }: ProcessRawConversationListOptions) {
172
    const state = this.data?.conversationList || [];
304✔
173
    const newState: MessageStoreModel['conversationList'] = [];
304✔
174
    const stateMap: Record<string, { index: number }> = {};
304✔
175
    if (!isFSyncSuccess) {
304✔
176
      if (!records || records.length === 0) {
39!
177
        return state;
×
178
      }
179
      state.forEach((oldConversation) => {
39✔
180
        newState.push(oldConversation);
385✔
181
        stateMap[oldConversation.id] = {
385✔
182
          index: newState.length - 1,
183
        };
184
      });
185
    }
186
    records.forEach((record) => {
304✔
187
      const message = messageHelper.normalizeRecord(record);
1,089✔
188
      const id = message.conversationId;
1,089✔
189
      const newCreationTime = message.creationTime;
1,089✔
190
      const isDeleted = messageHelper.messageIsDeleted(message);
1,089✔
191
      // @ts-expect-error
192
      if (stateMap[id]) {
1,089✔
193
        // @ts-expect-error
194
        const oldConversation = newState[stateMap[id].index];
29✔
195
        const creationTime = oldConversation.creationTime;
29✔
196
        // @ts-expect-error
197
        if (creationTime < newCreationTime && !isDeleted) {
29✔
198
          // @ts-expect-error
199
          newState[stateMap[id].index] = {
13✔
200
            // @ts-expect-error
201
            id,
202
            // @ts-expect-error
203
            creationTime: newCreationTime,
204
            type: message.type,
205
            messageId: message.id,
206
          };
207
        }
208
        // when user deleted a coversation message
209
        if (isDeleted && message.id === oldConversation.messageId) {
29!
210
          // @ts-expect-error
211
          const oldMessageList = conversationStore[id] || [];
×
212
          const exsitedMessageList = oldMessageList.filter(
×
213
            // @ts-expect-error
214
            (m) => m.id !== message.id,
×
215
          );
216
          if (exsitedMessageList.length > 0) {
×
217
            // @ts-expect-error
218
            newState[stateMap[id].index] = {
×
219
              // @ts-expect-error
220
              id,
221
              creationTime: exsitedMessageList[0].creationTime,
222
              type: exsitedMessageList[0].type,
223
              messageId: exsitedMessageList[0].id,
224
            };
225
            return;
×
226
          }
227
          // when user delete conversation
228
          // @ts-expect-error
229
          newState[stateMap[id].index] = null;
×
230
          // @ts-expect-error
231
          delete stateMap[id];
×
232
        }
233
        return;
29✔
234
      }
235
      if (isDeleted || !messageHelper.messageIsAcceptable(message)) {
1,060!
236
        return;
×
237
      }
238
      newState.push({
1,060✔
239
        // @ts-expect-error
240
        id,
241
        // @ts-expect-error
242
        creationTime: newCreationTime,
243
        type: message.type,
244
        messageId: message.id,
245
      });
246
      // @ts-expect-error
247
      stateMap[id] = {
1,060✔
248
        index: newState.length - 1,
249
      };
250
    });
251
    return newState
304✔
252
      .filter((c) => !!c && typeof c.creationTime === 'number')
1,445✔
253
      .sort(messageHelper.sortByCreationTime);
254
  }
255

256
  _processRawConversationStore({
257
    records,
258
    isFSyncSuccess,
259
  }: ProcessRawConversationStoreOptions) {
260
    const state = this.data?.conversationStore ?? {};
304✔
261
    let newState: MessageStoreModel['conversationStore'] = {};
304✔
262
    const updatedConversations: Record<string, number> = {};
304✔
263
    if (!isFSyncSuccess) {
304✔
264
      if (!records || records.length === 0) {
39!
265
        return state;
×
266
      }
267
      newState = {
39✔
268
        ...state,
269
      };
270
    }
271
    records.forEach((record) => {
304✔
272
      const message = messageHelper.normalizeRecord(record);
1,089✔
273
      const id = message.conversationId;
1,089✔
274
      // @ts-expect-error
275
      const newMessages = newState[id] ? [].concat(newState[id]) : [];
1,089✔
276
      // @ts-expect-error
277
      const oldMessageIndex = newMessages.findIndex((r) => r.id === record.id);
1,089✔
278
      if (messageHelper.messageIsDeleted(message)) {
1,089!
279
        // @ts-expect-error
280
        newState[id] = newMessages.filter((m) => m.id !== message.id);
×
281
        // @ts-expect-error
282
        if (newState[id].length === 0) {
×
283
          // @ts-expect-error
284
          delete newState[id];
×
285
        }
286
        return;
×
287
      }
288
      if (oldMessageIndex > -1) {
1,089✔
289
        if (
25✔
290
          // @ts-expect-error
291
          newMessages[oldMessageIndex].lastModifiedTime <
292
          // @ts-expect-error
293
          message.lastModifiedTime
294
        ) {
295
          // @ts-expect-error
296
          newMessages[oldMessageIndex] = message;
16✔
297
        }
298
      } else if (messageHelper.messageIsAcceptable(message)) {
1,064!
299
        // @ts-expect-error
300
        newMessages.push(message);
1,064✔
301
      }
302
      // @ts-expect-error
303
      updatedConversations[id] = 1;
1,089✔
304
      // @ts-expect-error
305
      newState[id] = newMessages;
1,089✔
306
    });
307
    Object.keys(updatedConversations).forEach((id) => {
304✔
308
      const noSorted = newState[id];
1,085✔
309
      newState[id] = noSorted.sort(messageHelper.sortByCreationTime);
1,085✔
310
    });
311
    return newState;
304✔
312
  }
313

314
  async _syncFunction({
315
    recordCount,
316
    conversationLoadLength,
317
    dateFrom,
318
    dateTo,
319
    syncToken,
320
    receivedRecordsLength = 0,
279✔
321
  }: SyncFunctionOptions): Promise<MessageSyncList> {
322
    const params = getSyncParams({
279✔
323
      recordCount,
324
      conversationLoadLength,
325
      dateFrom,
326
      dateTo,
327
      syncToken,
328
    });
329
    const { records, syncInfo = {} }: MessageSyncList = await this._deps.client
279✔
330
      .account()
331
      .extension()
332
      .messageSync()
333
      .list(params);
334
    receivedRecordsLength += records.length;
279✔
335
    // @ts-expect-error
336
    if (!syncInfo.olderRecordsExist || receivedRecordsLength >= recordCount) {
279!
337
      return { records, syncInfo };
279✔
338
    }
339
    await sleep(500);
×
340
    // @ts-expect-error
341
    const olderDateTo = new Date(records[records.length - 1].creationTime);
×
342
    const olderRecordResult = await this._syncFunction({
×
343
      conversationLoadLength,
344
      dateFrom,
345
      dateTo: olderDateTo,
346
    });
347
    return {
×
348
      records: records.concat(olderRecordResult.records),
349
      syncInfo,
350
    };
351
  }
352

353
  // @ts-expect-error
354
  async _syncData({ dateTo = null as Date, passive = false } = {}) {
817✔
355
    const conversationsLoadLength = this._conversationsLoadLength;
279✔
356
    const conversationLoadLength = this._conversationLoadLength;
279✔
357
    const { ownerId } = this._deps.auth;
279✔
358
    try {
279✔
359
      const dateFrom = new Date();
279✔
360
      dateFrom.setDate(dateFrom.getDate() - this._daySpan);
279✔
361
      let syncToken = dateTo ? null : this.syncInfo?.syncToken;
279!
362
      const recordCount = conversationsLoadLength * conversationLoadLength;
279✔
363
      let data;
364
      try {
279✔
365
        data = await this._syncFunction({
279✔
366
          recordCount,
367
          conversationLoadLength,
368
          dateFrom,
369
          // @ts-expect-error
370
          syncToken,
371
          dateTo,
372
        });
373
      } catch (e: unknown) {
NEW
374
        const error = e as ApiError;
×
375
        if (
×
376
          error.response?.status === 400 &&
×
377
          (await error.response?.clone().json())?.error?.some(
378
            ({ errorCode = '' } = {}) =>
×
NEW
379
              INVALID_TOKEN_ERROR_CODES.includes(errorCode),
×
380
          )
381
        ) {
382
          data = await this._syncFunction({
×
383
            recordCount,
384
            conversationLoadLength,
385
            dateFrom,
386
            // @ts-expect-error
387
            syncToken: null,
388
            dateTo,
389
          });
390
          syncToken = null;
×
391
        } else {
392
          throw error;
×
393
        }
394
      }
395
      if (this._deps.auth.ownerId === ownerId) {
279!
396
        const records = this._messagesFilter(data.records);
279✔
397
        const isFSyncSuccess = !syncToken;
279✔
398
        // this is only executed in passive sync mode (aka. invoked by subscription)
399
        if (passive) {
279✔
400
          this._handledRecord = records;
10✔
401
        }
402
        return {
279✔
403
          conversationList: this._processRawConversationList({
404
            records,
405
            conversationStore: this.conversationStore,
406
            isFSyncSuccess,
407
          }),
408
          conversationStore: this._processRawConversationStore({
409
            records,
410
            isFSyncSuccess,
411
          }),
412
          syncInfo: data.syncInfo,
413
        };
414
      }
415
    } catch (error: any /** TODO: confirm with instanceof */) {
416
      if (this._deps.auth.ownerId === ownerId) {
×
417
        console.error(error);
×
418
        throw error;
×
419
      }
420
    }
421
  }
422

423
  @proxify
424
  override async fetchData({ passive = false } = {}) {
×
425
    const data = await this._syncData({ passive });
10✔
426
    this._updateData(data);
10✔
427
    if (passive && this._handledRecord) {
10!
428
      this._dispatchMessageHandlers(this._handledRecord);
10✔
429
      // @ts-expect-error
430
      this._handledRecord = null;
10✔
431
    }
432
  }
433

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

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

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

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

502
  @proxify
503
  async pushMessages(records: GetMessageInfoResponse[]) {
504
    this._deps.dataFetcherV2.updateData(
25✔
505
      this._source,
506
      {
507
        ...this.data,
508
        conversationList: this._processRawConversationList({
509
          records,
510
          conversationStore: this.conversationStore,
511
        }),
512
        conversationStore: this._processRawConversationStore({
513
          records,
514
        }),
515
      },
516
      // @ts-expect-error
517
      this.timestamp,
518
    );
519
  }
520

521
  pushMessage(record: GetMessageInfoResponse) {
522
    this.pushMessages([record]);
2✔
523
  }
524

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

537
  async deleteMessageApi(messageId: string) {
538
    const response: string = await this._deps.client
×
539
      .account()
540
      .extension()
541
      .messageStore(messageId)
542
      .delete();
543
    return response;
×
544
  }
545

546
  sliceConversations() {
547
    const conversationIds = Object.keys(this.conversationStore);
×
548
    const messages = conversationIds.reduce(
×
549
      (acc, id) => acc.concat(this.conversationStore[id]),
×
550
      [] as Messages,
551
    );
552
    const messageIds = this._messagesFilter(messages).map(
×
553
      (item: Message) => item.id,
×
554
    );
555
    const conversationList = (this.data?.conversationList ?? []).filter(
×
556
      ({ messageId }) => messageIds.indexOf(messageId) > -1,
×
557
    );
558
    const conversationStore = Object.keys(this.conversationStore).reduce(
×
559
      (acc, key) => {
560
        const messages = this.conversationStore[key];
×
561
        const persist = messages.filter(
×
562
          ({ id }) => messageIds.indexOf(id) > -1,
×
563
        );
564
        if (!persist.length) {
×
565
          return acc;
×
566
        }
567
        acc[key] = persist;
×
568
        return acc;
×
569
      },
570
      {} as Record<string, Messages>,
571
    );
572
    this._deps.dataFetcherV2.updateData(
×
573
      this._source,
574
      {
575
        ...this.data,
576
        conversationList,
577
        conversationStore,
578
      },
579
      // @ts-expect-error
580
      this.timestamp,
581
    );
582
  }
583

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

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

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

624
    const results: GetMessageInfoResponse[] = [];
6✔
625

626
    for (let index = 0; ; index++) {
6✔
627
      let nextLength = (index + 1) * UPDATE_MESSAGE_ONCE_COUNT;
6✔
628

629
      if (nextLength > allMessageIds.length) {
6!
630
        nextLength = allMessageIds.length - index * UPDATE_MESSAGE_ONCE_COUNT;
6✔
631
      } else {
632
        nextLength = UPDATE_MESSAGE_ONCE_COUNT;
×
633
      }
634

635
      // If there's only one message, use another api to update its status
636
      if (nextLength === 1) {
6!
637
        // @ts-expect-error
638
        const result = await this._updateMessageApi(messageIds[0], status);
6✔
639
        return [result];
6✔
640
      }
641

642
      const leftIds = allMessageIds.slice(
×
643
        index * UPDATE_MESSAGE_ONCE_COUNT,
644
        index * UPDATE_MESSAGE_ONCE_COUNT + nextLength,
645
      );
646

647
      const body = leftIds.map(() => ({ body: { readStatus: status } }));
×
648
      const responses = await this._batchUpdateMessagesApi(leftIds, body);
×
649
      await Promise.all(
×
650
        // @ts-expect-error
651
        responses.map(async (res) => {
652
          if (res.status === 200) {
×
653
            const result = await res.json();
×
654
            results.push(result);
×
655
          }
656
        }),
657
      );
658

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

671
    return results;
×
672
  }
673

674
  /**
675
   * Set message status to `READ`.
676
   */
677
  @proxify
678
  async readMessages(conversationId: Message['conversationId']) {
679
    this._debouncedSetConversationAsRead(conversationId);
69✔
680
  }
681

682
  _debouncedSetConversationAsRead = debounce({
263✔
683
    fn: this._setConversationAsRead,
684
    threshold: 500,
685
    leading: true,
686
  });
687

688
  async _setConversationAsRead(conversationId: Message['conversationId']) {
689
    // @ts-expect-error
690
    const messageList = this.conversationStore[conversationId];
46✔
691
    if (!messageList || messageList.length === 0) {
46✔
692
      return;
21✔
693
    }
694
    const unreadMessageIds = messageList
25✔
695
      .filter(messageHelper.messageIsUnread)
696
      // @ts-expect-error
697
      .map((m) => m.id);
6✔
698
    if (unreadMessageIds.length === 0) {
25✔
699
      return;
19✔
700
    }
701
    try {
6✔
702
      const { ownerId } = this._deps.auth;
6✔
703
      const updatedMessages = await this._updateMessagesApi(
6✔
704
        unreadMessageIds,
705
        'Read',
706
      );
707

708
      if (ownerId !== this._deps.auth.ownerId) {
6!
709
        return;
×
710
      }
711

712
      this.pushMessages(updatedMessages);
6✔
713
    } catch (error: any /** TODO: confirm with instanceof */) {
714
      console.error(error);
×
715

716
      if (
×
717
        !this._deps.availabilityMonitor ||
×
718
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
719
      ) {
720
        this._deps.alert.warning({ message: messageStoreErrors.readFailed });
×
721
      }
722
    }
723
  }
724

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

737
      if (
×
738
        !this._deps.availabilityMonitor ||
×
739
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
740
      ) {
741
        this._deps.alert.warning({ message: messageStoreErrors.unreadFailed });
×
742
      }
743
    }
744
  }
745

746
  @track(trackEvents.flagVoicemail)
747
  @proxify
748
  async onUnmarkMessages() {
749
    //  for track mark message
750
  }
751

752
  @track((that: MessageStore, conversationId: Message['conversationId']) => {
753
    // @ts-expect-error
754
    const [conversation] = that.conversationStore[conversationId] ?? [];
×
755
    if (!conversation) return;
×
756
    if (conversation.type === 'VoiceMail') {
×
757
      return [trackEvents.deleteVoicemail];
×
758
    }
759
    if (conversation.type === 'Fax') {
×
760
      return [trackEvents.deleteFax];
×
761
    }
762
  })
763
  @proxify
764
  async onDeleteConversation(conversationId: Message['conversationId']) {
765
    //  for track delete message
766
  }
767

768
  _deleteConversationStore(conversationId: Message['conversationId']) {
769
    // @ts-expect-error
770
    if (!this.conversationStore[conversationId]) {
×
771
      return this.conversationStore;
×
772
    }
773
    const newState = { ...this.conversationStore };
×
774
    // @ts-expect-error
775
    delete newState[conversationId];
×
776
    return newState;
×
777
  }
778

779
  _deleteConversation(conversationId: Message['conversationId']) {
780
    const conversationList = (this.data?.conversationList ?? []).filter(
×
781
      (c) => c.id !== conversationId,
×
782
    );
783
    this.onDeleteConversation(conversationId);
×
784
    const conversationStore = this._deleteConversationStore(conversationId);
×
785
    this._deps.dataFetcherV2.updateData(
×
786
      this._source,
787
      {
788
        ...this.data,
789
        conversationList,
790
        conversationStore,
791
      },
792
      // @ts-expect-error
793
      this.timestamp,
794
    );
795
  }
796

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

813
      if (
×
814
        !this._deps.availabilityMonitor ||
×
815
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
816
      ) {
817
        this._deps.alert.warning({ message: messageStoreErrors.deleteFailed });
×
818
      }
819
    }
820
  }
821

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

835
      if (
×
836
        !this._deps.availabilityMonitor ||
×
837
        !(await this._deps.availabilityMonitor.checkIfHAError(error))
838
      ) {
839
        this._deps.alert.warning({ message: messageStoreErrors.deleteFailed });
×
840
      }
841
    }
842
  }
843

844
  @track(trackEvents.clickToSMSVoicemailList)
845
  @proxify
846
  async onClickToSMS() {
847
    // for track click to sms in message list
848
  }
849

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

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

878
  override get data() {
879
    return this._deps.dataFetcherV2.getData(this._source);
107,397✔
880
  }
881

882
  get timestamp() {
883
    return this._deps.dataFetcherV2.getTimestamp(this._source);
83,368✔
884
  }
885

886
  get syncInfo() {
887
    return this.data?.syncInfo;
279✔
888
  }
889

890
  @computed((that: MessageStore) => [that.data?.conversationStore])
52,661✔
891
  get conversationStore() {
892
    return this.data?.conversationStore || {};
799✔
893
  }
894

895
  get _hasPermission() {
896
    return this._deps.appFeatures.hasReadMessagesPermission;
60,751✔
897
  }
898

899
  @computed((that: MessageStore) => [
52,398✔
900
    that.data?.conversationList,
901
    that.conversationStore,
902
  ])
903
  get allConversations(): MessageStoreConversations {
904
    const { conversationList = [] } = this.data ?? {};
627✔
905
    return conversationList.map((conversationItem) => {
627✔
906
      const messageList = this.conversationStore[conversationItem.id] || [];
1,445!
907
      return {
1,445✔
908
        ...messageList[0],
909
        unreadCounts: messageList.filter(messageHelper.messageIsUnread).length,
910
      };
911
    });
912
  }
913

914
  @computed((that: MessageStore) => [that.allConversations])
14,515✔
915
  get textConversations() {
916
    return this.allConversations.filter((conversation) =>
396✔
917
      messageHelper.messageIsTextMessage(conversation),
1,442✔
918
    );
919
  }
920

921
  @computed((that: MessageStore) => [that.textConversations])
14,515✔
922
  get textUnreadCounts() {
923
    return this.textConversations.reduce((a, b) => a + b.unreadCounts, 0);
714✔
924
  }
925

926
  @computed((that: MessageStore) => [that.allConversations])
14,515✔
927
  get faxMessages() {
928
    return this.allConversations.filter((conversation) =>
396✔
929
      messageHelper.messageIsFax(conversation),
1,442✔
930
    );
931
  }
932

933
  @computed((that: MessageStore) => [that.faxMessages])
14,515✔
934
  get faxUnreadCounts() {
935
    return this.faxMessages.reduce((a, b) => a + b.unreadCounts, 0);
726✔
936
  }
937

938
  @computed((that: MessageStore) => [that.allConversations])
14,515✔
939
  get voicemailMessages() {
940
    return this.allConversations.filter((conversation) =>
396✔
941
      messageHelper.messageIsVoicemail(conversation),
1,442✔
942
    );
943
  }
944

945
  @computed((that: MessageStore) => [that.voicemailMessages])
14,515✔
946
  get voiceUnreadCounts() {
947
    return this.voicemailMessages.reduce((a, b) => a + b.unreadCounts, 0);
396✔
948
  }
949

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