Skip to content

Commit 192eed2

Browse files
authored
Add mobile banner for breakout rooms and handle scenario of being unassigned from breakout room (#5059)
* Use banners on mobile to join breakout room and leave breakout room * Hide join breakout room button while moving to breakout room automatically * cover scenario when user is unassigned from breakout room * refactors * Add option to change style of Banner button * set banner button to primary to join breakout room * final fixes for beta release * fix bad call adapter state bug when breakout room times out by using private property for the origin calll
1 parent a58c2bf commit 192eed2

File tree

12 files changed

+335
-50
lines changed

12 files changed

+335
-50
lines changed

packages/calling-stateful-client/src/BreakoutRoomsSubscriber.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -52,20 +52,16 @@ export class BreakoutRoomsSubscriber {
5252
}
5353

5454
private onBreakoutRoomsUpdated = (eventData: BreakoutRoomsEventData): void => {
55-
if (!eventData.data) {
56-
return;
57-
}
58-
5955
if (eventData.type === 'assignedBreakoutRooms') {
6056
this.onAssignedBreakoutRoomUpdated(eventData.data);
6157
} else if (eventData.type === 'join') {
6258
this.onBreakoutRoomsJoined(eventData.data);
63-
} else if (eventData.type === 'breakoutRoomsSettings') {
59+
} else if (eventData.type === 'breakoutRoomsSettings' && eventData.data) {
6460
this.onBreakoutRoomSettingsUpdated(eventData.data);
6561
}
6662
};
6763

68-
private onAssignedBreakoutRoomUpdated = (breakoutRoom: BreakoutRoom): void => {
64+
private onAssignedBreakoutRoomUpdated = (breakoutRoom?: BreakoutRoom): void => {
6965
const callState = this._context.getState().calls[this._callIdRef.callId];
7066
const currentAssignedBreakoutRoom = callState?.breakoutRooms?.assignedBreakoutRoom;
7167

@@ -75,6 +71,11 @@ export class BreakoutRoomsSubscriber {
7571
return;
7672
}
7773

74+
if (!breakoutRoom) {
75+
this._context.setAssignedBreakoutRoom(this._callIdRef.callId, breakoutRoom);
76+
return;
77+
}
78+
7879
if (
7980
breakoutRoom.state === 'open' &&
8081
currentAssignedBreakoutRoom?.state === 'open' &&

packages/calling-stateful-client/src/CallContext.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -629,7 +629,7 @@ export class CallContext {
629629
}
630630

631631
/* @conditional-compile-remove(breakout-rooms) */
632-
public setAssignedBreakoutRoom(callId: string, breakoutRoom: BreakoutRoom): void {
632+
public setAssignedBreakoutRoom(callId: string, breakoutRoom?: BreakoutRoom): void {
633633
this.modifyState((draft: CallClientState) => {
634634
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
635635
if (call) {

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

+4
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,8 @@ export interface CallCompositeStrings {
873873
invalidMeetingIdentifier: string;
874874
inviteToRoomRemovedDetails?: string;
875875
inviteToRoomRemovedTitle: string;
876+
joinBreakoutRoomBannerButtonLabel: string;
877+
joinBreakoutRoomBannerTitle: string;
876878
joinBreakoutRoomButtonLabel: string;
877879
learnMore: string;
878880
leaveBreakoutRoomAndMeetingButtonLabel: string;
@@ -947,6 +949,8 @@ export interface CallCompositeStrings {
947949
resumeCallButtonLabel: string;
948950
resumingCallButtonAriaLabel: string;
949951
resumingCallButtonLabel: string;
952+
returnFromBreakoutRoomBannerButtonLabel: string;
953+
returnFromBreakoutRoomBannerTitle: string;
950954
returnFromBreakoutRoomButtonLabel: string;
951955
returnToCallButtonAriaDescription?: string;
952956
returnToCallButtonAriaLabel?: string;

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

+22
Original file line numberDiff line numberDiff line change
@@ -887,4 +887,26 @@ export interface CallCompositeStrings {
887887
* Notification title for when a user joins a breakout room
888888
*/
889889
breakoutRoomJoinedNotificationTitle: string;
890+
/* @conditional-compile-remove(breakout-rooms) */
891+
/**
892+
* Title for banner to join the assigned breakout room. The banner is shown in mobile view instead of the
893+
* notification.
894+
*/
895+
joinBreakoutRoomBannerTitle: string;
896+
/* @conditional-compile-remove(breakout-rooms) */
897+
/**
898+
* Label for button in banner to join breakout room. The banner is shown in mobile view instead of the notification.
899+
*/
900+
joinBreakoutRoomBannerButtonLabel: string;
901+
/* @conditional-compile-remove(breakout-rooms) */
902+
/**
903+
* Title for banner to return from breakout room. The banner is shown in mobile view instead of the notification.
904+
*/
905+
returnFromBreakoutRoomBannerTitle: string;
906+
/* @conditional-compile-remove(breakout-rooms) */
907+
/**
908+
* Label for button in banner to return from breakout room. The banner is shown in mobile view instead of the
909+
* notification.
910+
*/
911+
returnFromBreakoutRoomBannerButtonLabel: string;
890912
}

packages/react-composites/src/composites/CallComposite/adapter/AzureCommunicationCallAdapter.ts

+34-29
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,18 @@ class CallContext {
241241
: undefined;
242242
const transferCall = latestAcceptedTransfer ? clientState.calls[latestAcceptedTransfer.callId] : undefined;
243243

244+
/* @conditional-compile-remove(breakout-rooms) */
245+
const originCall = call?.breakoutRooms?.breakoutRoomOriginCallId
246+
? clientState.calls[call?.breakoutRooms?.breakoutRoomOriginCallId]
247+
: latestEndedCall?.breakoutRooms?.breakoutRoomOriginCallId
248+
? clientState.calls[latestEndedCall?.breakoutRooms?.breakoutRoomOriginCallId]
249+
: undefined;
250+
244251
const newPage = getCallCompositePage(
245252
call,
246253
latestEndedCall,
247254
transferCall,
255+
/* @conditional-compile-remove(breakout-rooms) */ originCall,
248256
/* @conditional-compile-remove(unsupported-browser) */ environmentInfo
249257
);
250258
if (!IsCallEndedPage(oldPage) && IsCallEndedPage(newPage)) {
@@ -349,6 +357,8 @@ export class AzureCommunicationCallAdapter<AgentType extends CallAgent | TeamsCa
349357
private emitter: EventEmitter = new EventEmitter();
350358
private callingSoundSubscriber: CallingSoundSubscriber | undefined;
351359
private onClientStateChange: (clientState: CallClientState) => void;
360+
/* @conditional-compile-remove(breakout-rooms) */
361+
private originCall: CallCommon | undefined;
352362

353363
private onResolveVideoBackgroundEffectsDependency?: () => Promise<VideoBackgroundEffectsDependency>;
354364

@@ -644,6 +654,8 @@ export class AzureCommunicationCallAdapter<AgentType extends CallAgent | TeamsCa
644654
: {};
645655
const call = this._joinCall(audioOptions, videoOptions);
646656

657+
/* @conditional-compile-remove(breakout-rooms) */
658+
this.originCall = call;
647659
this.processNewCall(call);
648660
return call;
649661
});
@@ -1057,32 +1069,19 @@ export class AzureCommunicationCallAdapter<AgentType extends CallAgent | TeamsCa
10571069

10581070
/* @conditional-compile-remove(breakout-rooms) */
10591071
public async returnFromBreakoutRoom(): Promise<void> {
1060-
if (this.call === undefined) {
1061-
return;
1072+
if (!this.originCall) {
1073+
throw new Error('Could not return from breakout room because the origin call could not be retrieved.');
10621074
}
10631075

1064-
// Find call state of current call from stateful layer. The current call state of breakout room may not be present in calls array
1065-
// if the breakout room call is ended. So search the callsEnded array as well.
1066-
const callState = this.callClient.getState().calls[this.call?.id]
1067-
? this.callClient.getState().callsEnded[this.call?.id]
1068-
: undefined;
1069-
1070-
// Find origin call id from breakout room call state
1071-
const originCallId = callState?.breakoutRooms?.breakoutRoomOriginCallId;
1072-
1073-
// Find origin call from call agent
1074-
const originCall = this.callAgent?.calls.find((callAgentCall) => {
1075-
return callAgentCall.id === originCallId;
1076-
});
1077-
1078-
if (!originCall) {
1079-
throw new Error('Could not return from breakout room because the origin call could not be retrieved.');
1076+
if (this.call?.id === this.originCall.id) {
1077+
console.error('Return from breakout room will not be done because current call is the origin call.');
1078+
return;
10801079
}
10811080

10821081
const breakoutRoomCall = this.call;
1083-
this.processNewCall(originCall);
1082+
this.processNewCall(this.originCall);
10841083
await this.resumeCall();
1085-
if (breakoutRoomCall?.state === 'Connected') {
1084+
if (breakoutRoomCall?.state && !['Disconnecting', 'Disconnected'].includes(breakoutRoomCall.state)) {
10861085
breakoutRoomCall.hangUp();
10871086
}
10881087
}
@@ -1220,6 +1219,12 @@ export class AzureCommunicationCallAdapter<AgentType extends CallAgent | TeamsCa
12201219
if (this.callingSoundSubscriber) {
12211220
this.callingSoundSubscriber.unsubscribeAll();
12221221
}
1222+
/* @conditional-compile-remove(breakout-rooms) */
1223+
const breakoutRoomsFeature = this.call?.feature(Features.BreakoutRooms);
1224+
/* @conditional-compile-remove(breakout-rooms) */
1225+
if (breakoutRoomsFeature) {
1226+
breakoutRoomsFeature.off('breakoutRoomsUpdated', this.breakoutRoomsUpdated.bind(this));
1227+
}
12231228
}
12241229

12251230
private isMyMutedChanged = (): void => {
@@ -1340,20 +1345,20 @@ export class AzureCommunicationCallAdapter<AgentType extends CallAgent | TeamsCa
13401345

13411346
/* @conditional-compile-remove(breakout-rooms) */
13421347
private breakoutRoomsUpdated(eventData: BreakoutRoomsEventData): void {
1343-
if (eventData.data) {
1344-
if (eventData.type === 'assignedBreakoutRooms') {
1345-
this.assignedBreakoutRoomUpdated(eventData.data);
1346-
} else if (eventData.type === 'join') {
1347-
this.breakoutRoomJoined(eventData.data);
1348-
}
1348+
if (eventData.type === 'assignedBreakoutRooms') {
1349+
this.assignedBreakoutRoomUpdated(eventData.data);
1350+
} else if (eventData.type === 'join') {
1351+
this.breakoutRoomJoined(eventData.data);
13491352
}
1350-
13511353
this.emitter.emit('breakoutRoomsUpdated', eventData);
13521354
}
13531355

13541356
/* @conditional-compile-remove(breakout-rooms) */
1355-
private assignedBreakoutRoomUpdated(breakoutRoom: BreakoutRoom): void {
1356-
if (breakoutRoom.state === 'closed') {
1357+
private assignedBreakoutRoomUpdated(breakoutRoom?: BreakoutRoom): void {
1358+
if (!this.call?.id) {
1359+
return;
1360+
}
1361+
if (this.originCall?.id !== this.call?.id && (!breakoutRoom || breakoutRoom.state === 'closed')) {
13571362
this.returnFromBreakoutRoom();
13581363
}
13591364
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
import { DefaultButton, Theme, mergeStyles } from '@fluentui/react';
5+
import { _pxToRem } from '@internal/acs-ui-common';
6+
// eslint-disable-next-line no-restricted-imports
7+
import { IIconProps, Icon, PrimaryButton, Stack, Text, useTheme } from '@fluentui/react';
8+
import React from 'react';
9+
10+
/**
11+
* Props for {@link Banner}.
12+
*
13+
* @private
14+
*/
15+
export interface BannerProps {
16+
/**
17+
* Banner strings.
18+
*/
19+
strings?: BannerStrings;
20+
21+
/**
22+
* Banner icon.
23+
*/
24+
iconProps?: IIconProps;
25+
26+
/**
27+
* Callback called when the button inside banner is clicked.
28+
*/
29+
onClickButton?: () => void;
30+
31+
/**
32+
* If true, the primary button will be styled as a primary button. Default is false.
33+
*/
34+
primaryButton?: boolean;
35+
}
36+
37+
/**
38+
* All strings that may be shown on the UI in the {@link Banner}.
39+
*
40+
* @private
41+
*/
42+
export interface BannerStrings {
43+
/**
44+
* Banner title.
45+
*/
46+
title: string;
47+
/**
48+
* Banner primary button label.
49+
*/
50+
primaryButtonLabel: string;
51+
}
52+
53+
/**
54+
* A component to show a banner in the UI.
55+
*
56+
* @private
57+
*/
58+
export const Banner = (props: BannerProps): JSX.Element => {
59+
const strings = props.strings;
60+
const theme = useTheme();
61+
62+
return (
63+
<Stack horizontalAlign="center">
64+
<Stack data-ui-id="banner" className={containerStyles(theme)}>
65+
<Stack horizontal horizontalAlign="space-between">
66+
<Stack horizontal>
67+
{props.iconProps?.iconName && (
68+
<Icon className={bannerIconClassName} iconName={props.iconProps?.iconName} {...props.iconProps} />
69+
)}
70+
<Text className={titleTextClassName}>{strings?.title}</Text>
71+
</Stack>
72+
{props.primaryButton ? (
73+
<PrimaryButton
74+
text={strings?.primaryButtonLabel}
75+
ariaLabel={strings?.primaryButtonLabel}
76+
onClick={props.onClickButton}
77+
/>
78+
) : (
79+
<DefaultButton
80+
text={strings?.primaryButtonLabel}
81+
ariaLabel={strings?.primaryButtonLabel}
82+
onClick={props.onClickButton}
83+
/>
84+
)}
85+
</Stack>
86+
</Stack>
87+
</Stack>
88+
);
89+
};
90+
91+
const titleTextClassName = mergeStyles({
92+
fontWeight: 400,
93+
fontSize: _pxToRem(14),
94+
lineHeight: _pxToRem(16),
95+
alignSelf: 'center'
96+
});
97+
98+
const containerStyles = (theme: Theme): string =>
99+
mergeStyles({
100+
boxShadow: theme.effects.elevation8,
101+
width: '20rem',
102+
padding: '0.75rem',
103+
borderRadius: '0.25rem',
104+
position: 'relative',
105+
backgroundColor: theme.palette.white
106+
});
107+
108+
const bannerIconClassName = mergeStyles({
109+
fontSize: '1.25rem',
110+
alignSelf: 'center',
111+
marginRight: '0.5rem',
112+
svg: {
113+
width: '1.25rem',
114+
height: '1.25rem'
115+
}
116+
});

0 commit comments

Comments
 (0)