Skip to content

Commit a178b09

Browse files
[DTMF Dialer] introduce dialpad to composite (#4041)
* Add new screen for dialpadContent * add dialer button * refactor callback type for button action * Change files * Duplicate change files for beta release * fix build * fix cc * Update packages/react-composites CallWithChatComposite browser test snapshots * Update packages/react-composites CallComposite browser test snapshots --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 988a3f1 commit a178b09

127 files changed

Lines changed: 319 additions & 2 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "prerelease",
3+
"area": "feature",
4+
"workstream": "DTMF Dialer",
5+
"comment": "Introduce new DTMF tone screen where you can use the dialpad to send DTMF tones in the call",
6+
"packageName": "@azure/communication-react",
7+
"email": "94866715+dmceachernmsft@users.noreply.github.com",
8+
"dependentChangeType": "patch"
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"type": "prerelease",
3+
"area": "feature",
4+
"workstream": "DTMF Dialer",
5+
"comment": "Introduce new DTMF tone screen where you can use the dialpad to send DTMF tones in the call",
6+
"packageName": "@azure/communication-react",
7+
"email": "94866715+dmceachernmsft@users.noreply.github.com",
8+
"dependentChangeType": "patch"
9+
}

packages/communication-react/review/beta/communication-react.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,7 @@ export type CallCompositeIcons = {
594594
OverflowGalleryTop?: JSX.Element;
595595
LargeGalleryLayout?: JSX.Element;
596596
DefaultCustomButton?: JSX.Element;
597+
DtmfDialpadButton?: JSX.Element;
597598
};
598599

599600
// @public
@@ -1243,6 +1244,7 @@ export type CallWithChatCompositeIcons = {
12431244
PeoplePaneOpenDialpad?: JSX.Element;
12441245
DialpadStartCall?: JSX.Element;
12451246
DefaultCustomButton?: JSX.Element;
1247+
DtmfDialpadButton?: JSX.Element;
12461248
EditBoxCancel?: JSX.Element;
12471249
EditBoxSubmit?: JSX.Element;
12481250
MessageDelivered?: JSX.Element;
@@ -2536,6 +2538,7 @@ export const DEFAULT_COMPOSITE_ICONS: {
25362538
OverflowGalleryTop?: JSX.Element | undefined;
25372539
LargeGalleryLayout?: JSX.Element | undefined;
25382540
DefaultCustomButton?: JSX.Element | undefined;
2541+
DtmfDialpadButton?: JSX.Element | undefined;
25392542
ChevronLeft?: JSX.Element | undefined;
25402543
ControlBarChatButtonActive?: JSX.Element | undefined;
25412544
ControlBarChatButtonInactive?: JSX.Element | undefined;

packages/react-composites/review/beta/react-composites.api.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ export type CallCompositeIcons = {
411411
OverflowGalleryTop?: JSX.Element;
412412
LargeGalleryLayout?: JSX.Element;
413413
DefaultCustomButton?: JSX.Element;
414+
DtmfDialpadButton?: JSX.Element;
414415
};
415416

416417
// @public
@@ -941,6 +942,7 @@ export type CallWithChatCompositeIcons = {
941942
PeoplePaneOpenDialpad?: JSX.Element;
942943
DialpadStartCall?: JSX.Element;
943944
DefaultCustomButton?: JSX.Element;
945+
DtmfDialpadButton?: JSX.Element;
944946
EditBoxCancel?: JSX.Element;
945947
EditBoxSubmit?: JSX.Element;
946948
MessageDelivered?: JSX.Element;
@@ -1482,6 +1484,7 @@ export const DEFAULT_COMPOSITE_ICONS: {
14821484
OverflowGalleryTop?: JSX.Element | undefined;
14831485
LargeGalleryLayout?: JSX.Element | undefined;
14841486
DefaultCustomButton?: JSX.Element | undefined;
1487+
DtmfDialpadButton?: JSX.Element | undefined;
14851488
ChevronLeft?: JSX.Element | undefined;
14861489
ControlBarChatButtonActive?: JSX.Element | undefined;
14871490
ControlBarChatButtonInactive?: JSX.Element | undefined;

packages/react-composites/src/composites/CallComposite/CallComposite.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ import { capabilitiesChangedInfoAndRoleSelector } from './selectors/capabilities
5757
/* @conditional-compile-remove(capabilities) */
5858
import { useTrackedCapabilityChangedNotifications } from './utils/TrackCapabilityChangedNotifications';
5959
import { useEndedCallConsoleErrors } from './utils/useConsoleErrors';
60+
/* @conditional-compile-remove(dtmf-dialer) */
61+
import { DtmfDialpadPage } from './pages/DtmfDialpadPage';
6062

6163
/**
6264
* Props for {@link CallComposite}.
@@ -365,7 +367,8 @@ const MainScreen = (props: MainScreenProps): JSX.Element => {
365367

366368
const [sidePaneRenderer, setSidePaneRenderer] = React.useState<SidePaneRenderer | undefined>();
367369
const [injectedSidePaneProps, setInjectedSidePaneProps] = React.useState<InjectedSidePaneProps>();
368-
370+
/* @conditional-compile-remove(dtmf-dialer) */
371+
const [dialpadScreen, setDialpadScreen] = useState<boolean>(false);
369372
/* @conditional-compile-remove(gallery-layouts) */
370373
const [userSetGalleryLayout, setUserSetGalleryLayout] = useState<VideoGalleryLayout>(
371374
props.options?.galleryOptions?.layout ?? 'floatingLocalVideo'
@@ -575,6 +578,8 @@ const MainScreen = (props: MainScreenProps): JSX.Element => {
575578
userSetOverflowGalleryPosition={userSetOverflowGalleryPosition}
576579
/* @conditional-compile-remove(capabilities) */
577580
capabilitiesChangedNotificationBarProps={capabilitiesChangedNotificationBarProps}
581+
/* @conditional-compile-remove(dtmf-dialer) */
582+
onSetDialpadPage={() => setDialpadScreen(!dialpadScreen)}
578583
/>
579584
);
580585
break;
@@ -619,6 +624,26 @@ const MainScreen = (props: MainScreenProps): JSX.Element => {
619624
break;
620625
}
621626

627+
/* @conditional-compile-remove(dtmf-dialer) */
628+
if (dialpadScreen) {
629+
pageElement = (
630+
<>
631+
<DtmfDialpadPage
632+
mobileView={props.mobileView}
633+
modalLayerHostId={props.modalLayerHostId}
634+
options={props.options}
635+
updateSidePaneRenderer={setSidePaneRenderer}
636+
mobileChatTabHeader={props.mobileChatTabHeader}
637+
latestErrors={latestErrors}
638+
onDismissError={onDismissError}
639+
/* @conditional-compile-remove(capabilities) */
640+
capabilitiesChangedNotificationBarProps={capabilitiesChangedNotificationBarProps}
641+
onSetDialpadPage={() => setDialpadScreen(!dialpadScreen)}
642+
/>
643+
</>
644+
);
645+
}
646+
622647
if (!pageElement) {
623648
throw new Error('Invalid call composite page');
624649
}

packages/react-composites/src/composites/CallComposite/components/CallArrangement.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ export interface CallArrangementProps {
109109
/* @conditional-compile-remove(capabilities) */
110110
capabilitiesChangedNotificationBarProps?: CapabilitiesChangeNotificationBarProps;
111111
onCloseChatPane?: () => void;
112+
/* @conditional-compile-remove(dtmf-dialer) */
113+
onSetDialpadPage?: () => void;
112114
}
113115

114116
/**
@@ -363,6 +365,8 @@ export const CallArrangement = (props: CallArrangementProps): JSX.Element => {
363365
onUserSetGalleryLayout={props.onUserSetGalleryLayoutChange}
364366
/* @conditional-compile-remove(gallery-layouts) */
365367
userSetGalleryLayout={props.userSetGalleryLayout}
368+
/* @conditional-compile-remove(dtmf-dialer) */
369+
onSetDialpadPage={props.onSetDialpadPage}
366370
peopleButtonRef={peopleButtonRef}
367371
cameraButtonRef={cameraButtonRef}
368372
/>

packages/react-composites/src/composites/CallComposite/pages/CallPage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export interface CallPageProps {
5353
/* @conditional-compile-remove(gallery-layouts) */
5454
onSetUserSetOverflowGalleryPosition?: (position: 'Responsive' | 'horizontalTop') => void;
5555
onCloseChatPane?: () => void;
56+
/* @conditional-compile-remove(dtmf-dialer) */
57+
onSetDialpadPage: () => void;
5658
}
5759

5860
/**
@@ -148,6 +150,8 @@ export const CallPage = (props: CallPageProps): JSX.Element => {
148150
userSetGalleryLayout={galleryLayout}
149151
/* @conditional-compile-remove(capabilities) */
150152
capabilitiesChangedNotificationBarProps={props.capabilitiesChangedNotificationBarProps}
153+
/* @conditional-compile-remove(dtmf-dialer) */
154+
onSetDialpadPage={props.onSetDialpadPage}
151155
/>
152156
);
153157
};
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { ActiveErrorMessage, Dialpad, DtmfTone, ErrorBar } from '@internal/react-components';
5+
import { MobileChatSidePaneTabHeaderProps } from '../../common/TabHeader';
6+
import { CallCompositeOptions } from '../CallComposite';
7+
import { SidePaneRenderer } from '../components/SidePane/SidePaneProvider';
8+
import { CapabilitiesChangeNotificationBarProps } from '../components/CapabilitiesChangedNotificationBar';
9+
import { usePropsFor } from '../hooks/usePropsFor';
10+
import { useLocale } from '../../localization';
11+
import { disableCallControls, reduceCallControlsForMobile } from '../utils';
12+
import React, { useRef, useState } from 'react';
13+
import { useAdapter } from '../adapter/CallAdapterProvider';
14+
import { CallArrangement } from '../components/CallArrangement';
15+
import { CommonCallAdapter } from '../adapter';
16+
import { Stack, Text, useTheme } from '@fluentui/react';
17+
import { getReadableTime } from '../utils/timerUtils';
18+
import { DtmfDialpadContentTimerStyles } from '../styles/DtmfDialpadPage.styles';
19+
20+
/**
21+
* @internal
22+
*/
23+
export interface DialpadPageProps {
24+
mobileView: boolean;
25+
options?: CallCompositeOptions;
26+
modalLayerHostId: string;
27+
updateSidePaneRenderer: (renderer: SidePaneRenderer | undefined) => void;
28+
mobileChatTabHeader?: MobileChatSidePaneTabHeaderProps;
29+
latestErrors: ActiveErrorMessage[];
30+
onDismissError: (error: ActiveErrorMessage) => void;
31+
/* @conditional-compile-remove(capabilities) */
32+
capabilitiesChangedNotificationBarProps?: CapabilitiesChangeNotificationBarProps;
33+
onSetDialpadPage: () => void;
34+
}
35+
36+
interface DialpadPageContentProps {
37+
mobileView: boolean;
38+
adapter: CommonCallAdapter;
39+
}
40+
41+
const DtmfDialpadPageContent = (props: DialpadPageContentProps): JSX.Element => {
42+
const { adapter } = props;
43+
44+
const [time, setTime] = useState<number>(0);
45+
const elapsedTime = getReadableTime(time);
46+
const startTime = useRef(performance.now());
47+
const adapterState = adapter.getState();
48+
const theme = useTheme();
49+
50+
const calleeId = adapterState.targetCallees?.[0];
51+
const remoteParticipants = adapterState.call?.remoteParticipants;
52+
let calleeName;
53+
54+
if (remoteParticipants) {
55+
calleeName = Object.values(remoteParticipants).find((p) => p.identifier === calleeId);
56+
}
57+
58+
React.useEffect(() => {
59+
const interval = setInterval(() => {
60+
setTime(performance.now() - startTime.current);
61+
}, 10);
62+
return () => {
63+
clearInterval(interval);
64+
};
65+
}, [startTime]);
66+
67+
return (
68+
<Stack style={{ height: '100%', width: '100%', background: theme.palette.white }}>
69+
<Stack style={{ margin: 'auto' }}>
70+
<Text styles={DtmfDialpadContentTimerStyles}>{elapsedTime}</Text>
71+
<Text>{calleeName !== 'Unnamed participant' ? calleeName : ''}</Text>
72+
<Dialpad
73+
onSendDtmfTone={async (tone: DtmfTone) => {
74+
/* @conditional-compile-remove(dtmf-dialer) */
75+
await adapter.sendDtmfTone(tone);
76+
}}
77+
enableInputEditing={false}
78+
></Dialpad>
79+
</Stack>
80+
</Stack>
81+
);
82+
};
83+
84+
/**
85+
* @internal
86+
*/
87+
export const DtmfDialpadPage = (props: DialpadPageProps): JSX.Element => {
88+
const errorBarProps = usePropsFor(ErrorBar);
89+
const strings = useLocale().strings.call;
90+
const adapter = useAdapter();
91+
92+
let callControlOptions = props.mobileView
93+
? reduceCallControlsForMobile(props.options?.callControls)
94+
: props.options?.callControls;
95+
96+
callControlOptions = disableCallControls(callControlOptions, [
97+
'cameraButton',
98+
'microphoneButton',
99+
'devicesButton',
100+
'screenShareButton',
101+
/* @conditional-compile-remove(PSTN-calls) */
102+
/* @conditional-compile-remove(one-to-n-calling) */
103+
'holdButton'
104+
]);
105+
106+
return (
107+
<CallArrangement
108+
complianceBannerProps={{ strings }}
109+
errorBarProps={props.options?.errorBar !== false && errorBarProps}
110+
callControlProps={{
111+
options: callControlOptions,
112+
increaseFlyoutItemSize: props.mobileView
113+
}}
114+
mobileView={props.mobileView}
115+
modalLayerHostId={props.modalLayerHostId}
116+
onRenderGalleryContent={() => <DtmfDialpadPageContent adapter={adapter} mobileView={props.mobileView} />}
117+
dataUiId={'hold-page'}
118+
updateSidePaneRenderer={props.updateSidePaneRenderer}
119+
mobileChatTabHeader={props.mobileChatTabHeader}
120+
latestErrors={props.latestErrors}
121+
onDismissError={props.onDismissError}
122+
/* @conditional-compile-remove(dtmf-dialer) */
123+
onSetDialpadPage={props.onSetDialpadPage}
124+
/>
125+
);
126+
};
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { ITextStyles } from '@fluentui/react';
5+
import { _pxToRem } from '@internal/acs-ui-common';
6+
7+
/**
8+
* styles for hold pane timer
9+
*
10+
* @private
11+
*/
12+
export const DtmfDialpadContentTimerStyles: ITextStyles = {
13+
root: {
14+
color: 'inherit',
15+
fontWeight: 600,
16+
fontSize: _pxToRem(20),
17+
lineHeight: _pxToRem(28),
18+
margin: 'auto'
19+
}
20+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
/**
5+
* @internal
6+
*/
7+
export const getMinutes = (time: number): number => {
8+
return Math.floor(getSeconds(time) / 60);
9+
};
10+
11+
/**
12+
* @internal
13+
*/
14+
export const getSeconds = (time: number): number => {
15+
return Math.floor(time / 1000);
16+
};
17+
18+
/**
19+
* @internal
20+
*/
21+
export const getHours = (time: number): number => {
22+
return Math.floor(getMinutes(time) / 60);
23+
};
24+
25+
/**
26+
* @internal
27+
*/
28+
export const getReadableTime = (time: number): string => {
29+
const hours = getHours(time);
30+
const readableMinutes = ('0' + (getMinutes(time) % 60)).slice(-2);
31+
const readableSeconds = ('0' + (getSeconds(time) % 60)).slice(-2);
32+
return `${hours > 0 ? hours + ':' : ''}${readableMinutes}:${readableSeconds}`;
33+
};

0 commit comments

Comments
 (0)