Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GA breakout rooms #5584

Draft
wants to merge 18 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "prerelease",
"area": "feature",
"workstream": "Breakout rooms",
"comment": "Promote breakout rooms feature to GA",
"packageName": "@azure/communication-react",
"email": "[email protected]",
"dependentChangeType": "patch"
}
8 changes: 4 additions & 4 deletions common/config/babel/features.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,14 @@ module.exports = {
"teams-identity-support-beta",
// feature for tracking the callParticipantsLocator
"call-participants-locator",
// feature for breakout rooms
"breakout-rooms",
],
stable: [
// Demo feature. Used in live-documentation of conditional compilation.
// Do not use in production code.
"stabilizedDemo",
// Feature for forbid/permit Teams meeting/group call attendee' audio/video access
"media-access"
// Feature for forbid/permit remote participants audio/video access
"media-access",
// Feature for breakout rooms
"breakout-rooms"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,6 @@ export type CreateDefaultCallingHandlers = (
*/
export const createDefaultCallingHandlers: CreateDefaultCallingHandlers = memoizeOne((...args) => {
const [callClient, callAgent, deviceManager, call, options] = args;
/* @conditional-compile-remove(breakout-rooms) */
const callState = call?.id ? callClient.getState().calls[call?.id] : undefined;
/* @conditional-compile-remove(breakout-rooms) */
const breakoutRoomOriginCallId = callState?.breakoutRooms?.breakoutRoomOriginCallId;
/* @conditional-compile-remove(breakout-rooms) */
const breakoutRoomOriginCall = callAgent?.calls.find((call) => call.id === breakoutRoomOriginCallId);
const commonCallingHandlers = createDefaultCommonCallingHandlers(callClient, deviceManager, call, options);
return {
...commonCallingHandlers,
Expand Down Expand Up @@ -116,11 +110,7 @@ export const createDefaultCallingHandlers: CreateDefaultCallingHandlers = memoiz
if (incomingCall) {
await incomingCall.reject();
}
},
/* @conditional-compile-remove(breakout-rooms) */
onHangUp: breakoutRoomOriginCall
? async () => breakoutRoomOriginCall.hangUp().then(() => commonCallingHandlers.onHangUp())
: commonCallingHandlers.onHangUp
}
};
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,13 @@ export const notificationStackSelector: NotificationStackSelector = createSelect
});
}
/* @conditional-compile-remove(breakout-rooms) */
if (latestNotifications['assignedBreakoutRoomClosed']) {
activeNotifications.push({
type: 'assignedBreakoutRoomClosed',
timestamp: latestNotifications['assignedBreakoutRoomClosed'].timestamp
});
}
/* @conditional-compile-remove(breakout-rooms) */
if (latestNotifications['breakoutRoomJoined']) {
activeNotifications.push({
type: 'breakoutRoomJoined',
Expand Down
63 changes: 39 additions & 24 deletions packages/calling-stateful-client/src/BreakoutRoomsSubscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,39 +82,52 @@ export class BreakoutRoomsSubscriber {
return;
}

// TODO: Fix the condition in this if statement to check for different breakout room ID instead of the display name
// when Calling SDK fixes the breakout room call id to be correct
if (
breakoutRoom.state === 'open' &&
currentAssignedBreakoutRoom?.state === 'open' &&
currentAssignedBreakoutRoom?.call?.id !== breakoutRoom.call?.id
callState.breakoutRooms?.breakoutRoomDisplayName &&
callState.breakoutRooms.breakoutRoomDisplayName !== breakoutRoom.displayName
) {
if (
!this._context.getState().latestNotifications['assignedBreakoutRoomOpened'] &&
!this._context.getState().latestNotifications['assignedBreakoutRoomOpenedPromptJoin']
) {
this._context.setLatestNotification(this._callIdRef.callId, {
target: 'assignedBreakoutRoomChanged',
timestamp: new Date(Date.now())
});
}
} else if (breakoutRoom.state === 'open') {
if (!this._context.getState().latestNotifications['assignedBreakoutRoomChanged']) {
const target: NotificationTarget =
breakoutRoom.autoMoveParticipantToBreakoutRoom === false
? 'assignedBreakoutRoomOpenedPromptJoin'
: 'assignedBreakoutRoomOpened';
this._context.setLatestNotification(this._callIdRef.callId, { target, timestamp: new Date(Date.now()) });
}
} else if (breakoutRoom.state === 'closed' && currentAssignedBreakoutRoom?.state === 'closed') {
this._context.setLatestNotification(this._callIdRef.callId, {
target: 'assignedBreakoutRoomChanged',
timestamp: new Date(Date.now())
});
} else if (
breakoutRoom.state === 'open' &&
!callState?.breakoutRooms?.breakoutRoomSettings &&
this._context.getOpenBreakoutRoom() === undefined
) {
const target: NotificationTarget =
breakoutRoom.autoMoveParticipantToBreakoutRoom === false
? 'assignedBreakoutRoomOpenedPromptJoin'
: 'assignedBreakoutRoomOpened';
this._context.setLatestNotification(this._callIdRef.callId, { target, timestamp: new Date(Date.now()) });
} else if (breakoutRoom.state === 'closed' && currentAssignedBreakoutRoom?.state === 'open') {
// This scenario covers the case where the breakout room is opened but then closed before the user joins.
this._context.deleteLatestNotification(this._callIdRef.callId, 'assignedBreakoutRoomOpened');
this._context.deleteLatestNotification(this._callIdRef.callId, 'assignedBreakoutRoomOpenedPromptJoin');
this._context.deleteLatestNotification(this._callIdRef.callId, 'assignedBreakoutRoomChanged');
} else if (breakoutRoom.state === 'closed' && currentAssignedBreakoutRoom?.call?.id) {
// This scenario covers the case where the breakout room is changed to a closed breakout room.
this._context.deleteLatestNotification(currentAssignedBreakoutRoom.call.id, 'breakoutRoomJoined');
this._context.deleteLatestNotification(currentAssignedBreakoutRoom.call.id, 'breakoutRoomClosingSoon');
this._context.deleteLatestNotification(this._callIdRef.callId, 'breakoutRoomJoined');
} else if (breakoutRoom.state === 'closed') {
// This scenario covers the case where the breakout room is closed
this._context.deleteLatestNotification(this._callIdRef.callId, 'assignedBreakoutRoomOpened');
this._context.deleteLatestNotification(this._callIdRef.callId, 'assignedBreakoutRoomOpenedPromptJoin');
this._context.deleteLatestNotification(this._callIdRef.callId, 'assignedBreakoutRoomChanged');
this._context.deleteLatestNotification(this._callIdRef.callId, 'breakoutRoomJoined');
this._context.deleteLatestNotification(this._callIdRef.callId, 'breakoutRoomClosingSoon');
clearTimeout(this._breakoutRoomClosingSoonTimeoutId);
const openBreakoutRoomId = this._context.getOpenBreakoutRoom();
if (openBreakoutRoomId && this._context.getState().calls[openBreakoutRoomId]) {
// Show notification that the assigned breakout room was closed if the user is in that breakout room.
this._context.setLatestNotification(this._callIdRef.callId, {
target: 'assignedBreakoutRoomClosed',
timestamp: new Date(Date.now())
});
}
this._context.deleteOpenBreakoutRoom();
}

this._context.setAssignedBreakoutRoom(this._callIdRef.callId, breakoutRoom);
};

Expand All @@ -130,6 +143,8 @@ export class BreakoutRoomsSubscriber {
timestamp: new Date(Date.now())
});

this._context.setOpenBreakoutRoom(call.id);

// If assigned breakout room has a display name, set the display name for its call state.
const assignedBreakoutRoomDisplayName =
this._context.getState().calls[this._callIdRef.callId]?.breakoutRooms?.assignedBreakoutRoom?.displayName;
Expand Down
5 changes: 5 additions & 0 deletions packages/calling-stateful-client/src/CallClientState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,13 @@ export interface SpotlightState {
* @public
*/
export interface BreakoutRoomsState {
/** Breakout room assigned to local user in call */
assignedBreakoutRoom?: BreakoutRoom;
/** Breakout room settings of call. This is defined when call is a breakout room. */
breakoutRoomSettings?: BreakoutRoomsSettings;
/** Origin call id of breakout room. This is defined when call is a breakout room. */
breakoutRoomOriginCallId?: string;
/** Display name of breakout room. This is defined when call is a breakout room. */
breakoutRoomDisplayName?: string;
}

Expand Down Expand Up @@ -1206,6 +1210,7 @@ export type NotificationTarget =
| /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomOpened'
| /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomOpenedPromptJoin'
| /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomChanged'
| /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomClosed'
| /* @conditional-compile-remove(breakout-rooms) */ 'breakoutRoomJoined'
| /* @conditional-compile-remove(breakout-rooms) */ 'breakoutRoomClosingSoon'
| 'capabilityTurnVideoOnPresent'
Expand Down
49 changes: 39 additions & 10 deletions packages/calling-stateful-client/src/CallContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,9 @@ export class CallContext {
private _atomicId: number;
private _callIdHistory: CallIdHistory = new CallIdHistory();
private _timeOutId: { [key: string]: ReturnType<typeof setTimeout> } = {};
private _latestCallIdsThatPushedNotifications: Partial<Record<NotificationTarget, string>> = {};
private _latestCallIdOfNotification: Partial<Record<NotificationTarget, string>> = {};
/* @conditional-compile-remove(breakout-rooms) */
private _openBreakoutRoom: string | undefined;

constructor(userId: CommunicationIdentifierKind, maxListeners = 50) {
this._logger = createClientLogger('communication-react:calling-context');
Expand Down Expand Up @@ -257,6 +259,20 @@ export class CallContext {
call.breakoutRooms?.breakoutRoomOriginCallId === newCallId;
}
});

/* @conditional-compile-remove(breakout-rooms) */
// Update call ids in latestCallIdsThatPushedNotifications
Object.keys(this._latestCallIdOfNotification).forEach((key: string) => {
if (this._latestCallIdOfNotification[key as NotificationTarget] === oldCallId) {
this._latestCallIdOfNotification[key as NotificationTarget] = newCallId;
}
});

/* @conditional-compile-remove(breakout-rooms) */
// Update the open breakout room call id if it matches the old call id
if (this._openBreakoutRoom === oldCallId) {
this._openBreakoutRoom = newCallId;
}
});
}

Expand Down Expand Up @@ -776,6 +792,21 @@ export class CallContext {
});
}

/* @conditional-compile-remove(breakout-rooms) */
public setOpenBreakoutRoom(callId: string): void {
this._openBreakoutRoom = callId;
}

/* @conditional-compile-remove(breakout-rooms) */
public deleteOpenBreakoutRoom(): void {
this._openBreakoutRoom = undefined;
}

/* @conditional-compile-remove(breakout-rooms) */
public getOpenBreakoutRoom(): string | undefined {
return this._openBreakoutRoom;
}

public setCallScreenShareParticipant(callId: string, participantKey: string | undefined): void {
this.modifyState((draft: CallClientState) => {
const call = draft.calls[this._callIdHistory.latestCallId(callId)];
Expand Down Expand Up @@ -1510,20 +1541,18 @@ export class CallContext {
}

public setLatestNotification(callId: string, notification: CallNotification): void {
this._latestCallIdsThatPushedNotifications[notification.target] = callId;
this._latestCallIdOfNotification[notification.target] = callId;
this.modifyState((draft: CallClientState) => {
draft.latestNotifications[notification.target] = notification;
});
}

public deleteLatestNotification(callId: string, notificationTarget: NotificationTarget): void {
let callIdToPushLatestNotification = this._latestCallIdsThatPushedNotifications[notificationTarget];
callIdToPushLatestNotification = callIdToPushLatestNotification
? this._callIdHistory.latestCallId(callIdToPushLatestNotification)
: undefined;
// Only delete the notification if the call that pushed the notification is the same as the call that is trying
// to delete it.
if (callIdToPushLatestNotification !== callId) {
public deleteLatestNotification(callId: string | undefined, notificationTarget: NotificationTarget): void {
const callIdOfNotification = this._latestCallIdOfNotification[notificationTarget];

// Only delete the notification if the call that pushed the notification is the same as the callId specified if it
// is provided
if (callId && callIdOfNotification !== callId) {
return;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/calling-stateful-client/src/CallSubscriber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,9 @@ export class CallSubscriber {
this._context,
this._call.feature(Features.Spotlight)
);

// Clear assigned breakout room closed notification for this call.
this._context.deleteLatestNotification(undefined, 'assignedBreakoutRoomClosed');
/* @conditional-compile-remove(breakout-rooms) */
this._breakoutRoomsSubscriber = new BreakoutRoomsSubscriber(
this._callIdRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -364,13 +364,9 @@ export interface BlockedMessage extends MessageCommon {

// @public
export interface BreakoutRoomsState {
// (undocumented)
assignedBreakoutRoom?: BreakoutRoom;
// (undocumented)
breakoutRoomDisplayName?: string;
// (undocumented)
breakoutRoomOriginCallId?: string;
// (undocumented)
breakoutRoomSettings?: BreakoutRoomsSettings;
}

Expand Down Expand Up @@ -805,7 +801,7 @@ export type CallCompositeOptions = {
};

// @public
export type CallCompositePage = 'accessDeniedTeamsMeeting' | 'call' | 'configuration' | 'hold' | 'joinCallFailedDueToNoNetwork' | 'leftCall' | 'leaving' | 'lobby' | 'removedFromCall' | /* @conditional-compile-remove(unsupported-browser) */ 'unsupportedEnvironment' | 'transferring' | 'badRequest';
export type CallCompositePage = 'accessDeniedTeamsMeeting' | 'call' | 'configuration' | 'hold' | 'joinCallFailedDueToNoNetwork' | 'leftCall' | 'leaving' | 'lobby' | 'removedFromCall' | /* @conditional-compile-remove(unsupported-browser) */ 'unsupportedEnvironment' | 'transferring' | 'badRequest' | /* @conditional-compile-remove(breakout-rooms) */ 'returningFromBreakoutRoom';

// @public
export interface CallCompositeProps extends BaseCompositeProps<CallCompositeIcons> {
Expand Down Expand Up @@ -2980,6 +2976,7 @@ export const DEFAULT_COMPONENT_ICONS: {
NotificationBarBreakoutRoomChanged: React_2.JSX.Element;
NotificationBarBreakoutRoomJoined: React_2.JSX.Element;
NotificationBarBreakoutRoomClosingSoon: React_2.JSX.Element;
NotificationBarBreakoutRoomClosed: React_2.JSX.Element;
HorizontalGalleryLeftButton: React_2.JSX.Element;
HorizontalGalleryRightButton: React_2.JSX.Element;
MessageDelivered: React_2.JSX.Element;
Expand Down Expand Up @@ -3186,6 +3183,7 @@ export const DEFAULT_COMPOSITE_ICONS: {
NotificationBarBreakoutRoomChanged: React_2.JSX.Element;
NotificationBarBreakoutRoomJoined: React_2.JSX.Element;
NotificationBarBreakoutRoomClosingSoon: React_2.JSX.Element;
NotificationBarBreakoutRoomClosed: React_2.JSX.Element;
MessageResend: React_2.JSX.Element;
ParticipantItemSpotlighted: React_2.JSX.Element;
HoldCallContextualMenuItem: React_2.JSX.Element;
Expand Down Expand Up @@ -4195,6 +4193,7 @@ export type NotificationStackSelector = (state: CallClientState, props: CallingB
// @public
export interface NotificationStackStrings {
assignedBreakoutRoomChanged?: NotificationStrings;
assignedBreakoutRoomClosed?: NotificationStrings;
assignedBreakoutRoomOpened?: NotificationStrings;
assignedBreakoutRoomOpenedPromptJoin?: NotificationStrings;
breakoutRoomClosingSoon?: NotificationStrings;
Expand Down Expand Up @@ -4270,7 +4269,7 @@ export interface NotificationStyles {
}

// @public (undocumented)
export type NotificationTarget = /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomOpened' | /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomOpenedPromptJoin' | /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomChanged' | /* @conditional-compile-remove(breakout-rooms) */ 'breakoutRoomJoined' | /* @conditional-compile-remove(breakout-rooms) */ 'breakoutRoomClosingSoon' | 'capabilityTurnVideoOnPresent' | 'capabilityTurnVideoOnAbsent' | 'capabilityUnmuteMicPresent' | 'capabilityUnmuteMicAbsent' | /* @conditional-compile-remove(together-mode) */ 'togetherModeStarted' | /* @conditional-compile-remove(together-mode) */ 'togetherModeEnded';
export type NotificationTarget = /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomOpened' | /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomOpenedPromptJoin' | /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomChanged' | /* @conditional-compile-remove(breakout-rooms) */ 'assignedBreakoutRoomClosed' | /* @conditional-compile-remove(breakout-rooms) */ 'breakoutRoomJoined' | /* @conditional-compile-remove(breakout-rooms) */ 'breakoutRoomClosingSoon' | 'capabilityTurnVideoOnPresent' | 'capabilityTurnVideoOnAbsent' | 'capabilityUnmuteMicPresent' | 'capabilityUnmuteMicAbsent' | /* @conditional-compile-remove(together-mode) */ 'togetherModeStarted' | /* @conditional-compile-remove(together-mode) */ 'togetherModeEnded';

// @public
export type NotificationType = keyof NotificationStackStrings;
Expand Down
Loading