• 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

82.49
/packages/ringcentral-widgets/modules/CallControlUI/CallControlUI.ts
1
import callDirections from '@ringcentral-integration/commons/enums/callDirections';
2
import type { NormalizedSession } from '@ringcentral-integration/commons/interfaces/Webphone.interface';
3
import { getWebphoneSessionDisplayName } from '@ringcentral-integration/commons/lib/callLogHelpers';
4
import { Module } from '@ringcentral-integration/commons/lib/di';
5
import { formatNumber } from '@ringcentral-integration/commons/lib/formatNumber';
6
import { callingModes } from '@ringcentral-integration/commons/modules/CallingSettings';
7
import type {
8
  ConferenceCall,
9
  LastCallInfo,
10
} from '@ringcentral-integration/commons/modules/ConferenceCall';
11
import type { Webphone } from '@ringcentral-integration/commons/modules/Webphone';
12
import { sessionStatus } from '@ringcentral-integration/commons/modules/Webphone/sessionStatus';
13
import { RcUIModuleV2, computed } from '@ringcentral-integration/core';
14
import type { ObjectMapValue } from '@ringcentral-integration/core/lib/ObjectMap';
15
import { filter, find, values } from 'ramda';
16

17
import callCtrlLayouts from '../../enums/callCtrlLayouts';
18
import { checkShouldHidePhoneNumber } from '../../lib/checkShouldHidePhoneNumber';
19

20
import type {
21
  CallControlComponentProps,
22
  Deps,
23
} from './CallControlUI.interface';
24
import { getLastCallInfoFromWebphoneSession } from './CallControlUI.interface';
25

26
@Module({
27
  name: 'CallControlUI',
28
  deps: [
29
    'Webphone',
30
    'Locale',
31
    'ContactMatcher',
32
    'RegionSettings',
33
    'Brand',
34
    'ContactSearch',
35
    'CallingSettings',
36
    'ConnectivityManager',
37
    'ForwardingNumber',
38
    'CallMonitor',
39
    'ExtensionInfo',
40
    'AppFeatures',
41
    'AccountInfo',
42
    { dep: 'ConferenceCall', optional: true },
43
    { dep: 'RouterInteraction', optional: true },
44
  ],
45
})
46
export class CallControlUI<T extends Deps = Deps> extends RcUIModuleV2<T> {
47
  constructor(deps: T) {
48
    super({
342✔
49
      deps,
50
    });
51
  }
52

53
  // @ts-expect-error TS(2322): Type 'null' is not assignable to type 'string'.
54
  currentSessionId: string = null;
342✔
55

56
  @computed((that: CallControlUI) => [
2,570✔
57
    that.currentSessionId,
58
    that._deps.webphone.sessions,
59
    that._deps.webphone.activeSession,
60
  ])
61
  get currentSession() {
62
    return (
824✔
63
      (this.currentSessionId
1,705✔
64
        ? find(
65
            (session) => session.id === this.currentSessionId,
820✔
66
            this._deps.webphone.sessions,
67
          )
68
        : this._deps.webphone.activeSession) || ({} as NormalizedSession)
69
    );
70
  }
71

72
  @computed((that: CallControlUI) => [
469✔
73
    that._deps.contactMatcher?.dataMapping,
74
    that.currentSession.from,
75
  ])
76
  get fromMatches() {
77
    return (
117✔
78
      this._deps.contactMatcher?.dataMapping?.[this.currentSession.from] ?? []
232✔
79
    );
80
  }
81

82
  @computed((that: CallControlUI) => [
2,101✔
83
    that._deps.contactMatcher?.dataMapping,
84
    that.currentSession.to,
85
  ])
86
  get toMatches() {
87
    return (
277✔
88
      this._deps.contactMatcher?.dataMapping?.[this.currentSession.to] ?? []
440✔
89
    );
90
  }
91

92
  get callerIdName() {
NEW
93
    return getWebphoneSessionDisplayName(this.currentSession as any);
×
94
  }
95

96
  getUIProps({
97
    params,
98
    showCallQueueName = false,
×
99
    showCallerIdName = false,
2,673✔
100
    showPark = false,
×
101
    children,
102
  }: CallControlComponentProps) {
103
    // @ts-expect-error TS(2322): Type 'string | undefined' is not assignable to typ... Remove this comment to see the full error message
104
    this.currentSessionId = params?.sessionId;
2,673✔
105
    const nameMatches =
106
      this.currentSession.direction === callDirections.outbound
2,673✔
107
        ? this.toMatches
108
        : this.fromMatches;
109

110
    const isWebRTC =
111
      this._deps.callingSettings.callingMode === callingModes.webphone;
2,673✔
112
    const isInboundCall =
113
      this.currentSession.direction === callDirections.inbound;
2,673✔
114

115
    let lastCallInfo = this._deps.conferenceCall?.lastCallInfo;
2,673✔
116

117
    const conferenceCallEquipped =
118
      this._deps.conferenceCall?.hasPermission ?? false;
2,673!
119
    const conferenceData = conferenceCallEquipped
2,673!
120
      ? // @ts-expect-error TS(2532): Object is possibly 'undefined'.
121
        values(this._deps.conferenceCall.conferences)[0]
122
      : undefined;
123
    const isOnConference = conferenceCallEquipped
2,673!
124
      ? // @ts-expect-error TS(2532): Object is possibly 'undefined'.
125
        this._deps.conferenceCall.isConferenceSession(this.currentSession.id)
126
      : false;
127
    const isMerging =
128
      // @ts-expect-error TS(2532): Object is possibly 'undefined'.
129
      conferenceCallEquipped && this._deps.conferenceCall.isMerging;
2,673✔
130
    const conferenceCallId =
131
      conferenceData && isWebRTC ? conferenceData.conference.id : null;
2,673✔
132
    const isConferenceCallOverload =
133
      conferenceData && isWebRTC
2,673✔
134
        ? // @ts-expect-error TS(2532): Object is possibly 'undefined'.
135
          this._deps.conferenceCall.isOverload(conferenceCallId)
136
        : false;
137

138
    const hasConferenceCall = !!conferenceData;
2,673✔
139
    const conferenceCallParties = conferenceCallEquipped
2,673!
140
      ? // @ts-expect-error TS(2532): Object is possibly 'undefined'.
141
        this._deps.conferenceCall.partyProfiles
142
      : undefined;
143

144
    // TODO: investigate whether this can simply use isMerging
145
    const fromSessionId = conferenceCallEquipped
2,673!
146
      ? // @ts-expect-error TS(2532): Object is possibly 'undefined'.
147
        this._deps.conferenceCall.mergingPair?.fromSessionId
148
      : undefined;
149
    const hideChildren =
150
      conferenceCallEquipped &&
2,673✔
151
      !isInboundCall &&
152
      fromSessionId &&
153
      fromSessionId !== this.currentSession.id &&
154
      lastCallInfo &&
155
      lastCallInfo.status !== sessionStatus.finished;
156

157
    if (this.currentSession.warmTransferSessionId) {
2,673!
158
      const warmTransferSession = this._deps.webphone.sessions.find(
×
159
        (session) => session.id === this.currentSession.warmTransferSessionId,
×
160
      );
NEW
161
      lastCallInfo = getLastCallInfoFromWebphoneSession(
×
162
        warmTransferSession!,
163
        this._deps.contactMatcher.dataMapping,
164
      );
165
    }
166

167
    const disableLinks = !!(
2,673✔
168
      this._deps.connectivityManager.isOfflineMode ||
5,334✔
169
      this._deps.connectivityManager.isVoipOnlyMode
170
    );
171

172
    let phoneNumber =
173
      this.currentSession.direction === callDirections.outbound
2,673✔
174
        ? this.currentSession.to
175
        : this.currentSession.from;
176

177
    if (
2,673!
178
      this._deps.appFeatures.isCDCEnabled &&
5,346✔
179
      checkShouldHidePhoneNumber(phoneNumber, nameMatches)
180
    ) {
181
      // @ts-expect-error TS(2322): Type 'null' is not assignable to type 'string'.
182
      phoneNumber = null;
×
183
    }
184

185
    return {
2,673✔
186
      brand: this._deps.brand.name,
187
      nameMatches,
188
      phoneNumber,
189
      currentLocale: this._deps.locale.currentLocale,
190
      session: this.currentSession,
191
      areaCode: this._deps.regionSettings.areaCode,
192
      countryCode: this._deps.regionSettings.countryCode,
193
      showBackButton: true, // callMonitor.calls.length > 0,
194
      searchContactList: this._deps.contactSearch.sortedResult,
195
      showSpinner: isMerging,
196
      conferenceCallEquipped,
197
      hasConferenceCall,
198
      conferenceCallParties,
199
      conferenceCallId,
200
      lastCallInfo,
201
      callerIdName: showCallerIdName ? this.callerIdName : undefined,
2,673!
202
      // TODO: investigate whether it's better to just
203
      // use isMerging and let the component decide whether to display children
204
      children: hideChildren ? null : children,
2,673✔
205
      isOnConference,
206
      isWebRTC,
207
      disableLinks,
208
      isConferenceCallOverload,
209
      disableFlip: this._deps.forwardingNumber.flipNumbers.length === 0,
210
      showCallQueueName,
211
      showPark,
212
      controlBusy: this.currentSession.callStatus === sessionStatus.setup,
213
    };
214
  }
215

216
  getInitialLayout = ({
342✔
217
    conferenceCallEquipped,
218
    isOnConference,
219
    lastCallInfo,
220
    session,
221
  }: {
222
    conferenceCallEquipped: boolean;
223
    isOnConference: boolean;
224
    lastCallInfo?: LastCallInfo;
225
    session?: NormalizedSession;
226
  }) => {
227
    let layout = callCtrlLayouts.normalCtrl;
266✔
228
    // @ts-expect-error TS(2532): Object is possibly 'undefined'.
229
    if (session.warmTransferSessionId) {
266!
230
      return callCtrlLayouts.completeTransferCtrl;
×
231
    }
232
    if (!conferenceCallEquipped) {
266!
233
      return layout;
×
234
    }
235

236
    if (isOnConference) {
266✔
237
      return callCtrlLayouts.conferenceCtrl;
27✔
238
    }
239
    // @ts-expect-error TS(2532): Object is possibly 'undefined'.
240
    const isInboundCall = session.direction === callDirections.inbound;
239✔
241

242
    // @ts-expect-error TS(2532): Object is possibly 'undefined'.
243
    const { fromSessionId } = this._deps.conferenceCall.mergingPair;
239✔
244
    const fromSession = find(
239✔
245
      (x: any) => x.id === fromSessionId,
262✔
246
      this._deps.webphone.sessions,
247
    );
248

249
    const activeSessionId =
250
      this._deps.webphone &&
239✔
251
      this._deps.webphone.activeSession &&
252
      this._deps.webphone.activeSession.id;
253

254
    if (
239✔
255
      !isOnConference &&
688!
256
      !isInboundCall &&
257
      fromSession &&
258
      // @ts-expect-error TS(2532): Object is possibly 'undefined'.
259
      fromSessionId !== session.id &&
260
      lastCallInfo &&
261
      // @ts-expect-error TS(2532): Object is possibly 'undefined'.
262
      (session.callStatus !== sessionStatus.onHold ||
263
        // @ts-expect-error TS(2532): Object is possibly 'undefined'.
264
        (session.callStatus === sessionStatus.onHold &&
265
          // @ts-expect-error TS(2532): Object is possibly 'undefined'.
266
          session.id === activeSessionId))
267
    ) {
268
      // enter merge ctrl page.
269
      layout = callCtrlLayouts.mergeCtrl;
6✔
270
    }
271

272
    return layout;
239✔
273
  };
274

275
  getUIFunctions({
276
    getAvatarUrl,
277
    onBackButtonClick,
278
    phoneTypeRenderer,
279
    phoneSourceNameRenderer,
280
  }: CallControlComponentProps) {
281
    return {
2,570✔
282
      getInitialLayout: this.getInitialLayout,
283
      formatPhone: (phoneNumber: string) =>
284
        formatNumber({
5,520✔
285
          phoneNumber,
286
          areaCode: this._deps.regionSettings.areaCode,
287
          countryCode: this._deps.regionSettings.countryCode,
288
          siteCode: this._deps.extensionInfo?.site?.code ?? '',
11,040✔
289
          isMultipleSiteEnabled: this._deps.extensionInfo.isMultipleSiteEnabled,
290
          maxExtensionLength: this._deps.accountInfo.maxExtensionNumberLength,
291
          isEDPEnabled: this._deps.appFeatures.isEDPEnabled,
292
        }),
293
      onHangup: (
294
        sessionId: string,
295
        layout: ObjectMapValue<typeof callCtrlLayouts>,
296
      ) => {
297
        this._deps.webphone.hangup(sessionId);
8✔
298
        if (layout && layout === callCtrlLayouts.mergeCtrl) {
8!
299
          this._deps.callMonitor.mergeControlClickHangupTrack();
×
300
        }
301
      },
302
      onMute: (sessionId: string) => this._deps.webphone.mute(sessionId),
5✔
303
      onUnmute: (sessionId: string) => this._deps.webphone.unmute(sessionId),
5✔
304
      onHold: (sessionId: string) => this._deps.webphone.hold(sessionId),
18✔
305
      onUnhold: (sessionId: string) => {
306
        this._deps.webphone.unhold(sessionId);
13✔
307
      },
308
      onRecord: (sessionId: string) =>
309
        this._deps.webphone.startRecord(sessionId),
9✔
310
      onStopRecord: (sessionId: string) =>
311
        this._deps.webphone.stopRecord(sessionId),
3✔
312
      sendDTMF: (...args: Parameters<Webphone['sendDTMF']>) =>
313
        this._deps.webphone.sendDTMF(...args),
6✔
314
      updateSessionMatchedContact: (
315
        ...args: Parameters<Webphone['updateSessionMatchedContact']>
316
      ) => this._deps.webphone.updateSessionMatchedContact(...args),
×
317
      getAvatarUrl,
318
      onBackButtonClick,
319
      onFlip: (sessionId: string) => {
320
        // @ts-expect-error TS(2532): Object is possibly 'undefined'.
321
        this._deps.routerInteraction.push(`/flip/${sessionId}`);
4✔
322
      },
323
      onTransfer: (sessionId: string) => {
324
        // @ts-expect-error TS(2532): Object is possibly 'undefined'.
325
        this._deps.routerInteraction.push(`/transfer/${sessionId}/webphone`);
7✔
326
      },
327
      onCompleteTransfer: (sessionId: string) => {
328
        this._deps.webphone.completeWarmTransfer(sessionId);
×
329
      },
330
      onPark: (sessionId: string) => this._deps.webphone.park(sessionId),
×
331
      searchContact: (searchString: string) =>
332
        this._deps.contactSearch.debouncedSearch({ searchString }),
×
333
      phoneTypeRenderer,
334
      phoneSourceNameRenderer,
335
      onAdd: (sessionId: string) => {
336
        // track user click add on call control
337
        this._deps.callMonitor.callControlClickAddTrack();
19✔
338
        const session = find(
19✔
339
          (x: any) => x.id === sessionId,
20✔
340
          this._deps.webphone.sessions,
341
        );
342
        if (
19✔
343
          !session ||
38✔
344
          // @ts-expect-error TS(2532): Object is possibly 'undefined'.
345
          !this._deps.conferenceCall.validateCallRecording(session)
346
        ) {
347
          return;
2✔
348
        }
349
        let fromNumber = this._deps.callingSettings.fromNumber;
17✔
350
        if (session.direction === callDirections.outbound) {
17!
351
          fromNumber = session.fromNumber; // keep the same fromNumber
17✔
352
        }
353
        const otherCalls = filter(
17✔
354
          (call: any) =>
355
            call.webphoneSession && call.webphoneSession.id !== session.id,
35✔
356
          this._deps.callMonitor.allCalls,
357
        );
358
        if (otherCalls.length) {
17✔
359
          // goto 'calls on hold' page
360
          // @ts-expect-error TS(2532): Object is possibly 'undefined'.
361
          this._deps.routerInteraction.push(
10✔
362
            `/conferenceCall/callsOnhold/${fromNumber}/${session.id}`,
363
          );
364
        } else {
365
          if (this._deps.conferenceCall) {
7!
366
            this._deps.conferenceCall.setMergeParty({
7✔
367
              fromSessionId: sessionId,
368
            });
369
          }
370
          // goto dialer directly
371
          // @ts-expect-error TS(2532): Object is possibly 'undefined'.
372
          this._deps.routerInteraction.push(
7✔
373
            `/conferenceCall/dialer/${fromNumber}/${sessionId}`,
374
          );
375
        }
376
      },
377
      onBeforeMerge: (sessionId: string) => {
378
        const session = find(
9✔
379
          (x: any) => x.id === sessionId,
9✔
380
          this._deps.webphone.sessions,
381
        );
382
        if (
9!
383
          !session ||
18✔
384
          // @ts-expect-error TS(2532): Object is possibly 'undefined'.
385
          !this._deps.conferenceCall.validateCallRecording(session)
386
        ) {
387
          return false;
×
388
        }
389
        if (this._deps.conferenceCall) {
9!
390
          const conferenceData = Object.values(
9✔
391
            this._deps.conferenceCall.conferences,
392
          )[0];
393
          if (conferenceData) {
9✔
394
            const conferenceSession = find(
5✔
395
              (x: any) => x.id === conferenceData.sessionId,
10✔
396
              this._deps.webphone.sessions,
397
            );
398
            if (
5✔
399
              conferenceSession &&
10✔
400
              !this._deps.conferenceCall.validateCallRecording(
401
                conferenceSession,
402
              )
403
            ) {
404
              return false;
1✔
405
            }
406
          }
407
        }
408
        return true;
8✔
409
      },
410
      onMerge: async (sessionId: string) => {
411
        // @ts-expect-error TS(2532): Object is possibly 'undefined'.
412
        const sessions = await this._deps.conferenceCall.parseMergingSessions({
8✔
413
          sessionId,
414
        });
415
        if (sessions) {
8!
416
          // @ts-expect-error TS(2532): Object is possibly 'undefined'.
417
          await this._deps.conferenceCall.mergeSessions(sessions);
8✔
418
        }
419
      },
420

421
      gotoParticipantsCtrl: () => {
422
        // @ts-expect-error TS(2532): Object is possibly 'undefined'.
423
        this._deps.routerInteraction.push('/conferenceCall/participants');
2✔
424
        // track user click participant area on call control
425
        this._deps.callMonitor.callControlClickParticipantAreaTrack();
2✔
426
      },
427
      loadConference: (conferenceId: string) => {
428
        if (this._deps.conferenceCall) {
65!
429
          this._deps.conferenceCall.loadConference(conferenceId);
65✔
430
        }
431
      },
432
      closeMergingPair: () => {
UNCOV
433
        return (
×
434
          this._deps.conferenceCall &&
×
435
          this._deps.conferenceCall.closeMergingPair()
436
        );
437
      },
438
      setMergeParty: (...args: Parameters<ConferenceCall['setMergeParty']>) => {
439
        return (
6✔
440
          this._deps.conferenceCall &&
12✔
441
          this._deps.conferenceCall.setMergeParty(...args)
442
        );
443
      },
444
      // user action track functions
445
      afterHideMergeConfirm: () =>
446
        this._deps.callMonitor.confirmMergeClickCloseTrack(),
×
447
      afterConfirmMerge: () =>
448
        this._deps.callMonitor.confirmMergeClickMergeTrack(),
2✔
449
      afterOnMerge: () => this._deps.callMonitor.callControlClickMergeTrack(),
9✔
450
    };
451
  }
452
}
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