agora-appbuilder-core 4.1.10-beta.1 → 4.1.11-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/template/agora-rn-uikit/src/Utils/isBotUser.ts +1 -1
- package/template/android/app/build.gradle +0 -7
- package/template/bridge/rtc/webNg/RtcEngine.ts +2 -2
- package/template/bridge/rtm/web/Types.ts +0 -183
- package/template/bridge/rtm/web/index.ts +488 -450
- package/template/customization-api/typeDefinition.ts +0 -1
- package/template/defaultConfig.js +3 -4
- package/template/global.d.ts +0 -1
- package/template/ios/Podfile +0 -41
- package/template/package.json +5 -5
- package/template/src/AppRoutes.tsx +3 -3
- package/template/src/ai-agent/components/ControlButtons.tsx +1 -1
- package/template/src/assets/font-styles.css +1 -33
- package/template/src/assets/fonts/icomoon.ttf +0 -0
- package/template/src/assets/selection.json +1 -1
- package/template/src/atoms/ActionMenu.tsx +93 -13
- package/template/src/atoms/CustomIcon.tsx +1 -8
- package/template/src/atoms/DropDownMulti.tsx +80 -29
- package/template/src/atoms/Dropdown.tsx +0 -5
- package/template/src/atoms/Input.tsx +2 -1
- package/template/src/atoms/TertiaryButton.tsx +1 -1
- package/template/src/atoms/UserAvatar.tsx +1 -1
- package/template/src/components/ChatContext.ts +3 -5
- package/template/src/components/Controls.tsx +167 -208
- package/template/src/components/DeviceConfigure.tsx +1 -1
- package/template/src/components/EventsConfigure.tsx +168 -118
- package/template/src/components/Navbar.tsx +11 -14
- package/template/src/components/RTMConfigure.tsx +819 -32
- package/template/src/components/beauty-effect/useBeautyEffects.tsx +13 -50
- package/template/src/components/chat/chatConfigure.tsx +1 -7
- package/template/src/components/chat-messages/useChatMessages.tsx +11 -43
- package/template/src/components/controls/useControlPermissionMatrix.tsx +4 -32
- package/template/src/components/participants/AllHostParticipants.tsx +2 -10
- package/template/src/components/participants/Participant.tsx +1 -7
- package/template/src/components/participants/UserActionMenuOptions.tsx +2 -12
- package/template/src/components/precall/joinCallBtn.native.tsx +7 -2
- package/template/src/components/precall/joinCallBtn.tsx +7 -2
- package/template/src/components/precall/joinWaitingRoomBtn.native.tsx +16 -15
- package/template/src/components/precall/joinWaitingRoomBtn.tsx +31 -17
- package/template/src/components/precall/textInput.tsx +45 -22
- package/template/src/components/precall/usePreCall.tsx +7 -0
- package/template/src/components/recordings/RecordingsDateTable.tsx +2 -3
- package/template/src/components/room-info/useRoomInfo.tsx +5 -0
- package/template/src/components/useUserPreference.tsx +12 -39
- package/template/src/components/virtual-background/useVB.tsx +0 -18
- package/template/src/components/whiteboard/WhiteboardConfigure.tsx +0 -27
- package/template/src/language/default-labels/videoCallScreenLabels.ts +27 -11
- package/template/src/logger/AppBuilderLogger.tsx +3 -11
- package/template/src/pages/VideoCall.tsx +518 -171
- package/template/src/pages/video-call/ActionSheetContent.tsx +77 -77
- package/template/src/pages/video-call/SidePanelHeader.tsx +81 -53
- package/template/src/pages/video-call/VideoCallScreen.tsx +0 -18
- package/template/src/pages/video-call/VideoCallScreenWrapper.tsx +1 -0
- package/template/src/rtm/RTMEngine.ts +37 -262
- package/template/src/rtm/utils.ts +1 -68
- package/template/src/rtm-events/constants.ts +7 -40
- package/template/src/rtm-events-api/Events.ts +39 -158
- package/template/src/subComponents/ChatBubble.tsx +3 -3
- package/template/src/subComponents/ChatContainer.tsx +9 -19
- package/template/src/subComponents/LocalAudioMute.tsx +2 -2
- package/template/src/subComponents/LocalVideoMute.tsx +2 -2
- package/template/src/subComponents/SidePanelEnum.tsx +0 -1
- package/template/src/subComponents/caption/Caption.tsx +48 -7
- package/template/src/subComponents/caption/CaptionContainer.tsx +324 -51
- package/template/src/subComponents/caption/CaptionIcon.tsx +35 -34
- package/template/src/subComponents/caption/CaptionText.tsx +103 -2
- package/template/src/subComponents/caption/LanguageSelectorPopup.tsx +179 -69
- package/template/src/subComponents/caption/Transcript.tsx +46 -11
- package/template/src/subComponents/caption/TranscriptIcon.tsx +27 -35
- package/template/src/subComponents/caption/TranscriptText.tsx +78 -3
- package/template/src/subComponents/caption/proto/ptoto.js +38 -4
- package/template/src/subComponents/caption/proto/test.proto +34 -19
- package/template/src/subComponents/caption/useCaption.tsx +754 -11
- package/template/src/subComponents/caption/useSTTAPI.tsx +118 -205
- package/template/src/subComponents/caption/useStreamMessageUtils.native.ts +152 -33
- package/template/src/subComponents/caption/useStreamMessageUtils.ts +165 -34
- package/template/src/subComponents/caption/utils.ts +171 -3
- package/template/src/subComponents/chat/ChatSendButton.tsx +0 -1
- package/template/src/subComponents/screenshare/ScreenshareButton.tsx +0 -16
- package/template/src/subComponents/screenshare/ScreenshareConfigure.native.tsx +1 -1
- package/template/src/subComponents/waiting-rooms/WaitingRoomControls.tsx +4 -7
- package/template/src/utils/SdkEvents.ts +3 -0
- package/template/src/utils/useEndCall.ts +4 -4
- package/template/src/utils/useMuteToggleLocal.ts +10 -14
- package/template/src/utils/useSpeechToText.ts +31 -20
- package/template/bridge/rtm/web/index-legacy.ts +0 -540
- package/template/src/components/RTMConfigure-legacy.tsx +0 -848
- package/template/src/components/UserGlobalPreferenceProvider.tsx +0 -227
- package/template/src/components/breakout-room/BreakoutRoomPanel.tsx +0 -58
- package/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +0 -2508
- package/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx +0 -272
- package/template/src/components/breakout-room/events/constants.ts +0 -17
- package/template/src/components/breakout-room/hoc/BreakoutRoomNameRenderer.tsx +0 -68
- package/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts +0 -49
- package/template/src/components/breakout-room/state/reducer.ts +0 -522
- package/template/src/components/breakout-room/state/types.ts +0 -54
- package/template/src/components/breakout-room/ui/BreakoutMeetingTitle.tsx +0 -60
- package/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx +0 -136
- package/template/src/components/breakout-room/ui/BreakoutRoomAnnouncementModal.tsx +0 -135
- package/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +0 -588
- package/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx +0 -142
- package/template/src/components/breakout-room/ui/BreakoutRoomMemberActionMenu.tsx +0 -122
- package/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx +0 -124
- package/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx +0 -65
- package/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx +0 -227
- package/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx +0 -140
- package/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx +0 -52
- package/template/src/components/breakout-room/ui/BreakoutRoomView.tsx +0 -193
- package/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx +0 -79
- package/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx +0 -638
- package/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx +0 -57
- package/template/src/components/common/Dividers.tsx +0 -53
- package/template/src/components/controls/toolbar-items/ExitBreakoutRoomToolbarItem.tsx +0 -13
- package/template/src/components/raise-hand/RaiseHandButton.tsx +0 -50
- package/template/src/components/raise-hand/RaiseHandProvider.tsx +0 -308
- package/template/src/components/raise-hand/index.ts +0 -14
- package/template/src/components/room-info/useCurrentRoomInfo.tsx +0 -42
- package/template/src/components/room-info/useSetBreakoutRoomInfo.tsx +0 -64
- package/template/src/pages/video-call/BreakoutVideoCall.tsx +0 -213
- package/template/src/pages/video-call/VideoCallContent.tsx +0 -211
- package/template/src/pages/video-call/VideoCallStateWrapper.tsx +0 -495
- package/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx +0 -882
- package/template/src/rtm/RTMConfigureMainRoomProvider.tsx +0 -757
- package/template/src/rtm/RTMCoreProvider.tsx +0 -419
- package/template/src/rtm/RTMGlobalStateProvider.tsx +0 -706
- package/template/src/rtm/RTMStatusBanner.tsx +0 -99
- package/template/src/rtm/constants.ts +0 -12
- package/template/src/rtm/hooks/useMainRoomUserDisplayName.ts +0 -45
- package/template/src/rtm/rtm-presence-utils.ts +0 -344
- package/template/src/subComponents/chat/ChatAnnouncementView.tsx +0 -65
- package/template/src/utils/useDebouncedCallback.tsx +0 -20
|
@@ -1,2508 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
useContext,
|
|
3
|
-
useReducer,
|
|
4
|
-
useEffect,
|
|
5
|
-
useState,
|
|
6
|
-
useCallback,
|
|
7
|
-
useRef,
|
|
8
|
-
} from 'react';
|
|
9
|
-
import {ContentInterface, UidType} from '../../../../agora-rn-uikit';
|
|
10
|
-
import {createHook} from 'customization-implementation';
|
|
11
|
-
import {randomNameGenerator} from '../../../utils';
|
|
12
|
-
import StorageContext from '../../StorageContext';
|
|
13
|
-
import getUniqueID from '../../../utils/getUniqueID';
|
|
14
|
-
import {logger, LogSource} from '../../../logger/AppBuilderLogger';
|
|
15
|
-
import {useRoomInfo} from 'customization-api';
|
|
16
|
-
import {
|
|
17
|
-
BreakoutGroupActionTypes,
|
|
18
|
-
BreakoutGroup,
|
|
19
|
-
BreakoutRoomState,
|
|
20
|
-
breakoutRoomReducer,
|
|
21
|
-
initialBreakoutRoomState,
|
|
22
|
-
RoomAssignmentStrategy,
|
|
23
|
-
ManualParticipantAssignment,
|
|
24
|
-
BreakoutRoomUser,
|
|
25
|
-
} from '../state/reducer';
|
|
26
|
-
import {useLocalUid} from '../../../../agora-rn-uikit';
|
|
27
|
-
import {useContent} from '../../../../customization-api';
|
|
28
|
-
import events, {PersistanceLevel} from '../../../rtm-events-api';
|
|
29
|
-
import {BreakoutRoomAction, initialBreakoutGroups} from '../state/reducer';
|
|
30
|
-
import {BreakoutRoomEventNames} from '../events/constants';
|
|
31
|
-
import {EventNames} from '../../../rtm-events';
|
|
32
|
-
import {BreakoutRoomSyncStateEventPayload} from '../state/types';
|
|
33
|
-
import {IconsInterface} from '../../../atoms/CustomIcon';
|
|
34
|
-
import Toast from '../../../../react-native-toast-message';
|
|
35
|
-
import useBreakoutRoomExit from '../hooks/useBreakoutRoomExit';
|
|
36
|
-
import {useDebouncedCallback} from '../../../utils/useDebouncedCallback';
|
|
37
|
-
import {useLocation} from '../../../components/Router';
|
|
38
|
-
import {useMainRoomUserDisplayName} from '../../../rtm/hooks/useMainRoomUserDisplayName';
|
|
39
|
-
import {
|
|
40
|
-
RTMUserData,
|
|
41
|
-
useRTMGlobalState,
|
|
42
|
-
} from '../../../rtm/RTMGlobalStateProvider';
|
|
43
|
-
import {useScreenshare} from '../../../subComponents/screenshare/useScreenshare';
|
|
44
|
-
|
|
45
|
-
const HOST_BROADCASTED_OPERATIONS = [
|
|
46
|
-
BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM,
|
|
47
|
-
BreakoutGroupActionTypes.CREATE_GROUP,
|
|
48
|
-
BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS,
|
|
49
|
-
BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS,
|
|
50
|
-
BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS,
|
|
51
|
-
BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN,
|
|
52
|
-
BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP,
|
|
53
|
-
BreakoutGroupActionTypes.CLOSE_GROUP,
|
|
54
|
-
BreakoutGroupActionTypes.CLOSE_ALL_GROUPS,
|
|
55
|
-
BreakoutGroupActionTypes.RENAME_GROUP,
|
|
56
|
-
] as const;
|
|
57
|
-
|
|
58
|
-
const getSanitizedPayload = (
|
|
59
|
-
payload: BreakoutGroup[],
|
|
60
|
-
defaultContentRef: any,
|
|
61
|
-
mainRoomRTMUsers: {[uid: number]: RTMUserData},
|
|
62
|
-
) => {
|
|
63
|
-
return payload.map(({id, ...rest}) => {
|
|
64
|
-
const group = id !== undefined ? {...rest, id} : rest;
|
|
65
|
-
|
|
66
|
-
// Filter out offline users from participants
|
|
67
|
-
const filteredGroup = {
|
|
68
|
-
...group,
|
|
69
|
-
participants: {
|
|
70
|
-
hosts: group.participants.hosts.filter(uid => {
|
|
71
|
-
let user = mainRoomRTMUsers[uid];
|
|
72
|
-
if (defaultContentRef[uid]) {
|
|
73
|
-
user = defaultContentRef[uid];
|
|
74
|
-
}
|
|
75
|
-
if (user) {
|
|
76
|
-
return !user.offline && user.type === 'rtc';
|
|
77
|
-
}
|
|
78
|
-
}),
|
|
79
|
-
attendees: group.participants.attendees.filter(uid => {
|
|
80
|
-
let user = mainRoomRTMUsers[uid];
|
|
81
|
-
if (defaultContentRef[uid]) {
|
|
82
|
-
user = defaultContentRef[uid];
|
|
83
|
-
}
|
|
84
|
-
if (user) {
|
|
85
|
-
return !user.offline && user.type === 'rtc';
|
|
86
|
-
}
|
|
87
|
-
}),
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Remove temp IDs for API payload
|
|
92
|
-
if (typeof id === 'string' && id.startsWith('temp')) {
|
|
93
|
-
const {id: _, ...withoutId} = filteredGroup;
|
|
94
|
-
return withoutId;
|
|
95
|
-
}
|
|
96
|
-
return filteredGroup;
|
|
97
|
-
});
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// const validateRollbackState = (state: BreakoutRoomState): boolean => {
|
|
101
|
-
// return (
|
|
102
|
-
// Array.isArray(state.breakoutGroups) &&
|
|
103
|
-
// typeof state.breakoutSessionId === 'string' &&
|
|
104
|
-
// typeof state.canUserSwitchRoom === 'boolean' &&
|
|
105
|
-
// state.breakoutGroups.every(
|
|
106
|
-
// group =>
|
|
107
|
-
// typeof group.id === 'string' &&
|
|
108
|
-
// typeof group.name === 'string' &&
|
|
109
|
-
// Array.isArray(group.participants?.hosts) &&
|
|
110
|
-
// Array.isArray(group.participants?.attendees),
|
|
111
|
-
// )
|
|
112
|
-
// );
|
|
113
|
-
// };
|
|
114
|
-
|
|
115
|
-
export const deepCloneBreakoutGroups = (
|
|
116
|
-
groups: BreakoutGroup[] = [],
|
|
117
|
-
): BreakoutGroup[] =>
|
|
118
|
-
groups.map(group => ({
|
|
119
|
-
...group,
|
|
120
|
-
participants: {
|
|
121
|
-
hosts: [...(group.participants?.hosts ?? [])],
|
|
122
|
-
attendees: [...(group.participants?.attendees ?? [])],
|
|
123
|
-
},
|
|
124
|
-
}));
|
|
125
|
-
|
|
126
|
-
const needsDeepCloning = (action: BreakoutRoomAction): boolean => {
|
|
127
|
-
const CLONING_REQUIRED_ACTIONS = [
|
|
128
|
-
BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP,
|
|
129
|
-
BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN,
|
|
130
|
-
BreakoutGroupActionTypes.EXIT_GROUP,
|
|
131
|
-
BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS,
|
|
132
|
-
BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS,
|
|
133
|
-
BreakoutGroupActionTypes.CLOSE_GROUP, // Safe to include
|
|
134
|
-
BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, // Safe to include
|
|
135
|
-
BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS,
|
|
136
|
-
BreakoutGroupActionTypes.SYNC_STATE,
|
|
137
|
-
];
|
|
138
|
-
|
|
139
|
-
return CLONING_REQUIRED_ACTIONS.includes(action.type as any);
|
|
140
|
-
};
|
|
141
|
-
export interface MemberDropdownOption {
|
|
142
|
-
type: 'move-to-main' | 'move-to-room' | 'make-presenter';
|
|
143
|
-
icon: keyof IconsInterface;
|
|
144
|
-
title: string;
|
|
145
|
-
roomId?: string;
|
|
146
|
-
roomName?: string;
|
|
147
|
-
onOptionPress: () => void;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
interface BreakoutRoomPermissions {
|
|
151
|
-
// Room navigation
|
|
152
|
-
canJoinRoom: boolean;
|
|
153
|
-
canExitRoom: boolean;
|
|
154
|
-
canSwitchBetweenRooms: boolean;
|
|
155
|
-
// Media controls
|
|
156
|
-
canScreenshare: boolean;
|
|
157
|
-
canRaiseHands: boolean;
|
|
158
|
-
// Room management (host only)
|
|
159
|
-
canHostManageMainRoom: boolean;
|
|
160
|
-
canAssignParticipants: boolean;
|
|
161
|
-
canCreateRooms: boolean;
|
|
162
|
-
canMoveUsers: boolean;
|
|
163
|
-
canCloseRooms: boolean;
|
|
164
|
-
canMakePresenter: boolean;
|
|
165
|
-
}
|
|
166
|
-
const defaulBreakoutRoomPermission: BreakoutRoomPermissions = {
|
|
167
|
-
canJoinRoom: false,
|
|
168
|
-
canExitRoom: false,
|
|
169
|
-
canSwitchBetweenRooms: false, // Media controls
|
|
170
|
-
canScreenshare: true,
|
|
171
|
-
canRaiseHands: false,
|
|
172
|
-
// Room management (host only)
|
|
173
|
-
canHostManageMainRoom: false,
|
|
174
|
-
canAssignParticipants: false,
|
|
175
|
-
canCreateRooms: false,
|
|
176
|
-
canMoveUsers: false,
|
|
177
|
-
canCloseRooms: false,
|
|
178
|
-
canMakePresenter: false,
|
|
179
|
-
};
|
|
180
|
-
interface BreakoutRoomContextValue {
|
|
181
|
-
mainChannelId: string;
|
|
182
|
-
isBreakoutUILocked: boolean;
|
|
183
|
-
breakoutSessionId: BreakoutRoomState['breakoutSessionId'];
|
|
184
|
-
breakoutGroups: BreakoutRoomState['breakoutGroups'];
|
|
185
|
-
assignmentStrategy: RoomAssignmentStrategy;
|
|
186
|
-
canUserSwitchRoom: boolean;
|
|
187
|
-
toggleRoomSwitchingAllowed: (value: boolean) => void;
|
|
188
|
-
unassignedParticipants: {uid: UidType; user: BreakoutRoomUser}[];
|
|
189
|
-
manualAssignments: ManualParticipantAssignment[];
|
|
190
|
-
setManualAssignments: (assignments: ManualParticipantAssignment[]) => void;
|
|
191
|
-
clearManualAssignments: () => void;
|
|
192
|
-
createBreakoutRoomGroup: (name?: string) => void;
|
|
193
|
-
isUserInRoom: (room?: BreakoutGroup) => boolean;
|
|
194
|
-
joinRoom: (roomId: string, permissionAtCallTime?: boolean) => void;
|
|
195
|
-
exitRoom: (roomId?: string, permissionAtCallTime?: boolean) => Promise<void>;
|
|
196
|
-
closeRoom: (roomId: string) => void;
|
|
197
|
-
closeAllRooms: () => void;
|
|
198
|
-
updateRoomName: (newRoomName: string, roomId: string) => void;
|
|
199
|
-
getAllRooms: () => BreakoutGroup[];
|
|
200
|
-
getRoomMemberDropdownOptions: (memberUid: UidType) => MemberDropdownOption[];
|
|
201
|
-
upsertBreakoutRoomAPI: (type: 'START' | 'UPDATE') => Promise<void>;
|
|
202
|
-
checkIfBreakoutRoomSessionExistsAPI: () => Promise<boolean>;
|
|
203
|
-
handleAssignParticipants: (strategy: RoomAssignmentStrategy) => void;
|
|
204
|
-
// Presenters
|
|
205
|
-
// onMakeMePresenter: (
|
|
206
|
-
// action: 'start' | 'stop',
|
|
207
|
-
// shouldSendEvent?: boolean,
|
|
208
|
-
// ) => void;
|
|
209
|
-
// presenters: {uid: UidType; timestamp: number}[];
|
|
210
|
-
// clearAllPresenters: () => void;
|
|
211
|
-
// State sync
|
|
212
|
-
handleBreakoutRoomSyncState: (
|
|
213
|
-
data: BreakoutRoomSyncStateEventPayload['data'],
|
|
214
|
-
timestamp: number,
|
|
215
|
-
) => void;
|
|
216
|
-
// Multi-host coordination handlers
|
|
217
|
-
handleHostOperationStart: (
|
|
218
|
-
operationName: string,
|
|
219
|
-
hostUid: UidType,
|
|
220
|
-
hostName: string,
|
|
221
|
-
) => void;
|
|
222
|
-
handleHostOperationEnd: (
|
|
223
|
-
operationName: string,
|
|
224
|
-
hostUid: UidType,
|
|
225
|
-
hostName: string,
|
|
226
|
-
) => void;
|
|
227
|
-
permissions: BreakoutRoomPermissions;
|
|
228
|
-
// Loading states
|
|
229
|
-
isBreakoutUpdateInFlight: boolean;
|
|
230
|
-
// Multi-host coordination
|
|
231
|
-
currentOperatingHostName?: string;
|
|
232
|
-
// State version for forcing re-computation in dependent hooks
|
|
233
|
-
breakoutRoomVersion: number;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const BreakoutRoomContext = React.createContext<BreakoutRoomContextValue>({
|
|
237
|
-
mainChannelId: '',
|
|
238
|
-
isBreakoutUILocked: false,
|
|
239
|
-
breakoutSessionId: undefined,
|
|
240
|
-
unassignedParticipants: [],
|
|
241
|
-
breakoutGroups: [],
|
|
242
|
-
assignmentStrategy: RoomAssignmentStrategy.NO_ASSIGN,
|
|
243
|
-
manualAssignments: [],
|
|
244
|
-
setManualAssignments: () => {},
|
|
245
|
-
clearManualAssignments: () => {},
|
|
246
|
-
canUserSwitchRoom: false,
|
|
247
|
-
toggleRoomSwitchingAllowed: () => {},
|
|
248
|
-
handleAssignParticipants: () => {},
|
|
249
|
-
createBreakoutRoomGroup: () => {},
|
|
250
|
-
isUserInRoom: () => false,
|
|
251
|
-
joinRoom: () => {},
|
|
252
|
-
exitRoom: async () => {},
|
|
253
|
-
closeRoom: () => {},
|
|
254
|
-
closeAllRooms: () => {},
|
|
255
|
-
updateRoomName: () => {},
|
|
256
|
-
getAllRooms: () => [],
|
|
257
|
-
getRoomMemberDropdownOptions: () => [],
|
|
258
|
-
upsertBreakoutRoomAPI: async () => {},
|
|
259
|
-
checkIfBreakoutRoomSessionExistsAPI: async () => false,
|
|
260
|
-
// onMakeMePresenter: () => {},
|
|
261
|
-
// presenters: [],
|
|
262
|
-
// clearAllPresenters: () => {},
|
|
263
|
-
handleBreakoutRoomSyncState: () => {},
|
|
264
|
-
// Multi-host coordination handlers
|
|
265
|
-
handleHostOperationStart: () => {},
|
|
266
|
-
handleHostOperationEnd: () => {},
|
|
267
|
-
// Provide a safe non-null default object
|
|
268
|
-
permissions: {...defaulBreakoutRoomPermission},
|
|
269
|
-
// Loading states
|
|
270
|
-
isBreakoutUpdateInFlight: false,
|
|
271
|
-
// Multi-host coordination
|
|
272
|
-
currentOperatingHostName: undefined,
|
|
273
|
-
// State version for forcing re-computation in dependent hooks
|
|
274
|
-
breakoutRoomVersion: 0,
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
const BreakoutRoomProvider = ({
|
|
278
|
-
children,
|
|
279
|
-
mainChannel,
|
|
280
|
-
handleLeaveBreakout,
|
|
281
|
-
}: {
|
|
282
|
-
children: React.ReactNode;
|
|
283
|
-
mainChannel: string;
|
|
284
|
-
handleLeaveBreakout: () => void;
|
|
285
|
-
}) => {
|
|
286
|
-
const {store} = useContext(StorageContext);
|
|
287
|
-
const {defaultContent, activeUids} = useContent();
|
|
288
|
-
const {mainRoomRTMUsers, customRTMMainRoomData, setCustomRTMMainRoomData} =
|
|
289
|
-
useRTMGlobalState();
|
|
290
|
-
const localUid = useLocalUid();
|
|
291
|
-
const {
|
|
292
|
-
data: {isHost, roomId: joinRoomId},
|
|
293
|
-
} = useRoomInfo();
|
|
294
|
-
const breakoutRoomExit = useBreakoutRoomExit(handleLeaveBreakout);
|
|
295
|
-
const [state, baseDispatch] = useReducer(
|
|
296
|
-
breakoutRoomReducer,
|
|
297
|
-
initialBreakoutRoomState,
|
|
298
|
-
);
|
|
299
|
-
console.log('supriya-event state', state);
|
|
300
|
-
const [isBreakoutUpdateInFlight, setBreakoutUpdateInFlight] = useState(false);
|
|
301
|
-
// Parse URL to determine current mode
|
|
302
|
-
const location = useLocation();
|
|
303
|
-
const searchParams = new URLSearchParams(location.search);
|
|
304
|
-
const isBreakoutMode = searchParams.get('breakout') === 'true';
|
|
305
|
-
// Main Room RTM data
|
|
306
|
-
const getDisplayName = useMainRoomUserDisplayName();
|
|
307
|
-
|
|
308
|
-
// Permissions:
|
|
309
|
-
const [permissions, setPermissions] = useState<BreakoutRoomPermissions>({
|
|
310
|
-
...defaulBreakoutRoomPermission,
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
// Store the last operation
|
|
314
|
-
const [currentOperatingHostName, setCurrentOperatingHostName] = useState<
|
|
315
|
-
string | undefined
|
|
316
|
-
>(undefined);
|
|
317
|
-
|
|
318
|
-
// Timestamp Server (authoritative ordering)
|
|
319
|
-
const lastProcessedServerTsRef = useRef(0);
|
|
320
|
-
// 2Self join guard (prevent stale reverts) (when self join happens)
|
|
321
|
-
const lastSelfJoinRef = useRef<{roomId: string; ts: number} | null>(null);
|
|
322
|
-
// Timestamp client tracking for event ordering client side
|
|
323
|
-
const lastSyncedTimestampRef = useRef(0);
|
|
324
|
-
const isBreakoutUILocked =
|
|
325
|
-
isBreakoutUpdateInFlight || !!currentOperatingHostName;
|
|
326
|
-
const lastSyncedSnapshotRef = useRef<{
|
|
327
|
-
session_id: string;
|
|
328
|
-
switch_room: boolean;
|
|
329
|
-
assignment_type: string;
|
|
330
|
-
breakout_room: BreakoutGroup[];
|
|
331
|
-
} | null>(null);
|
|
332
|
-
|
|
333
|
-
// Breakout sync queue (latest-event-wins)
|
|
334
|
-
const breakoutSyncQueueRef = useRef<{
|
|
335
|
-
latestTask: {
|
|
336
|
-
payload: BreakoutRoomSyncStateEventPayload['data'];
|
|
337
|
-
timestamp: number;
|
|
338
|
-
} | null;
|
|
339
|
-
isProcessing: boolean;
|
|
340
|
-
}>({
|
|
341
|
-
latestTask: null,
|
|
342
|
-
isProcessing: false,
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// Join Room pending intent
|
|
346
|
-
const [selfJoinRoomId, setSelfJoinRoomId] = useState<string | null>(null);
|
|
347
|
-
|
|
348
|
-
// Presenter
|
|
349
|
-
const {isScreenshareActive, stopScreenshare} = useScreenshare();
|
|
350
|
-
|
|
351
|
-
// const [canIPresent, setICanPresent] = useState<boolean>(false);
|
|
352
|
-
// Get presenters from custom RTM main room data (memoized to maintain stable reference)
|
|
353
|
-
// const presenters = React.useMemo(
|
|
354
|
-
// () => customRTMMainRoomData.breakout_room_presenters || [],
|
|
355
|
-
// [customRTMMainRoomData],
|
|
356
|
-
// );
|
|
357
|
-
|
|
358
|
-
// State version tracker to force dependent hooks to re-compute
|
|
359
|
-
const [breakoutRoomVersion, setBreakoutRoomVersion] = useState(0);
|
|
360
|
-
|
|
361
|
-
// Refs to avoid stale closures in async callbacks
|
|
362
|
-
const stateRef = useRef(state);
|
|
363
|
-
const prevStateRef = useRef(state);
|
|
364
|
-
const isHostRef = useRef(isHost);
|
|
365
|
-
const defaultContentRef = useRef(defaultContent);
|
|
366
|
-
const isMountedRef = useRef(true);
|
|
367
|
-
// Enhanced dispatch that tracks user actions
|
|
368
|
-
const [lastAction, setLastAction] = useState<BreakoutRoomAction | null>(null);
|
|
369
|
-
|
|
370
|
-
const dispatch = useCallback((action: BreakoutRoomAction) => {
|
|
371
|
-
if (needsDeepCloning(action)) {
|
|
372
|
-
// Only deep clone when necessary
|
|
373
|
-
prevStateRef.current = {
|
|
374
|
-
...stateRef.current,
|
|
375
|
-
breakoutGroups: deepCloneBreakoutGroups(
|
|
376
|
-
stateRef.current.breakoutGroups,
|
|
377
|
-
),
|
|
378
|
-
};
|
|
379
|
-
} else {
|
|
380
|
-
// Shallow copy for non-participant actions
|
|
381
|
-
prevStateRef.current = {
|
|
382
|
-
...stateRef.current,
|
|
383
|
-
breakoutGroups: [...stateRef.current.breakoutGroups], // Shallow copy
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
baseDispatch(action);
|
|
387
|
-
setLastAction(action);
|
|
388
|
-
}, []);
|
|
389
|
-
|
|
390
|
-
useEffect(() => {
|
|
391
|
-
stateRef.current = state;
|
|
392
|
-
}, [state]);
|
|
393
|
-
useEffect(() => {
|
|
394
|
-
isHostRef.current = isHost;
|
|
395
|
-
}, [isHost]);
|
|
396
|
-
useEffect(() => {
|
|
397
|
-
defaultContentRef.current = defaultContent;
|
|
398
|
-
}, [defaultContent]);
|
|
399
|
-
useEffect(() => {
|
|
400
|
-
return () => {
|
|
401
|
-
isMountedRef.current = false;
|
|
402
|
-
|
|
403
|
-
// // Clear presenter attribute on unmount if user is presenting
|
|
404
|
-
// if (canIPresent && !isHostRef.current) {
|
|
405
|
-
// logger.log(
|
|
406
|
-
// LogSource.Internals,
|
|
407
|
-
// 'BREAKOUT_ROOM',
|
|
408
|
-
// 'Clearing presenter attribute on unmount',
|
|
409
|
-
// {localUid},
|
|
410
|
-
// );
|
|
411
|
-
|
|
412
|
-
// // Send event to clear presenter status
|
|
413
|
-
// events.send(
|
|
414
|
-
// EventNames.BREAKOUT_PRESENTER_ATTRIBUTE,
|
|
415
|
-
// JSON.stringify({
|
|
416
|
-
// uid: localUid,
|
|
417
|
-
// isPresenter: false,
|
|
418
|
-
// timestamp: Date.now(),
|
|
419
|
-
// }),
|
|
420
|
-
// PersistanceLevel.Sender,
|
|
421
|
-
// );
|
|
422
|
-
// }
|
|
423
|
-
};
|
|
424
|
-
}, [localUid]);
|
|
425
|
-
|
|
426
|
-
// Timeouts
|
|
427
|
-
const timeoutsRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set());
|
|
428
|
-
|
|
429
|
-
const safeSetTimeout = useCallback((fn: () => void, delay: number) => {
|
|
430
|
-
const id = setTimeout(() => {
|
|
431
|
-
fn();
|
|
432
|
-
timeoutsRef.current.delete(id); // cleanup after execution
|
|
433
|
-
}, delay);
|
|
434
|
-
|
|
435
|
-
timeoutsRef.current.add(id);
|
|
436
|
-
return id;
|
|
437
|
-
}, []);
|
|
438
|
-
|
|
439
|
-
// Clear all timeouts
|
|
440
|
-
useEffect(() => {
|
|
441
|
-
const snapshot = timeoutsRef.current;
|
|
442
|
-
return () => {
|
|
443
|
-
snapshot.forEach(timeoutId => clearTimeout(timeoutId));
|
|
444
|
-
snapshot.clear();
|
|
445
|
-
};
|
|
446
|
-
}, []);
|
|
447
|
-
|
|
448
|
-
// Toast duplication
|
|
449
|
-
const toastDedupeRef = useRef<Set<string>>(new Set());
|
|
450
|
-
|
|
451
|
-
const showDeduplicatedToast = useCallback((key: string, toastConfig: any) => {
|
|
452
|
-
if (toastDedupeRef.current.has(key)) {
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
toastDedupeRef.current.add(key);
|
|
457
|
-
Toast.show(toastConfig);
|
|
458
|
-
|
|
459
|
-
safeSetTimeout(() => {
|
|
460
|
-
toastDedupeRef.current.delete(key);
|
|
461
|
-
}, toastConfig.visibilityTime || 3000);
|
|
462
|
-
}, []);
|
|
463
|
-
|
|
464
|
-
// Multi-host coordination functions
|
|
465
|
-
const broadcastHostOperationStart = useCallback(
|
|
466
|
-
(operationName: string) => {
|
|
467
|
-
if (!isHostRef.current) {
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
const hostName = getDisplayName(localUid);
|
|
472
|
-
|
|
473
|
-
logger.log(
|
|
474
|
-
LogSource.Internals,
|
|
475
|
-
'BREAKOUT_ROOM',
|
|
476
|
-
'Broadcasting host operation start',
|
|
477
|
-
{operation: operationName, hostName, hostUid: localUid},
|
|
478
|
-
);
|
|
479
|
-
|
|
480
|
-
events.send(
|
|
481
|
-
BreakoutRoomEventNames.BREAKOUT_ROOM_HOST_OPERATION_START,
|
|
482
|
-
JSON.stringify({
|
|
483
|
-
operationName,
|
|
484
|
-
hostUid: localUid,
|
|
485
|
-
hostName,
|
|
486
|
-
timestamp: Date.now(),
|
|
487
|
-
}),
|
|
488
|
-
);
|
|
489
|
-
},
|
|
490
|
-
[localUid],
|
|
491
|
-
);
|
|
492
|
-
|
|
493
|
-
const broadcastHostOperationEnd = useCallback(
|
|
494
|
-
(operationName: string) => {
|
|
495
|
-
if (!isHostRef.current) {
|
|
496
|
-
return;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
const hostName = getDisplayName(localUid);
|
|
500
|
-
|
|
501
|
-
logger.log(
|
|
502
|
-
LogSource.Internals,
|
|
503
|
-
'BREAKOUT_ROOM',
|
|
504
|
-
'Broadcasting host operation end',
|
|
505
|
-
{operation: operationName, hostName, hostUid: localUid},
|
|
506
|
-
);
|
|
507
|
-
|
|
508
|
-
events.send(
|
|
509
|
-
BreakoutRoomEventNames.BREAKOUT_ROOM_HOST_OPERATION_END,
|
|
510
|
-
JSON.stringify({
|
|
511
|
-
operationName,
|
|
512
|
-
hostUid: localUid,
|
|
513
|
-
hostName,
|
|
514
|
-
timestamp: Date.now(),
|
|
515
|
-
}),
|
|
516
|
-
);
|
|
517
|
-
},
|
|
518
|
-
[localUid],
|
|
519
|
-
);
|
|
520
|
-
|
|
521
|
-
// Common operation lock for API-triggering actions with multi-host coordination
|
|
522
|
-
const acquireOperationLock = useCallback(
|
|
523
|
-
(operationName: string): boolean => {
|
|
524
|
-
// Check if another host is operating
|
|
525
|
-
console.log('supriya-state-sync acquiring lock step 1');
|
|
526
|
-
|
|
527
|
-
// Check if API call is in progress
|
|
528
|
-
if (isBreakoutUpdateInFlight) {
|
|
529
|
-
logger.log(
|
|
530
|
-
LogSource.Internals,
|
|
531
|
-
'BREAKOUT_ROOM',
|
|
532
|
-
'Operation blocked - API call in progress',
|
|
533
|
-
{
|
|
534
|
-
blockedOperation: operationName,
|
|
535
|
-
currentlyInFlight: isBreakoutUpdateInFlight,
|
|
536
|
-
},
|
|
537
|
-
);
|
|
538
|
-
return false;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Broadcast that this host is starting an operation
|
|
542
|
-
console.log(
|
|
543
|
-
'supriya-state-sync broadcasting host operation start',
|
|
544
|
-
operationName,
|
|
545
|
-
);
|
|
546
|
-
setBreakoutUpdateInFlight(true);
|
|
547
|
-
broadcastHostOperationStart(operationName);
|
|
548
|
-
|
|
549
|
-
logger.log(
|
|
550
|
-
LogSource.Internals,
|
|
551
|
-
'BREAKOUT_ROOM',
|
|
552
|
-
`Operation lock acquired for ${operationName}`,
|
|
553
|
-
{operation: operationName},
|
|
554
|
-
);
|
|
555
|
-
return true;
|
|
556
|
-
},
|
|
557
|
-
[
|
|
558
|
-
isBreakoutUpdateInFlight,
|
|
559
|
-
broadcastHostOperationStart,
|
|
560
|
-
setBreakoutUpdateInFlight,
|
|
561
|
-
],
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
// Update unassigned participants and remove offline users from breakout rooms
|
|
565
|
-
useEffect(() => {
|
|
566
|
-
if (!stateRef.current?.breakoutSessionId) {
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Filter users from defaultContent first, then check if they're in activeUids
|
|
571
|
-
// This follows the legacy RTM pattern: start with defaultContent, then filter by activeUids
|
|
572
|
-
const filteredParticipants = Object.entries(defaultContent)
|
|
573
|
-
.filter(([k, v]) => {
|
|
574
|
-
// Only include RTC users
|
|
575
|
-
if (v?.type !== 'rtc') {
|
|
576
|
-
return false;
|
|
577
|
-
}
|
|
578
|
-
// Exclude offline users
|
|
579
|
-
if (v?.offline) {
|
|
580
|
-
return false;
|
|
581
|
-
}
|
|
582
|
-
// Exclude screenshare UIDs (they typically have a parentUid)
|
|
583
|
-
if (v?.parentUid) {
|
|
584
|
-
return false;
|
|
585
|
-
}
|
|
586
|
-
// KEY CHECK: Only include users who are in activeUids (actually in the call)
|
|
587
|
-
const uid = parseInt(k);
|
|
588
|
-
if (activeUids.indexOf(uid) === -1) {
|
|
589
|
-
return false;
|
|
590
|
-
}
|
|
591
|
-
return true;
|
|
592
|
-
})
|
|
593
|
-
.map(([k, v]) => {
|
|
594
|
-
const uid = parseInt(k);
|
|
595
|
-
|
|
596
|
-
// Get additional RTM data if available for cross-room scenarios
|
|
597
|
-
const rtmUser = mainRoomRTMUsers[uid];
|
|
598
|
-
const user = v || rtmUser;
|
|
599
|
-
|
|
600
|
-
console.log('supriya-breakoutSessionId user: ', user);
|
|
601
|
-
|
|
602
|
-
// Create BreakoutRoomUser object with proper fallback
|
|
603
|
-
const breakoutRoomUser: BreakoutRoomUser = {
|
|
604
|
-
name: user?.name || rtmUser?.name || '',
|
|
605
|
-
isHost: user?.isHost === 'true',
|
|
606
|
-
};
|
|
607
|
-
|
|
608
|
-
return {uid, user: breakoutRoomUser};
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
// Sort participants to show local user first
|
|
612
|
-
filteredParticipants.sort((a, b) => {
|
|
613
|
-
if (a.uid === localUid) {
|
|
614
|
-
return -1;
|
|
615
|
-
}
|
|
616
|
-
if (b.uid === localUid) {
|
|
617
|
-
return 1;
|
|
618
|
-
}
|
|
619
|
-
return 0;
|
|
620
|
-
});
|
|
621
|
-
|
|
622
|
-
// // Find offline users who are currently assigned to breakout rooms
|
|
623
|
-
// const currentlyAssignedUids = new Set<UidType>();
|
|
624
|
-
// stateRef.current.breakoutGroups.forEach(group => {
|
|
625
|
-
// group.participants.hosts.forEach(uid => currentlyAssignedUids.add(uid));
|
|
626
|
-
// group.participants.attendees.forEach(uid => currentlyAssignedUids.add(uid));
|
|
627
|
-
// });
|
|
628
|
-
|
|
629
|
-
// const offlineAssignedUsers = Array.from(currentlyAssignedUids).filter(uid => {
|
|
630
|
-
// const user = defaultContent[uid];
|
|
631
|
-
// return !user || user.offline || user.type !== 'rtc';
|
|
632
|
-
// });
|
|
633
|
-
|
|
634
|
-
// // Remove offline users from breakout rooms if any found
|
|
635
|
-
// if (offlineAssignedUsers.length > 0) {
|
|
636
|
-
// console.log('Removing offline users from breakout rooms:', offlineAssignedUsers);
|
|
637
|
-
// dispatch({
|
|
638
|
-
// type: BreakoutGroupActionTypes.REMOVE_OFFLINE_USERS,
|
|
639
|
-
// payload: {
|
|
640
|
-
// offlineUserUids: offlineAssignedUsers,
|
|
641
|
-
// },
|
|
642
|
-
// });
|
|
643
|
-
// }
|
|
644
|
-
|
|
645
|
-
// Update unassigned participants
|
|
646
|
-
dispatch({
|
|
647
|
-
type: BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS,
|
|
648
|
-
payload: {
|
|
649
|
-
unassignedParticipants: filteredParticipants,
|
|
650
|
-
},
|
|
651
|
-
});
|
|
652
|
-
}, [
|
|
653
|
-
defaultContent,
|
|
654
|
-
activeUids,
|
|
655
|
-
localUid,
|
|
656
|
-
dispatch,
|
|
657
|
-
state.breakoutSessionId,
|
|
658
|
-
mainRoomRTMUsers,
|
|
659
|
-
]);
|
|
660
|
-
|
|
661
|
-
// Increment version when breakout group assignments change
|
|
662
|
-
useEffect(() => {
|
|
663
|
-
setBreakoutRoomVersion(prev => prev + 1);
|
|
664
|
-
}, [state.breakoutGroups]);
|
|
665
|
-
|
|
666
|
-
// Check if there is already an active breakout session
|
|
667
|
-
// We can call this to trigger sync events
|
|
668
|
-
const checkIfBreakoutRoomSessionExistsAPI =
|
|
669
|
-
useCallback(async (): Promise<boolean> => {
|
|
670
|
-
// Skip API call if roomId is not available or if API update is in progress
|
|
671
|
-
if (!joinRoomId?.host && !joinRoomId?.attendee) {
|
|
672
|
-
console.log('supriya-sync-queue: Skipping GET no roomId available');
|
|
673
|
-
return false;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
if (isBreakoutUpdateInFlight) {
|
|
677
|
-
console.log('supriya-sync-queue upsert in progress: Skipping GET');
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
680
|
-
console.log(
|
|
681
|
-
'supriya-sync-queue calling checkIfBreakoutRoomSessionExistsAPI',
|
|
682
|
-
joinRoomId,
|
|
683
|
-
isHostRef.current,
|
|
684
|
-
);
|
|
685
|
-
const startTime = Date.now();
|
|
686
|
-
const requestId = getUniqueID();
|
|
687
|
-
const url = `${
|
|
688
|
-
$config.BACKEND_ENDPOINT
|
|
689
|
-
}/v1/channel/breakout-room?passphrase=${
|
|
690
|
-
isHostRef.current ? joinRoomId.host : joinRoomId.attendee
|
|
691
|
-
}`;
|
|
692
|
-
|
|
693
|
-
// Log internals for breakout room lifecycle
|
|
694
|
-
logger.log(
|
|
695
|
-
LogSource.Internals,
|
|
696
|
-
'BREAKOUT_ROOM',
|
|
697
|
-
'Checking active session',
|
|
698
|
-
{
|
|
699
|
-
isHost: isHostRef.current,
|
|
700
|
-
sessionId: stateRef.current.breakoutSessionId,
|
|
701
|
-
},
|
|
702
|
-
);
|
|
703
|
-
|
|
704
|
-
try {
|
|
705
|
-
const response = await fetch(url, {
|
|
706
|
-
method: 'GET',
|
|
707
|
-
headers: {
|
|
708
|
-
'Content-Type': 'application/json',
|
|
709
|
-
authorization: store.token ? `Bearer ${store.token}` : '',
|
|
710
|
-
'X-Request-Id': requestId,
|
|
711
|
-
'X-Session-Id': logger.getSessionId(),
|
|
712
|
-
},
|
|
713
|
-
});
|
|
714
|
-
// Guard against component unmount after fetch
|
|
715
|
-
if (!isMountedRef.current) {
|
|
716
|
-
logger.log(
|
|
717
|
-
LogSource.Internals,
|
|
718
|
-
'BREAKOUT_ROOM',
|
|
719
|
-
'Check session API cancelled - component unmounted',
|
|
720
|
-
{requestId},
|
|
721
|
-
);
|
|
722
|
-
return false;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const latency = Date.now() - startTime;
|
|
726
|
-
|
|
727
|
-
// Log network request
|
|
728
|
-
logger.log(
|
|
729
|
-
LogSource.NetworkRest,
|
|
730
|
-
'breakout-room',
|
|
731
|
-
'GET breakout-room session',
|
|
732
|
-
{
|
|
733
|
-
url,
|
|
734
|
-
method: 'GET',
|
|
735
|
-
status: response.status,
|
|
736
|
-
latency,
|
|
737
|
-
requestId,
|
|
738
|
-
},
|
|
739
|
-
);
|
|
740
|
-
if (!response.ok) {
|
|
741
|
-
throw new Error(`Failed with status ${response.status}`);
|
|
742
|
-
}
|
|
743
|
-
if (response.status === 204) {
|
|
744
|
-
logger.log(
|
|
745
|
-
LogSource.Internals,
|
|
746
|
-
'BREAKOUT_ROOM',
|
|
747
|
-
'No active session found',
|
|
748
|
-
);
|
|
749
|
-
return false;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
const data = await response.json();
|
|
753
|
-
console.log('supriya-api-get response', data.sts, data);
|
|
754
|
-
|
|
755
|
-
if (data?.session_id) {
|
|
756
|
-
logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Session exists', {
|
|
757
|
-
sessionId: data.session_id,
|
|
758
|
-
roomCount: data?.breakout_room?.length || 0,
|
|
759
|
-
assignmentType: data?.assignment_type,
|
|
760
|
-
switchRoom: data?.switch_room,
|
|
761
|
-
});
|
|
762
|
-
return true;
|
|
763
|
-
}
|
|
764
|
-
return false;
|
|
765
|
-
} catch (error) {
|
|
766
|
-
const latency = Date.now() - startTime;
|
|
767
|
-
logger.log(LogSource.NetworkRest, 'breakout-room', 'API call failed', {
|
|
768
|
-
url,
|
|
769
|
-
method: 'GET',
|
|
770
|
-
error: error.message,
|
|
771
|
-
latency,
|
|
772
|
-
requestId,
|
|
773
|
-
});
|
|
774
|
-
return false;
|
|
775
|
-
}
|
|
776
|
-
}, [isBreakoutUpdateInFlight, joinRoomId, store.token]);
|
|
777
|
-
|
|
778
|
-
useEffect(() => {
|
|
779
|
-
if (!joinRoomId?.host && !joinRoomId?.attendee) {
|
|
780
|
-
return;
|
|
781
|
-
}
|
|
782
|
-
const loadInitialData = async () => {
|
|
783
|
-
console.log(
|
|
784
|
-
'supriya-sync-queue checkIfBreakoutRoomSessionExistsAPI called',
|
|
785
|
-
);
|
|
786
|
-
await checkIfBreakoutRoomSessionExistsAPI();
|
|
787
|
-
};
|
|
788
|
-
|
|
789
|
-
// Check if we just transitioned to breakout mode as that we can delay the call
|
|
790
|
-
// to check breakout api
|
|
791
|
-
const justEnteredBreakout = sessionStorage.getItem(
|
|
792
|
-
'breakout_room_transition',
|
|
793
|
-
);
|
|
794
|
-
const delay = justEnteredBreakout ? 3000 : 1200;
|
|
795
|
-
|
|
796
|
-
if (justEnteredBreakout) {
|
|
797
|
-
sessionStorage.removeItem('breakout_room_transition'); // Clear flag
|
|
798
|
-
console.log('Using extended delay for breakout transition');
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const timeoutId = setTimeout(() => {
|
|
802
|
-
loadInitialData();
|
|
803
|
-
}, delay);
|
|
804
|
-
|
|
805
|
-
return () => {
|
|
806
|
-
clearTimeout(timeoutId);
|
|
807
|
-
};
|
|
808
|
-
}, [joinRoomId, checkIfBreakoutRoomSessionExistsAPI]);
|
|
809
|
-
|
|
810
|
-
const upsertBreakoutRoomAPI = useCallback(
|
|
811
|
-
async (type: 'START' | 'UPDATE' = 'START', retryCount = 0) => {
|
|
812
|
-
type UpsertPayload = {
|
|
813
|
-
passphrase: string;
|
|
814
|
-
switch_room: boolean;
|
|
815
|
-
session_id: string;
|
|
816
|
-
assignment_type: RoomAssignmentStrategy;
|
|
817
|
-
breakout_room: ReturnType<typeof getSanitizedPayload>;
|
|
818
|
-
join_room_id?: string;
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
const startReqTs = Date.now();
|
|
822
|
-
const requestId = getUniqueID();
|
|
823
|
-
const url = `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room`;
|
|
824
|
-
|
|
825
|
-
// Log internals for lifecycle
|
|
826
|
-
logger.log(
|
|
827
|
-
LogSource.Internals,
|
|
828
|
-
'BREAKOUT_ROOM',
|
|
829
|
-
`Upsert API called - ${type}`,
|
|
830
|
-
{
|
|
831
|
-
type,
|
|
832
|
-
isHost: isHostRef.current,
|
|
833
|
-
sessionId: stateRef.current.breakoutSessionId,
|
|
834
|
-
roomCount: stateRef.current.breakoutGroups.length,
|
|
835
|
-
assignmentStrategy: stateRef.current.assignmentStrategy,
|
|
836
|
-
canSwitchRoom: stateRef.current.canUserSwitchRoom,
|
|
837
|
-
selfJoinRoomId,
|
|
838
|
-
},
|
|
839
|
-
);
|
|
840
|
-
|
|
841
|
-
try {
|
|
842
|
-
const sessionId =
|
|
843
|
-
stateRef.current.breakoutSessionId || randomNameGenerator(6);
|
|
844
|
-
|
|
845
|
-
const payload: UpsertPayload = {
|
|
846
|
-
passphrase: isHostRef.current ? joinRoomId.host : joinRoomId.attendee,
|
|
847
|
-
switch_room: stateRef.current.canUserSwitchRoom,
|
|
848
|
-
session_id: sessionId,
|
|
849
|
-
assignment_type: stateRef.current.assignmentStrategy,
|
|
850
|
-
breakout_room:
|
|
851
|
-
type === 'START'
|
|
852
|
-
? getSanitizedPayload(
|
|
853
|
-
initialBreakoutGroups,
|
|
854
|
-
defaultContentRef,
|
|
855
|
-
mainRoomRTMUsers,
|
|
856
|
-
)
|
|
857
|
-
: getSanitizedPayload(
|
|
858
|
-
stateRef.current.breakoutGroups,
|
|
859
|
-
defaultContentRef,
|
|
860
|
-
mainRoomRTMUsers,
|
|
861
|
-
),
|
|
862
|
-
};
|
|
863
|
-
|
|
864
|
-
// Only add join_room_id if attendee has called this api(during join room)
|
|
865
|
-
if (selfJoinRoomId) {
|
|
866
|
-
payload.join_room_id = selfJoinRoomId;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const response = await fetch(url, {
|
|
870
|
-
method: 'POST',
|
|
871
|
-
headers: {
|
|
872
|
-
'Content-Type': 'application/json',
|
|
873
|
-
authorization: store.token ? `Bearer ${store.token}` : '',
|
|
874
|
-
'X-Request-Id': requestId,
|
|
875
|
-
'X-Session-Id': logger.getSessionId(),
|
|
876
|
-
},
|
|
877
|
-
body: JSON.stringify(payload),
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
// Guard against component unmount after fetch
|
|
881
|
-
if (!isMountedRef.current) {
|
|
882
|
-
logger.log(
|
|
883
|
-
LogSource.Internals,
|
|
884
|
-
'BREAKOUT_ROOM',
|
|
885
|
-
'Upsert API cancelled - component unmounted after fetch',
|
|
886
|
-
{type, requestId},
|
|
887
|
-
);
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
const endRequestTs = Date.now();
|
|
892
|
-
const latency = endRequestTs - startReqTs;
|
|
893
|
-
|
|
894
|
-
// Log network request
|
|
895
|
-
logger.log(
|
|
896
|
-
LogSource.NetworkRest,
|
|
897
|
-
'breakout-room',
|
|
898
|
-
'POST breakout-room upsert',
|
|
899
|
-
{
|
|
900
|
-
url,
|
|
901
|
-
method: 'POST',
|
|
902
|
-
status: response.status,
|
|
903
|
-
latency,
|
|
904
|
-
requestId,
|
|
905
|
-
type,
|
|
906
|
-
payloadSize: JSON.stringify(payload).length,
|
|
907
|
-
},
|
|
908
|
-
);
|
|
909
|
-
|
|
910
|
-
if (!response.ok) {
|
|
911
|
-
const msg = await response.text();
|
|
912
|
-
|
|
913
|
-
// 🛡️ Guard against component unmount after error text parsing
|
|
914
|
-
if (!isMountedRef.current) {
|
|
915
|
-
logger.log(
|
|
916
|
-
LogSource.Internals,
|
|
917
|
-
'BREAKOUT_ROOM',
|
|
918
|
-
'Error text parsing cancelled - component unmounted',
|
|
919
|
-
{type, status: response.status, requestId},
|
|
920
|
-
);
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
throw new Error(`Breakout room creation failed: ${msg}`);
|
|
925
|
-
} else {
|
|
926
|
-
const data = await response.json();
|
|
927
|
-
console.log('supriya-api-upsert response', data.sts, data);
|
|
928
|
-
|
|
929
|
-
// 🛡️ Guard against component unmount after JSON parsing
|
|
930
|
-
if (!isMountedRef.current) {
|
|
931
|
-
logger.log(
|
|
932
|
-
LogSource.Internals,
|
|
933
|
-
'BREAKOUT_ROOM',
|
|
934
|
-
'Upsert API success cancelled - component unmounted after parsing',
|
|
935
|
-
{type, requestId},
|
|
936
|
-
);
|
|
937
|
-
return;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
logger.log(
|
|
941
|
-
LogSource.Internals,
|
|
942
|
-
'BREAKOUT_ROOM',
|
|
943
|
-
`Upsert API success - ${type}`,
|
|
944
|
-
{
|
|
945
|
-
type,
|
|
946
|
-
newSessionId: data?.session_id,
|
|
947
|
-
roomsUpdated: !!data?.breakout_room,
|
|
948
|
-
latency,
|
|
949
|
-
},
|
|
950
|
-
);
|
|
951
|
-
|
|
952
|
-
if (type === 'START' && data?.session_id) {
|
|
953
|
-
dispatch({
|
|
954
|
-
type: BreakoutGroupActionTypes.SET_SESSION_ID,
|
|
955
|
-
payload: {sessionId: data.session_id},
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
} catch (err) {
|
|
960
|
-
const latency = Date.now() - startReqTs;
|
|
961
|
-
const maxRetries = 3;
|
|
962
|
-
const isRetriableError =
|
|
963
|
-
err.name === 'TypeError' || // Network errors
|
|
964
|
-
err.message.includes('fetch') ||
|
|
965
|
-
err.message.includes('timeout') ||
|
|
966
|
-
err.response?.status >= 500; // Server errors
|
|
967
|
-
|
|
968
|
-
logger.log(
|
|
969
|
-
LogSource.NetworkRest,
|
|
970
|
-
'breakout-room',
|
|
971
|
-
'Upsert API failed',
|
|
972
|
-
{
|
|
973
|
-
url,
|
|
974
|
-
method: 'POST',
|
|
975
|
-
error: err.message,
|
|
976
|
-
latency,
|
|
977
|
-
requestId,
|
|
978
|
-
type,
|
|
979
|
-
retryCount,
|
|
980
|
-
isRetriableError,
|
|
981
|
-
willRetry: retryCount < maxRetries && isRetriableError,
|
|
982
|
-
},
|
|
983
|
-
);
|
|
984
|
-
|
|
985
|
-
// 🛡️ Retry logic for network/server errors
|
|
986
|
-
if (retryCount < maxRetries && isRetriableError) {
|
|
987
|
-
const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 5000); // Exponential backoff, max 5s
|
|
988
|
-
|
|
989
|
-
logger.log(
|
|
990
|
-
LogSource.Internals,
|
|
991
|
-
'BREAKOUT_ROOM',
|
|
992
|
-
`Retrying upsert API in ${retryDelay}ms`,
|
|
993
|
-
{retryCount: retryCount + 1, maxRetries, type},
|
|
994
|
-
);
|
|
995
|
-
|
|
996
|
-
// Don't clear polling/selfJoinRoomId on retry
|
|
997
|
-
safeSetTimeout(() => {
|
|
998
|
-
// 🛡️ Guard against component unmount during retry delay
|
|
999
|
-
if (!isMountedRef.current) {
|
|
1000
|
-
logger.log(
|
|
1001
|
-
LogSource.Internals,
|
|
1002
|
-
'BREAKOUT_ROOM',
|
|
1003
|
-
'API retry cancelled - component unmounted',
|
|
1004
|
-
{type, retryCount: retryCount + 1},
|
|
1005
|
-
);
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
console.log('supriya-state-sync calling upsertBreakoutRoomAPI 941');
|
|
1009
|
-
upsertBreakoutRoomAPI(type, retryCount + 1);
|
|
1010
|
-
}, retryDelay);
|
|
1011
|
-
return; // Don't execute finally block on retry
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// 🛡️ Only clear state if we're not retrying
|
|
1015
|
-
setSelfJoinRoomId(null);
|
|
1016
|
-
} finally {
|
|
1017
|
-
// 🛡️ Only clear state on successful completion (not on retry)
|
|
1018
|
-
if (retryCount === 0) {
|
|
1019
|
-
setSelfJoinRoomId(null);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
},
|
|
1023
|
-
[
|
|
1024
|
-
joinRoomId.host,
|
|
1025
|
-
store.token,
|
|
1026
|
-
dispatch,
|
|
1027
|
-
selfJoinRoomId,
|
|
1028
|
-
joinRoomId.attendee,
|
|
1029
|
-
mainRoomRTMUsers,
|
|
1030
|
-
],
|
|
1031
|
-
);
|
|
1032
|
-
|
|
1033
|
-
const setManualAssignments = useCallback(
|
|
1034
|
-
(assignments: ManualParticipantAssignment[]) => {
|
|
1035
|
-
dispatch({
|
|
1036
|
-
type: BreakoutGroupActionTypes.SET_MANUAL_ASSIGNMENTS,
|
|
1037
|
-
payload: {assignments},
|
|
1038
|
-
});
|
|
1039
|
-
},
|
|
1040
|
-
[dispatch],
|
|
1041
|
-
);
|
|
1042
|
-
|
|
1043
|
-
const clearManualAssignments = useCallback(() => {
|
|
1044
|
-
dispatch({
|
|
1045
|
-
type: BreakoutGroupActionTypes.CLEAR_MANUAL_ASSIGNMENTS,
|
|
1046
|
-
});
|
|
1047
|
-
}, [dispatch]);
|
|
1048
|
-
|
|
1049
|
-
const toggleRoomSwitchingAllowed = (value: boolean) => {
|
|
1050
|
-
console.log(
|
|
1051
|
-
'supriya-state-sync toggleRoomSwitchingAllowed value is',
|
|
1052
|
-
value,
|
|
1053
|
-
);
|
|
1054
|
-
if (!acquireOperationLock('SET_ALLOW_PEOPLE_TO_SWITCH_ROOM')) {
|
|
1055
|
-
console.log('supriya-state-sync lock acquired');
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
|
-
logger.log(
|
|
1060
|
-
LogSource.Internals,
|
|
1061
|
-
'BREAKOUT_ROOM',
|
|
1062
|
-
'Switch rooms permission changed',
|
|
1063
|
-
{
|
|
1064
|
-
previousValue: stateRef.current.canUserSwitchRoom,
|
|
1065
|
-
newValue: value,
|
|
1066
|
-
isHost: isHostRef.current,
|
|
1067
|
-
roomCount: stateRef.current.breakoutGroups.length,
|
|
1068
|
-
},
|
|
1069
|
-
);
|
|
1070
|
-
console.log(
|
|
1071
|
-
'supriya-state-sync dispatching SET_ALLOW_PEOPLE_TO_SWITCH_ROOM',
|
|
1072
|
-
);
|
|
1073
|
-
|
|
1074
|
-
dispatch({
|
|
1075
|
-
type: BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM,
|
|
1076
|
-
payload: {
|
|
1077
|
-
canUserSwitchRoom: value,
|
|
1078
|
-
},
|
|
1079
|
-
});
|
|
1080
|
-
};
|
|
1081
|
-
|
|
1082
|
-
const createBreakoutRoomGroup = () => {
|
|
1083
|
-
if (!acquireOperationLock('CREATE_GROUP')) {
|
|
1084
|
-
return;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
logger.log(
|
|
1088
|
-
LogSource.Internals,
|
|
1089
|
-
'BREAKOUT_ROOM',
|
|
1090
|
-
'Creating new breakout room',
|
|
1091
|
-
{
|
|
1092
|
-
currentRoomCount: stateRef.current.breakoutGroups.length,
|
|
1093
|
-
isHost: isHostRef.current,
|
|
1094
|
-
sessionId: stateRef.current.breakoutSessionId,
|
|
1095
|
-
},
|
|
1096
|
-
);
|
|
1097
|
-
|
|
1098
|
-
dispatch({
|
|
1099
|
-
type: BreakoutGroupActionTypes.CREATE_GROUP,
|
|
1100
|
-
});
|
|
1101
|
-
};
|
|
1102
|
-
|
|
1103
|
-
const handleAssignParticipants = (strategy: RoomAssignmentStrategy) => {
|
|
1104
|
-
console.log('supriya-assign', stateRef.current);
|
|
1105
|
-
if (stateRef.current.breakoutGroups.length === 0) {
|
|
1106
|
-
Toast.show({
|
|
1107
|
-
type: 'info',
|
|
1108
|
-
text1: 'No breakout rooms found.',
|
|
1109
|
-
visibilityTime: 3000,
|
|
1110
|
-
});
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
|
|
1114
|
-
// Check for participants available for assignment based on strategy
|
|
1115
|
-
const availableParticipants =
|
|
1116
|
-
strategy === RoomAssignmentStrategy.AUTO_ASSIGN
|
|
1117
|
-
? stateRef.current.unassignedParticipants.filter(
|
|
1118
|
-
participant => participant.uid !== localUid,
|
|
1119
|
-
)
|
|
1120
|
-
: stateRef.current.unassignedParticipants;
|
|
1121
|
-
|
|
1122
|
-
if (availableParticipants.length === 0) {
|
|
1123
|
-
const message =
|
|
1124
|
-
strategy === RoomAssignmentStrategy.AUTO_ASSIGN &&
|
|
1125
|
-
stateRef.current.unassignedParticipants.length > 0
|
|
1126
|
-
? 'No other participants to assign. (Host is excluded from auto-assignment)'
|
|
1127
|
-
: 'No participants left to assign.';
|
|
1128
|
-
|
|
1129
|
-
Toast.show({
|
|
1130
|
-
type: 'info',
|
|
1131
|
-
text1: message,
|
|
1132
|
-
visibilityTime: 3000,
|
|
1133
|
-
});
|
|
1134
|
-
return;
|
|
1135
|
-
}
|
|
1136
|
-
if (!acquireOperationLock(`ASSIGN_${strategy}`)) {
|
|
1137
|
-
return;
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Assigning participants', {
|
|
1141
|
-
strategy,
|
|
1142
|
-
unassignedCount: stateRef.current.unassignedParticipants.length,
|
|
1143
|
-
roomCount: stateRef.current.breakoutGroups.length,
|
|
1144
|
-
isHost: isHostRef.current,
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
if (strategy === RoomAssignmentStrategy.AUTO_ASSIGN) {
|
|
1148
|
-
dispatch({
|
|
1149
|
-
type: BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS,
|
|
1150
|
-
payload: {
|
|
1151
|
-
localUid,
|
|
1152
|
-
},
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
if (strategy === RoomAssignmentStrategy.MANUAL_ASSIGN) {
|
|
1156
|
-
dispatch({
|
|
1157
|
-
type: BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS,
|
|
1158
|
-
});
|
|
1159
|
-
}
|
|
1160
|
-
if (strategy === RoomAssignmentStrategy.NO_ASSIGN) {
|
|
1161
|
-
dispatch({
|
|
1162
|
-
type: BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS,
|
|
1163
|
-
});
|
|
1164
|
-
}
|
|
1165
|
-
};
|
|
1166
|
-
|
|
1167
|
-
const moveUserToMainRoom = (uid: UidType) => {
|
|
1168
|
-
try {
|
|
1169
|
-
if (!uid) {
|
|
1170
|
-
logger.log(
|
|
1171
|
-
LogSource.Internals,
|
|
1172
|
-
'BREAKOUT_ROOM',
|
|
1173
|
-
'Move to main room failed - no uid provided',
|
|
1174
|
-
);
|
|
1175
|
-
return;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
1178
|
-
// 🛡️ Check for API operation conflicts first
|
|
1179
|
-
if (!acquireOperationLock('MOVE_PARTICIPANT_TO_MAIN')) {
|
|
1180
|
-
return;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
// 🛡️ Use fresh state to avoid race conditions
|
|
1184
|
-
const currentState = stateRef.current;
|
|
1185
|
-
const currentGroup = currentState.breakoutGroups.find(
|
|
1186
|
-
group =>
|
|
1187
|
-
group.participants.hosts.includes(uid) ||
|
|
1188
|
-
group.participants.attendees.includes(uid),
|
|
1189
|
-
);
|
|
1190
|
-
|
|
1191
|
-
logger.log(
|
|
1192
|
-
LogSource.Internals,
|
|
1193
|
-
'BREAKOUT_ROOM',
|
|
1194
|
-
'Moving user to main room',
|
|
1195
|
-
{
|
|
1196
|
-
userId: uid,
|
|
1197
|
-
fromGroupId: currentGroup?.id,
|
|
1198
|
-
fromGroupName: currentGroup?.name,
|
|
1199
|
-
},
|
|
1200
|
-
);
|
|
1201
|
-
|
|
1202
|
-
if (currentGroup) {
|
|
1203
|
-
dispatch({
|
|
1204
|
-
type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN,
|
|
1205
|
-
payload: {
|
|
1206
|
-
uid,
|
|
1207
|
-
fromGroupId: currentGroup.id,
|
|
1208
|
-
},
|
|
1209
|
-
});
|
|
1210
|
-
}
|
|
1211
|
-
} catch (error) {
|
|
1212
|
-
logger.log(
|
|
1213
|
-
LogSource.Internals,
|
|
1214
|
-
'BREAKOUT_ROOM',
|
|
1215
|
-
'Error moving user to main room',
|
|
1216
|
-
{
|
|
1217
|
-
userId: uid,
|
|
1218
|
-
error: error.message,
|
|
1219
|
-
},
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
};
|
|
1223
|
-
|
|
1224
|
-
const moveUserIntoGroup = (uid: UidType, toGroupId: string) => {
|
|
1225
|
-
try {
|
|
1226
|
-
if (!uid) {
|
|
1227
|
-
logger.log(
|
|
1228
|
-
LogSource.Internals,
|
|
1229
|
-
'BREAKOUT_ROOM',
|
|
1230
|
-
'Move to group failed - no uid provided',
|
|
1231
|
-
{toGroupId},
|
|
1232
|
-
);
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
// 🛡️ Check for API operation conflicts first
|
|
1237
|
-
if (!acquireOperationLock('MOVE_PARTICIPANT_TO_GROUP')) {
|
|
1238
|
-
return;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
// 🛡️ Use fresh state to avoid race conditions
|
|
1242
|
-
const currentState = stateRef.current;
|
|
1243
|
-
const currentGroup = currentState.breakoutGroups.find(
|
|
1244
|
-
group =>
|
|
1245
|
-
group.participants.hosts.includes(uid) ||
|
|
1246
|
-
group.participants.attendees.includes(uid),
|
|
1247
|
-
);
|
|
1248
|
-
const targetGroup = currentState.breakoutGroups.find(
|
|
1249
|
-
group => group.id === toGroupId,
|
|
1250
|
-
);
|
|
1251
|
-
|
|
1252
|
-
if (!targetGroup) {
|
|
1253
|
-
logger.log(
|
|
1254
|
-
LogSource.Internals,
|
|
1255
|
-
'BREAKOUT_ROOM',
|
|
1256
|
-
'Target group not found',
|
|
1257
|
-
{
|
|
1258
|
-
userId: uid,
|
|
1259
|
-
toGroupId,
|
|
1260
|
-
},
|
|
1261
|
-
);
|
|
1262
|
-
|
|
1263
|
-
return;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
logger.log(
|
|
1267
|
-
LogSource.Internals,
|
|
1268
|
-
'BREAKOUT_ROOM',
|
|
1269
|
-
'Moving user between groups',
|
|
1270
|
-
{
|
|
1271
|
-
userId: uid,
|
|
1272
|
-
fromGroupId: currentGroup?.id,
|
|
1273
|
-
fromGroupName: currentGroup?.name,
|
|
1274
|
-
toGroupId,
|
|
1275
|
-
toGroupName: targetGroup.name,
|
|
1276
|
-
},
|
|
1277
|
-
);
|
|
1278
|
-
|
|
1279
|
-
// // Clean up presenter status if user is switching rooms
|
|
1280
|
-
// const isPresenting = presenters.some(p => p.uid === uid);
|
|
1281
|
-
// if (isPresenting) {
|
|
1282
|
-
// setCustomRTMMainRoomData(prev => ({
|
|
1283
|
-
// ...prev,
|
|
1284
|
-
// breakout_room_presenters: (
|
|
1285
|
-
// prev.breakout_room_presenters || []
|
|
1286
|
-
// ).filter((p: any) => p.uid !== uid),
|
|
1287
|
-
// }));
|
|
1288
|
-
|
|
1289
|
-
// // Notify the user that their presenter access was removed
|
|
1290
|
-
// try {
|
|
1291
|
-
// events.send(
|
|
1292
|
-
// BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER,
|
|
1293
|
-
// JSON.stringify({
|
|
1294
|
-
// uid: uid,
|
|
1295
|
-
// timestamp: Date.now(),
|
|
1296
|
-
// action: 'stop',
|
|
1297
|
-
// }),
|
|
1298
|
-
// PersistanceLevel.None,
|
|
1299
|
-
// uid,
|
|
1300
|
-
// );
|
|
1301
|
-
// } catch (error) {
|
|
1302
|
-
// logger.log(
|
|
1303
|
-
// LogSource.Internals,
|
|
1304
|
-
// 'BREAKOUT_ROOM',
|
|
1305
|
-
// 'Error sending presenter stop event on room switch',
|
|
1306
|
-
// {error: error.message},
|
|
1307
|
-
// );
|
|
1308
|
-
// }
|
|
1309
|
-
// }
|
|
1310
|
-
|
|
1311
|
-
// Check if user is a host
|
|
1312
|
-
let isUserHost: boolean | undefined;
|
|
1313
|
-
if (currentGroup) {
|
|
1314
|
-
// User is moving from another breakout room
|
|
1315
|
-
isUserHost = currentGroup.participants.hosts.includes(uid);
|
|
1316
|
-
} else {
|
|
1317
|
-
// User is moving from main room - check mainRoomRTMUsers
|
|
1318
|
-
const rtmUser = mainRoomRTMUsers[uid];
|
|
1319
|
-
if (rtmUser) {
|
|
1320
|
-
isUserHost = rtmUser.isHost === 'true';
|
|
1321
|
-
}
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
dispatch({
|
|
1325
|
-
type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP,
|
|
1326
|
-
payload: {
|
|
1327
|
-
uid,
|
|
1328
|
-
fromGroupId: currentGroup?.id,
|
|
1329
|
-
toGroupId,
|
|
1330
|
-
isHost: isUserHost,
|
|
1331
|
-
},
|
|
1332
|
-
});
|
|
1333
|
-
} catch (error) {
|
|
1334
|
-
logger.log(
|
|
1335
|
-
LogSource.Internals,
|
|
1336
|
-
'BREAKOUT_ROOM',
|
|
1337
|
-
'Error moving user to breakout room',
|
|
1338
|
-
{
|
|
1339
|
-
userId: uid,
|
|
1340
|
-
toGroupId,
|
|
1341
|
-
error: error.message,
|
|
1342
|
-
},
|
|
1343
|
-
);
|
|
1344
|
-
}
|
|
1345
|
-
};
|
|
1346
|
-
|
|
1347
|
-
const isUserInRoom = useCallback(
|
|
1348
|
-
(room?: BreakoutGroup): boolean => {
|
|
1349
|
-
if (room) {
|
|
1350
|
-
// Check specific room
|
|
1351
|
-
return (
|
|
1352
|
-
room.participants.hosts.includes(localUid) ||
|
|
1353
|
-
room.participants.attendees.includes(localUid)
|
|
1354
|
-
);
|
|
1355
|
-
} else {
|
|
1356
|
-
// Check ALL rooms - is user in any room?
|
|
1357
|
-
return stateRef.current.breakoutGroups.some(
|
|
1358
|
-
group =>
|
|
1359
|
-
group.participants.hosts.includes(localUid) ||
|
|
1360
|
-
group.participants.attendees.includes(localUid),
|
|
1361
|
-
);
|
|
1362
|
-
}
|
|
1363
|
-
},
|
|
1364
|
-
[localUid, breakoutRoomVersion],
|
|
1365
|
-
);
|
|
1366
|
-
|
|
1367
|
-
const findUserRoomId = (uid: UidType, groups: BreakoutGroup[] = []) =>
|
|
1368
|
-
groups.find(g => {
|
|
1369
|
-
const hosts = Array.isArray(g?.participants?.hosts)
|
|
1370
|
-
? g.participants.hosts
|
|
1371
|
-
: [];
|
|
1372
|
-
const attendees = Array.isArray(g?.participants?.attendees)
|
|
1373
|
-
? g.participants.attendees
|
|
1374
|
-
: [];
|
|
1375
|
-
return hosts.includes(uid) || attendees.includes(uid);
|
|
1376
|
-
})?.id ?? null;
|
|
1377
|
-
|
|
1378
|
-
// Permissions
|
|
1379
|
-
useEffect(() => {
|
|
1380
|
-
if (lastSyncedSnapshotRef.current) {
|
|
1381
|
-
const current = lastSyncedSnapshotRef.current;
|
|
1382
|
-
|
|
1383
|
-
const currentlyInRoom = !!findUserRoomId(localUid, current.breakout_room);
|
|
1384
|
-
const hasAvailableRooms = current.breakout_room?.length > 0;
|
|
1385
|
-
const allowAttendeeSwitch = current.switch_room;
|
|
1386
|
-
console.log(
|
|
1387
|
-
'supriya-canraisehands',
|
|
1388
|
-
!isHostRef.current && !!current.session_id && currentlyInRoom,
|
|
1389
|
-
localUid,
|
|
1390
|
-
current.breakout_room,
|
|
1391
|
-
);
|
|
1392
|
-
const nextPermissions: BreakoutRoomPermissions = {
|
|
1393
|
-
canJoinRoom:
|
|
1394
|
-
hasAvailableRooms && (isHostRef.current || allowAttendeeSwitch),
|
|
1395
|
-
canExitRoom: isBreakoutMode && currentlyInRoom,
|
|
1396
|
-
canSwitchBetweenRooms:
|
|
1397
|
-
currentlyInRoom &&
|
|
1398
|
-
hasAvailableRooms &&
|
|
1399
|
-
(isHostRef.current || allowAttendeeSwitch),
|
|
1400
|
-
canScreenshare: true,
|
|
1401
|
-
// isHostRef.current
|
|
1402
|
-
// ? true
|
|
1403
|
-
// : currentlyInRoom
|
|
1404
|
-
// ? canIPresent
|
|
1405
|
-
// : true,
|
|
1406
|
-
canRaiseHands: !isHostRef.current && !!current.session_id,
|
|
1407
|
-
canAssignParticipants: isHostRef.current && !currentlyInRoom,
|
|
1408
|
-
canHostManageMainRoom: isHostRef.current,
|
|
1409
|
-
canCreateRooms: isHostRef.current,
|
|
1410
|
-
canMoveUsers: isHostRef.current,
|
|
1411
|
-
canCloseRooms:
|
|
1412
|
-
isHostRef.current && hasAvailableRooms && !!current.session_id,
|
|
1413
|
-
canMakePresenter: isHostRef.current,
|
|
1414
|
-
};
|
|
1415
|
-
setPermissions(nextPermissions);
|
|
1416
|
-
}
|
|
1417
|
-
}, [breakoutRoomVersion, isBreakoutMode, localUid]);
|
|
1418
|
-
|
|
1419
|
-
const joinRoom = (
|
|
1420
|
-
toRoomId: string,
|
|
1421
|
-
permissionAtCallTime = permissions.canJoinRoom,
|
|
1422
|
-
) => {
|
|
1423
|
-
if (!permissionAtCallTime) {
|
|
1424
|
-
logger.log(
|
|
1425
|
-
LogSource.Internals,
|
|
1426
|
-
'BREAKOUT_ROOM',
|
|
1427
|
-
'Join room blocked - no permission at call time',
|
|
1428
|
-
{
|
|
1429
|
-
toRoomId,
|
|
1430
|
-
permissionAtCallTime,
|
|
1431
|
-
currentPermission: permissions.canJoinRoom,
|
|
1432
|
-
},
|
|
1433
|
-
);
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
if (!localUid) {
|
|
1437
|
-
logger.log(
|
|
1438
|
-
LogSource.Internals,
|
|
1439
|
-
'BREAKOUT_ROOM',
|
|
1440
|
-
'Join room failed - user not found',
|
|
1441
|
-
{localUid, toRoomId},
|
|
1442
|
-
);
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'User joining room', {
|
|
1447
|
-
userId: localUid,
|
|
1448
|
-
toRoomId,
|
|
1449
|
-
toRoomName: stateRef.current.breakoutGroups.find(r => r.id === toRoomId)
|
|
1450
|
-
?.name,
|
|
1451
|
-
});
|
|
1452
|
-
lastSelfJoinRef.current = {roomId: toRoomId, ts: Date.now()};
|
|
1453
|
-
moveUserIntoGroup(localUid, toRoomId);
|
|
1454
|
-
if (!isHostRef.current) {
|
|
1455
|
-
setSelfJoinRoomId(toRoomId);
|
|
1456
|
-
}
|
|
1457
|
-
};
|
|
1458
|
-
|
|
1459
|
-
const exitRoom = useCallback(
|
|
1460
|
-
async (permissionAtCallTime = permissions.canExitRoom) => {
|
|
1461
|
-
// 🛡️ Use permission passed at call time to avoid race conditions
|
|
1462
|
-
if (!permissionAtCallTime) {
|
|
1463
|
-
logger.log(
|
|
1464
|
-
LogSource.Internals,
|
|
1465
|
-
'BREAKOUT_ROOM',
|
|
1466
|
-
'Exit room blocked - no permission at call time',
|
|
1467
|
-
{
|
|
1468
|
-
permissionAtCallTime,
|
|
1469
|
-
currentPermission: permissions.canExitRoom,
|
|
1470
|
-
},
|
|
1471
|
-
);
|
|
1472
|
-
return;
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
// If u are reciving or calling this tha means u will have
|
|
1476
|
-
// valid data in defaultcontent as u cannot exit from the room
|
|
1477
|
-
// you are not in
|
|
1478
|
-
const localUser = defaultContentRef.current[localUid];
|
|
1479
|
-
|
|
1480
|
-
// // Clean up presenter status if user is presenting
|
|
1481
|
-
// const isPresenting = presenters.some(p => p.uid === localUid);
|
|
1482
|
-
// if (isPresenting) {
|
|
1483
|
-
// setCustomRTMMainRoomData(prev => ({
|
|
1484
|
-
// ...prev,
|
|
1485
|
-
// breakout_room_presenters: (
|
|
1486
|
-
// prev.breakout_room_presenters || []
|
|
1487
|
-
// ).filter((p: any) => p.uid !== localUid),
|
|
1488
|
-
// }));
|
|
1489
|
-
// setICanPresent(false);
|
|
1490
|
-
// }
|
|
1491
|
-
|
|
1492
|
-
try {
|
|
1493
|
-
if (localUser) {
|
|
1494
|
-
// Use breakout-specific exit (doesn't destroy main RTM)
|
|
1495
|
-
await breakoutRoomExit();
|
|
1496
|
-
|
|
1497
|
-
// 🛡️ Guard against component unmount
|
|
1498
|
-
if (!isMountedRef.current) {
|
|
1499
|
-
logger.log(
|
|
1500
|
-
LogSource.Internals,
|
|
1501
|
-
'BREAKOUT_ROOM',
|
|
1502
|
-
'Exit room cancelled - component unmounted',
|
|
1503
|
-
{userId: localUid},
|
|
1504
|
-
);
|
|
1505
|
-
return;
|
|
1506
|
-
}
|
|
1507
|
-
}
|
|
1508
|
-
} catch (error) {
|
|
1509
|
-
logger.log(
|
|
1510
|
-
LogSource.Internals,
|
|
1511
|
-
'BREAKOUT_ROOM',
|
|
1512
|
-
'Exit room error - fallback dispatch',
|
|
1513
|
-
{
|
|
1514
|
-
userId: localUid,
|
|
1515
|
-
error: error.message,
|
|
1516
|
-
},
|
|
1517
|
-
);
|
|
1518
|
-
}
|
|
1519
|
-
},
|
|
1520
|
-
[
|
|
1521
|
-
localUid,
|
|
1522
|
-
permissions.canExitRoom, // TODO:SUP move to the method call
|
|
1523
|
-
breakoutRoomExit,
|
|
1524
|
-
],
|
|
1525
|
-
);
|
|
1526
|
-
|
|
1527
|
-
const closeRoom = (roomIdToClose: string) => {
|
|
1528
|
-
if (!acquireOperationLock('CLOSE_GROUP')) {
|
|
1529
|
-
return;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
const roomToClose = stateRef.current.breakoutGroups.find(
|
|
1533
|
-
r => r.id === roomIdToClose,
|
|
1534
|
-
);
|
|
1535
|
-
|
|
1536
|
-
logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Closing breakout room', {
|
|
1537
|
-
roomId: roomIdToClose,
|
|
1538
|
-
roomName: roomToClose?.name,
|
|
1539
|
-
participantCount:
|
|
1540
|
-
(roomToClose?.participants.hosts.length || 0) +
|
|
1541
|
-
(roomToClose?.participants.attendees.length || 0),
|
|
1542
|
-
isHost: isHostRef.current,
|
|
1543
|
-
});
|
|
1544
|
-
|
|
1545
|
-
dispatch({
|
|
1546
|
-
type: BreakoutGroupActionTypes.CLOSE_GROUP,
|
|
1547
|
-
payload: {groupId: roomIdToClose},
|
|
1548
|
-
});
|
|
1549
|
-
};
|
|
1550
|
-
|
|
1551
|
-
const closeAllRooms = () => {
|
|
1552
|
-
if (!acquireOperationLock('CLOSE_ALL_GROUPS')) {
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
logger.log(
|
|
1557
|
-
LogSource.Internals,
|
|
1558
|
-
'BREAKOUT_ROOM',
|
|
1559
|
-
'Closing all breakout rooms',
|
|
1560
|
-
{
|
|
1561
|
-
roomCount: stateRef.current.breakoutGroups.length,
|
|
1562
|
-
totalParticipants: stateRef.current.breakoutGroups.reduce(
|
|
1563
|
-
(sum, room) =>
|
|
1564
|
-
sum +
|
|
1565
|
-
room.participants.hosts.length +
|
|
1566
|
-
room.participants.attendees.length,
|
|
1567
|
-
0,
|
|
1568
|
-
),
|
|
1569
|
-
isHost: isHostRef.current,
|
|
1570
|
-
sessionId: stateRef.current.breakoutSessionId,
|
|
1571
|
-
},
|
|
1572
|
-
);
|
|
1573
|
-
|
|
1574
|
-
// Clear all presenters when closing all rooms
|
|
1575
|
-
// clearAllPresenters();
|
|
1576
|
-
|
|
1577
|
-
dispatch({type: BreakoutGroupActionTypes.CLOSE_ALL_GROUPS});
|
|
1578
|
-
};
|
|
1579
|
-
|
|
1580
|
-
const updateRoomName = (newRoomName: string, roomIdToEdit: string) => {
|
|
1581
|
-
if (!acquireOperationLock('RENAME_GROUP')) {
|
|
1582
|
-
return;
|
|
1583
|
-
}
|
|
1584
|
-
|
|
1585
|
-
const roomToRename = stateRef.current.breakoutGroups.find(
|
|
1586
|
-
r => r.id === roomIdToEdit,
|
|
1587
|
-
);
|
|
1588
|
-
|
|
1589
|
-
logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Renaming breakout room', {
|
|
1590
|
-
roomId: roomIdToEdit,
|
|
1591
|
-
oldName: roomToRename?.name,
|
|
1592
|
-
newName: newRoomName,
|
|
1593
|
-
isHost: isHostRef.current,
|
|
1594
|
-
});
|
|
1595
|
-
|
|
1596
|
-
dispatch({
|
|
1597
|
-
type: BreakoutGroupActionTypes.RENAME_GROUP,
|
|
1598
|
-
payload: {newName: newRoomName, groupId: roomIdToEdit},
|
|
1599
|
-
});
|
|
1600
|
-
};
|
|
1601
|
-
|
|
1602
|
-
const getAllRooms = () => {
|
|
1603
|
-
return stateRef.current.breakoutGroups.length > 0
|
|
1604
|
-
? stateRef.current.breakoutGroups
|
|
1605
|
-
: [];
|
|
1606
|
-
};
|
|
1607
|
-
|
|
1608
|
-
// const isUserPresenting = useCallback(
|
|
1609
|
-
// (uid?: UidType) => {
|
|
1610
|
-
// if (uid !== undefined) {
|
|
1611
|
-
// return presenters.some(presenter => presenter.uid === uid);
|
|
1612
|
-
// }
|
|
1613
|
-
// // fall back to current user
|
|
1614
|
-
// return canIPresent;
|
|
1615
|
-
// },
|
|
1616
|
-
// [presenters, canIPresent],
|
|
1617
|
-
// );
|
|
1618
|
-
|
|
1619
|
-
// // User wants to start presenting
|
|
1620
|
-
// const makePresenter = (uid: UidType, action: 'start' | 'stop') => {
|
|
1621
|
-
// logger.log(
|
|
1622
|
-
// LogSource.Internals,
|
|
1623
|
-
// 'BREAKOUT_ROOM',
|
|
1624
|
-
// `Make presenter - ${action}`,
|
|
1625
|
-
// {
|
|
1626
|
-
// targetUserId: uid,
|
|
1627
|
-
// action,
|
|
1628
|
-
// isHost: isHostRef.current,
|
|
1629
|
-
// },
|
|
1630
|
-
// );
|
|
1631
|
-
// if (!uid) {
|
|
1632
|
-
// return;
|
|
1633
|
-
// }
|
|
1634
|
-
// try {
|
|
1635
|
-
// const timestamp = Date.now();
|
|
1636
|
-
// console.log('supriya-presenter sending make presenter');
|
|
1637
|
-
// // Host sends BREAKOUT_ROOM_MAKE_PRESENTER event to the attendee
|
|
1638
|
-
// events.send(
|
|
1639
|
-
// BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER,
|
|
1640
|
-
// JSON.stringify({
|
|
1641
|
-
// uid: uid,
|
|
1642
|
-
// timestamp,
|
|
1643
|
-
// action,
|
|
1644
|
-
// }),
|
|
1645
|
-
// PersistanceLevel.None,
|
|
1646
|
-
// uid,
|
|
1647
|
-
// );
|
|
1648
|
-
|
|
1649
|
-
// // Host immediately updates their own customRTMMainRoomData
|
|
1650
|
-
// if (action === 'start') {
|
|
1651
|
-
// setCustomRTMMainRoomData(prev => {
|
|
1652
|
-
// const currentPresenters = prev.breakout_room_presenters || [];
|
|
1653
|
-
// // Check if already presenting to avoid duplicates
|
|
1654
|
-
// const exists = currentPresenters.find(
|
|
1655
|
-
// (presenter: any) => presenter.uid === uid,
|
|
1656
|
-
// );
|
|
1657
|
-
// if (exists) {
|
|
1658
|
-
// return prev;
|
|
1659
|
-
// }
|
|
1660
|
-
// return {
|
|
1661
|
-
// ...prev,
|
|
1662
|
-
// breakout_room_presenters: [...currentPresenters, {uid, timestamp}],
|
|
1663
|
-
// };
|
|
1664
|
-
// });
|
|
1665
|
-
// } else if (action === 'stop') {
|
|
1666
|
-
// setCustomRTMMainRoomData(prev => ({
|
|
1667
|
-
// ...prev,
|
|
1668
|
-
// breakout_room_presenters: (
|
|
1669
|
-
// prev.breakout_room_presenters || []
|
|
1670
|
-
// ).filter((presenter: any) => presenter.uid !== uid),
|
|
1671
|
-
// }));
|
|
1672
|
-
// }
|
|
1673
|
-
// } catch (error) {
|
|
1674
|
-
// logger.log(
|
|
1675
|
-
// LogSource.Internals,
|
|
1676
|
-
// 'BREAKOUT_ROOM',
|
|
1677
|
-
// 'Error making user presenter',
|
|
1678
|
-
// {
|
|
1679
|
-
// targetUserId: uid,
|
|
1680
|
-
// action,
|
|
1681
|
-
// error: error.message,
|
|
1682
|
-
// },
|
|
1683
|
-
// );
|
|
1684
|
-
// }
|
|
1685
|
-
// };
|
|
1686
|
-
|
|
1687
|
-
// const onMakeMePresenter = useCallback(
|
|
1688
|
-
// (action: 'start' | 'stop', shouldSendEvent: boolean = true) => {
|
|
1689
|
-
// logger.log(
|
|
1690
|
-
// LogSource.Internals,
|
|
1691
|
-
// 'BREAKOUT_ROOM',
|
|
1692
|
-
// `User became presenter - ${action}`,
|
|
1693
|
-
// );
|
|
1694
|
-
|
|
1695
|
-
// const timestamp = Date.now();
|
|
1696
|
-
|
|
1697
|
-
// // Send event only if requested (not when restoring from attribute)
|
|
1698
|
-
// if (shouldSendEvent) {
|
|
1699
|
-
// // Attendee sends BREAKOUT_PRESENTER_ATTRIBUTE event to persist their presenter status
|
|
1700
|
-
// events.send(
|
|
1701
|
-
// EventNames.BREAKOUT_PRESENTER_ATTRIBUTE,
|
|
1702
|
-
// JSON.stringify({
|
|
1703
|
-
// uid: localUid,
|
|
1704
|
-
// isPresenter: action === 'start',
|
|
1705
|
-
// timestamp,
|
|
1706
|
-
// }),
|
|
1707
|
-
// PersistanceLevel.Sender,
|
|
1708
|
-
// );
|
|
1709
|
-
// }
|
|
1710
|
-
|
|
1711
|
-
// if (action === 'start') {
|
|
1712
|
-
// setICanPresent(true);
|
|
1713
|
-
// // Show toast notification when presenter permission is granted
|
|
1714
|
-
// Toast.show({
|
|
1715
|
-
// type: 'success',
|
|
1716
|
-
// text1: 'You can now present in this breakout room',
|
|
1717
|
-
// visibilityTime: 3000,
|
|
1718
|
-
// });
|
|
1719
|
-
// } else if (action === 'stop') {
|
|
1720
|
-
// if (isScreenshareActive) {
|
|
1721
|
-
// stopScreenshare();
|
|
1722
|
-
// }
|
|
1723
|
-
// setICanPresent(false);
|
|
1724
|
-
// // Show toast notification when presenter permission is removed
|
|
1725
|
-
// Toast.show({
|
|
1726
|
-
// type: 'info',
|
|
1727
|
-
// text1: 'Your presenter access has been removed',
|
|
1728
|
-
// visibilityTime: 3000,
|
|
1729
|
-
// });
|
|
1730
|
-
// }
|
|
1731
|
-
// },
|
|
1732
|
-
// [isScreenshareActive, localUid],
|
|
1733
|
-
// );
|
|
1734
|
-
|
|
1735
|
-
// const clearAllPresenters = useCallback(() => {
|
|
1736
|
-
// setCustomRTMMainRoomData(prev => ({
|
|
1737
|
-
// ...prev,
|
|
1738
|
-
// breakout_room_presenters: [],
|
|
1739
|
-
// }));
|
|
1740
|
-
// }, [setCustomRTMMainRoomData]);
|
|
1741
|
-
|
|
1742
|
-
const getRoomMemberDropdownOptions = useCallback(
|
|
1743
|
-
(memberUid: UidType) => {
|
|
1744
|
-
const options: MemberDropdownOption[] = [];
|
|
1745
|
-
// Find which room the user is currently in
|
|
1746
|
-
|
|
1747
|
-
if (!memberUid) {
|
|
1748
|
-
return options;
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
const currentRoom = stateRef.current.breakoutGroups.find(
|
|
1752
|
-
group =>
|
|
1753
|
-
group.participants.hosts.includes(memberUid) ||
|
|
1754
|
-
group.participants.attendees.includes(memberUid),
|
|
1755
|
-
);
|
|
1756
|
-
console.log(
|
|
1757
|
-
'supriya-currentRoom',
|
|
1758
|
-
currentRoom,
|
|
1759
|
-
memberUid,
|
|
1760
|
-
JSON.stringify(stateRef.current.breakoutGroups),
|
|
1761
|
-
);
|
|
1762
|
-
// Move to Main Room option
|
|
1763
|
-
options.push({
|
|
1764
|
-
icon: 'double-up-arrow',
|
|
1765
|
-
type: 'move-to-main',
|
|
1766
|
-
title: 'Move to Main Room',
|
|
1767
|
-
onOptionPress: () => moveUserToMainRoom(memberUid),
|
|
1768
|
-
});
|
|
1769
|
-
// Move to other breakout rooms (exclude current room)
|
|
1770
|
-
stateRef.current.breakoutGroups
|
|
1771
|
-
.filter(group => group.id !== currentRoom?.id)
|
|
1772
|
-
.forEach(group => {
|
|
1773
|
-
options.push({
|
|
1774
|
-
type: 'move-to-room',
|
|
1775
|
-
icon: 'move-up',
|
|
1776
|
-
title: `Shift to : ${group.name}`,
|
|
1777
|
-
roomId: group.id,
|
|
1778
|
-
roomName: group.name,
|
|
1779
|
-
onOptionPress: () => moveUserIntoGroup(memberUid, group.id),
|
|
1780
|
-
});
|
|
1781
|
-
});
|
|
1782
|
-
|
|
1783
|
-
// // Make presenter option is available only for host
|
|
1784
|
-
// // and if the incoming member is also a host we dont
|
|
1785
|
-
// // need to show this option as they can already present
|
|
1786
|
-
// const isUserHost =
|
|
1787
|
-
// currentRoom?.participants.hosts.includes(memberUid) || false;
|
|
1788
|
-
// if (isUserHost) {
|
|
1789
|
-
// return options;
|
|
1790
|
-
// }
|
|
1791
|
-
// if (isHostRef.current) {
|
|
1792
|
-
// const userIsPresenting = isUserPresenting(memberUid);
|
|
1793
|
-
// const title = userIsPresenting ? 'Stop presenter' : 'Make a Presenter';
|
|
1794
|
-
// const action = userIsPresenting ? 'stop' : 'start';
|
|
1795
|
-
// options.push({
|
|
1796
|
-
// type: 'make-presenter',
|
|
1797
|
-
// icon: 'promote-filled',
|
|
1798
|
-
// title,
|
|
1799
|
-
// onOptionPress: () => makePresenter(memberUid, action),
|
|
1800
|
-
// });
|
|
1801
|
-
// }
|
|
1802
|
-
return options;
|
|
1803
|
-
},
|
|
1804
|
-
// [isUserPresenting, presenters, breakoutRoomVersion],
|
|
1805
|
-
[breakoutRoomVersion],
|
|
1806
|
-
);
|
|
1807
|
-
|
|
1808
|
-
// const handleBreakoutRoomSyncState = useCallback(
|
|
1809
|
-
// (payload: BreakoutRoomSyncStateEventPayload['data'], timestamp) => {
|
|
1810
|
-
// console.log(
|
|
1811
|
-
// 'supriya-api-sync response',
|
|
1812
|
-
// timestamp,
|
|
1813
|
-
// JSON.stringify(payload),
|
|
1814
|
-
// );
|
|
1815
|
-
|
|
1816
|
-
// // Skip events older than the last processed timestamp
|
|
1817
|
-
// if (timestamp && timestamp <= lastProcessedTimestampRef.current) {
|
|
1818
|
-
// console.log('supriya-api-sync Skipping old breakout room sync event', {
|
|
1819
|
-
// timestamp,
|
|
1820
|
-
// lastProcessed: lastProcessedTimestampRef.current,
|
|
1821
|
-
// });
|
|
1822
|
-
// return;
|
|
1823
|
-
// }
|
|
1824
|
-
|
|
1825
|
-
// const {srcuid, data} = payload;
|
|
1826
|
-
// console.log('supriya-event flow step 2', srcuid);
|
|
1827
|
-
// console.log('supriya-event uids', srcuid, localUid);
|
|
1828
|
-
|
|
1829
|
-
// // if (srcuid === localUid) {
|
|
1830
|
-
// // console.log('supriya-event flow skipping');
|
|
1831
|
-
|
|
1832
|
-
// // return;
|
|
1833
|
-
// // }
|
|
1834
|
-
// const {session_id, switch_room, breakout_room, assignment_type} = data;
|
|
1835
|
-
// console.log('supriya-event-sync new data: ', data);
|
|
1836
|
-
// console.log('supriya-event-sync old data: ', stateRef.current);
|
|
1837
|
-
|
|
1838
|
-
// logger.log(
|
|
1839
|
-
// LogSource.Internals,
|
|
1840
|
-
// 'BREAKOUT_ROOM',
|
|
1841
|
-
// 'Sync state event received',
|
|
1842
|
-
// {
|
|
1843
|
-
// sessionId: session_id,
|
|
1844
|
-
// incomingRoomCount: breakout_room?.length || 0,
|
|
1845
|
-
// currentRoomCount: stateRef.current.breakoutGroups.length,
|
|
1846
|
-
// switchRoom: switch_room,
|
|
1847
|
-
// assignmentType: assignment_type,
|
|
1848
|
-
// },
|
|
1849
|
-
// );
|
|
1850
|
-
|
|
1851
|
-
// if (isAnotherHostOperating) {
|
|
1852
|
-
// setIsAnotherHostOperating(false);
|
|
1853
|
-
// setCurrentOperatingHostName(undefined);
|
|
1854
|
-
// }
|
|
1855
|
-
// // 🛡️ BEFORE snapshot - using stateRef to avoid stale closure
|
|
1856
|
-
// const prevGroups = stateRef.current.breakoutGroups;
|
|
1857
|
-
// console.log('supriya-event-sync prevGroups: ', prevGroups);
|
|
1858
|
-
// const prevSwitchRoom = stateRef.current.canUserSwitchRoom;
|
|
1859
|
-
|
|
1860
|
-
// // Helpers to find membership
|
|
1861
|
-
// const findUserRoomId = (uid: UidType, groups: BreakoutGroup[] = []) =>
|
|
1862
|
-
// groups.find(g => {
|
|
1863
|
-
// const hosts = Array.isArray(g?.participants?.hosts)
|
|
1864
|
-
// ? g.participants.hosts
|
|
1865
|
-
// : [];
|
|
1866
|
-
// const attendees = Array.isArray(g?.participants?.attendees)
|
|
1867
|
-
// ? g.participants.attendees
|
|
1868
|
-
// : [];
|
|
1869
|
-
// return hosts.includes(uid) || attendees.includes(uid);
|
|
1870
|
-
// })?.id ?? null;
|
|
1871
|
-
|
|
1872
|
-
// const prevRoomId = findUserRoomId(localUid, prevGroups);
|
|
1873
|
-
// const nextRoomId = findUserRoomId(localUid, breakout_room);
|
|
1874
|
-
|
|
1875
|
-
// console.log(
|
|
1876
|
-
// 'supriya-event-sync prevRoomId and nextRoomId: ',
|
|
1877
|
-
// prevRoomId,
|
|
1878
|
-
// nextRoomId,
|
|
1879
|
-
// );
|
|
1880
|
-
|
|
1881
|
-
// console.log('supriya-event-sync 1: ');
|
|
1882
|
-
// // Show notifications based on changes
|
|
1883
|
-
// // 1. Switch room enabled notification
|
|
1884
|
-
// const senderName = getDisplayName(srcuid);
|
|
1885
|
-
// if (switch_room && !prevSwitchRoom) {
|
|
1886
|
-
// console.log('supriya-toast 1');
|
|
1887
|
-
// showDeduplicatedToast('switch-room-toggle', {
|
|
1888
|
-
// leadingIconName: 'open-room',
|
|
1889
|
-
// type: 'info',
|
|
1890
|
-
// text1: `Host:${senderName} has opened breakout rooms.`,
|
|
1891
|
-
// text2: 'Please choose a room to join.',
|
|
1892
|
-
// visibilityTime: 3000,
|
|
1893
|
-
// });
|
|
1894
|
-
// }
|
|
1895
|
-
// console.log('supriya-event-sync 2: ');
|
|
1896
|
-
|
|
1897
|
-
// // 2. User joined a room (compare previous and current state)
|
|
1898
|
-
// // The notification for this comes from the main room channel_join event
|
|
1899
|
-
// if (prevRoomId === nextRoomId) {
|
|
1900
|
-
// // No logic
|
|
1901
|
-
// }
|
|
1902
|
-
|
|
1903
|
-
// console.log('supriya-event-sync 3: ');
|
|
1904
|
-
|
|
1905
|
-
// // 3. User was moved to main room
|
|
1906
|
-
// if (prevRoomId && !nextRoomId) {
|
|
1907
|
-
// const prevRoom = prevGroups.find(r => r.id === prevRoomId);
|
|
1908
|
-
// // Distinguish "room closed" vs "moved to main"
|
|
1909
|
-
// const roomStillExists = breakout_room.some(r => r.id === prevRoomId);
|
|
1910
|
-
|
|
1911
|
-
// if (!roomStillExists) {
|
|
1912
|
-
// showDeduplicatedToast(`current-room-closed-${prevRoomId}`, {
|
|
1913
|
-
// leadingIconName: 'close-room',
|
|
1914
|
-
// type: 'error',
|
|
1915
|
-
// text1: `Host: ${senderName} has closed "${
|
|
1916
|
-
// prevRoom?.name || ''
|
|
1917
|
-
// }" room. `,
|
|
1918
|
-
// text2: 'Returning to main room...',
|
|
1919
|
-
// visibilityTime: 3000,
|
|
1920
|
-
// });
|
|
1921
|
-
// } else {
|
|
1922
|
-
// showDeduplicatedToast(`moved-to-main-${prevRoomId}`, {
|
|
1923
|
-
// leadingIconName: 'arrow-up',
|
|
1924
|
-
// type: 'info',
|
|
1925
|
-
// text1: `Host: ${senderName} has moved you to main room.`,
|
|
1926
|
-
// visibilityTime: 3000,
|
|
1927
|
-
// });
|
|
1928
|
-
// }
|
|
1929
|
-
// // Exit breakout room and return to main room
|
|
1930
|
-
// return exitRoom(true);
|
|
1931
|
-
// }
|
|
1932
|
-
|
|
1933
|
-
// console.log('supriya-event-sync 5: ');
|
|
1934
|
-
|
|
1935
|
-
// // 5. All breakout rooms closed
|
|
1936
|
-
// if (breakout_room.length === 0 && prevGroups.length > 0) {
|
|
1937
|
-
// console.log('supriya-toast 5', prevRoomId, nextRoomId);
|
|
1938
|
-
|
|
1939
|
-
// // Show different messages based on user's current location
|
|
1940
|
-
// if (prevRoomId) {
|
|
1941
|
-
// // User was in a breakout room - returning to main
|
|
1942
|
-
// showDeduplicatedToast('all-rooms-closed', {
|
|
1943
|
-
// leadingIconName: 'close-room',
|
|
1944
|
-
// type: 'info',
|
|
1945
|
-
// text1: `Host: ${senderName} has closed all breakout rooms.`,
|
|
1946
|
-
// text2: 'Returning to the main room...',
|
|
1947
|
-
// visibilityTime: 3000,
|
|
1948
|
-
// });
|
|
1949
|
-
// return exitRoom(true);
|
|
1950
|
-
// } else {
|
|
1951
|
-
// // User was already in main room - just notify about closure
|
|
1952
|
-
// showDeduplicatedToast('all-rooms-closed', {
|
|
1953
|
-
// leadingIconName: 'close-room',
|
|
1954
|
-
// type: 'info',
|
|
1955
|
-
// text1: `Host: ${senderName} has closed all breakout rooms`,
|
|
1956
|
-
// visibilityTime: 4000,
|
|
1957
|
-
// });
|
|
1958
|
-
// }
|
|
1959
|
-
// }
|
|
1960
|
-
|
|
1961
|
-
// console.log('supriya-event-sync 6: ');
|
|
1962
|
-
|
|
1963
|
-
// // 6) Room renamed (compare per-room names)
|
|
1964
|
-
// prevGroups.forEach(prevRoom => {
|
|
1965
|
-
// const after = breakout_room.find(r => r.id === prevRoom.id);
|
|
1966
|
-
// if (after && after.name !== prevRoom.name) {
|
|
1967
|
-
// showDeduplicatedToast(`room-renamed-${after.id}`, {
|
|
1968
|
-
// type: 'info',
|
|
1969
|
-
// text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`,
|
|
1970
|
-
// visibilityTime: 3000,
|
|
1971
|
-
// });
|
|
1972
|
-
// }
|
|
1973
|
-
// });
|
|
1974
|
-
|
|
1975
|
-
// console.log('supriya-event-sync 7: ');
|
|
1976
|
-
|
|
1977
|
-
// // The host clicked on the room to close in which he is a part of
|
|
1978
|
-
// if (!prevRoomId && !nextRoomId) {
|
|
1979
|
-
// return exitRoom(true);
|
|
1980
|
-
// }
|
|
1981
|
-
// // Finally, apply the authoritative state
|
|
1982
|
-
// dispatch({
|
|
1983
|
-
// type: BreakoutGroupActionTypes.SYNC_STATE,
|
|
1984
|
-
// payload: {
|
|
1985
|
-
// sessionId: session_id,
|
|
1986
|
-
// assignmentStrategy: assignment_type,
|
|
1987
|
-
// switchRoom: switch_room,
|
|
1988
|
-
// rooms: breakout_room,
|
|
1989
|
-
// },
|
|
1990
|
-
// });
|
|
1991
|
-
// // Update the last processed timestamp after successful processing
|
|
1992
|
-
// lastProcessedTimestampRef.current = timestamp || Date.now();
|
|
1993
|
-
// },
|
|
1994
|
-
// [
|
|
1995
|
-
// dispatch,
|
|
1996
|
-
// exitRoom,
|
|
1997
|
-
// localUid,
|
|
1998
|
-
// showDeduplicatedToast,
|
|
1999
|
-
// isAnotherHostOperating,
|
|
2000
|
-
// getDisplayName,
|
|
2001
|
-
// ],
|
|
2002
|
-
// );
|
|
2003
|
-
|
|
2004
|
-
// Multi-host coordination handlers
|
|
2005
|
-
const handleHostOperationStart = useCallback(
|
|
2006
|
-
(operationName: string, hostUid: UidType, hostName: string) => {
|
|
2007
|
-
// Only process if current user is also a host and it's not their own event
|
|
2008
|
-
console.log('supriya-state-sync host operation started', operationName);
|
|
2009
|
-
// if (!isHostRef.current || hostUid === localUid) {
|
|
2010
|
-
if (hostUid === localUid) {
|
|
2011
|
-
return;
|
|
2012
|
-
}
|
|
2013
|
-
|
|
2014
|
-
logger.log(
|
|
2015
|
-
LogSource.Internals,
|
|
2016
|
-
'BREAKOUT_ROOM',
|
|
2017
|
-
'Another host started operation - locking UI',
|
|
2018
|
-
{operationName, hostUid, hostName},
|
|
2019
|
-
);
|
|
2020
|
-
|
|
2021
|
-
setCurrentOperatingHostName(hostName);
|
|
2022
|
-
},
|
|
2023
|
-
[localUid],
|
|
2024
|
-
);
|
|
2025
|
-
|
|
2026
|
-
const handleHostOperationEnd = useCallback(
|
|
2027
|
-
(operationName: string, hostUid: UidType, hostName: string) => {
|
|
2028
|
-
// Only process if current user is also a host and it's not their own event
|
|
2029
|
-
console.log('supriya-state-sync host operation ended', operationName);
|
|
2030
|
-
|
|
2031
|
-
// if (!isHostRef.current || hostUid === localUid) {
|
|
2032
|
-
if (hostUid === localUid) {
|
|
2033
|
-
return;
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
setCurrentOperatingHostName(undefined);
|
|
2037
|
-
},
|
|
2038
|
-
[localUid],
|
|
2039
|
-
);
|
|
2040
|
-
|
|
2041
|
-
// Debounced API for performance with multi-host coordination
|
|
2042
|
-
const debouncedUpsertAPI = useDebouncedCallback(
|
|
2043
|
-
async (type: 'START' | 'UPDATE', operationName?: string) => {
|
|
2044
|
-
setBreakoutUpdateInFlight(true);
|
|
2045
|
-
|
|
2046
|
-
try {
|
|
2047
|
-
console.log(
|
|
2048
|
-
'supriya-state-sync before calling upsertBreakoutRoomAPI 2007',
|
|
2049
|
-
);
|
|
2050
|
-
|
|
2051
|
-
await upsertBreakoutRoomAPI(type);
|
|
2052
|
-
console.log(
|
|
2053
|
-
'supriya-state-sync after calling upsertBreakoutRoomAPI 2007',
|
|
2054
|
-
);
|
|
2055
|
-
console.log('supriya-state-sync operationName', operationName);
|
|
2056
|
-
|
|
2057
|
-
// Broadcast operation end after successful API call
|
|
2058
|
-
if (operationName) {
|
|
2059
|
-
console.log(
|
|
2060
|
-
'supriya-state-sync broadcasting host operation end',
|
|
2061
|
-
operationName,
|
|
2062
|
-
);
|
|
2063
|
-
|
|
2064
|
-
broadcastHostOperationEnd(operationName);
|
|
2065
|
-
}
|
|
2066
|
-
} catch (error) {
|
|
2067
|
-
logger.log(
|
|
2068
|
-
LogSource.Internals,
|
|
2069
|
-
'BREAKOUT_ROOM',
|
|
2070
|
-
'API call failed. Reverting to previous state.',
|
|
2071
|
-
error,
|
|
2072
|
-
);
|
|
2073
|
-
|
|
2074
|
-
// Broadcast operation end even on failure
|
|
2075
|
-
if (operationName) {
|
|
2076
|
-
broadcastHostOperationEnd(operationName);
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
// // 🔁 Rollback to last valid state
|
|
2080
|
-
// if (
|
|
2081
|
-
// prevStateRef.current &&
|
|
2082
|
-
// validateRollbackState(prevStateRef.current)
|
|
2083
|
-
// ) {
|
|
2084
|
-
// baseDispatch({
|
|
2085
|
-
// type: BreakoutGroupActionTypes.SYNC_STATE,
|
|
2086
|
-
// payload: {
|
|
2087
|
-
// sessionId: prevStateRef.current.breakoutSessionId,
|
|
2088
|
-
// assignmentStrategy: prevStateRef.current.assignmentStrategy,
|
|
2089
|
-
// switchRoom: prevStateRef.current.canUserSwitchRoom,
|
|
2090
|
-
// rooms: prevStateRef.current.breakoutGroups,
|
|
2091
|
-
// },
|
|
2092
|
-
// });
|
|
2093
|
-
// showDeduplicatedToast('breakout-api-failure', {
|
|
2094
|
-
// type: 'error',
|
|
2095
|
-
// text1: 'Sync failed. Reverted to previous state.',
|
|
2096
|
-
// });
|
|
2097
|
-
// } else {
|
|
2098
|
-
// showDeduplicatedToast('breakout-api-failure-no-rollback', {
|
|
2099
|
-
// type: 'error',
|
|
2100
|
-
// text1: 'Sync failed. Could not rollback safely.',
|
|
2101
|
-
// });
|
|
2102
|
-
// }
|
|
2103
|
-
} finally {
|
|
2104
|
-
setBreakoutUpdateInFlight(false);
|
|
2105
|
-
}
|
|
2106
|
-
},
|
|
2107
|
-
500,
|
|
2108
|
-
);
|
|
2109
|
-
|
|
2110
|
-
// Action-based API triggering
|
|
2111
|
-
useEffect(() => {
|
|
2112
|
-
if (!lastAction || !lastAction.type) {
|
|
2113
|
-
return;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
// Actions that should trigger API calls
|
|
2117
|
-
const API_TRIGGERING_ACTIONS = [
|
|
2118
|
-
BreakoutGroupActionTypes.CREATE_GROUP,
|
|
2119
|
-
BreakoutGroupActionTypes.RENAME_GROUP,
|
|
2120
|
-
BreakoutGroupActionTypes.CLOSE_GROUP,
|
|
2121
|
-
BreakoutGroupActionTypes.CLOSE_ALL_GROUPS,
|
|
2122
|
-
BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN,
|
|
2123
|
-
BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP,
|
|
2124
|
-
BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS,
|
|
2125
|
-
BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS,
|
|
2126
|
-
BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS,
|
|
2127
|
-
BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM,
|
|
2128
|
-
BreakoutGroupActionTypes.EXIT_GROUP,
|
|
2129
|
-
];
|
|
2130
|
-
|
|
2131
|
-
// Host can always trigger API calls for any action
|
|
2132
|
-
// Attendees can only trigger API when they self-join a room and switch_room is enabled
|
|
2133
|
-
const attendeeSelfJoinAllowed =
|
|
2134
|
-
stateRef.current.canUserSwitchRoom &&
|
|
2135
|
-
lastAction.type === BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP;
|
|
2136
|
-
|
|
2137
|
-
const shouldCallAPI =
|
|
2138
|
-
API_TRIGGERING_ACTIONS.includes(lastAction.type as any) &&
|
|
2139
|
-
(isHostRef.current || (!isHostRef.current && attendeeSelfJoinAllowed));
|
|
2140
|
-
|
|
2141
|
-
// Compute lastOperationName based on lastAction
|
|
2142
|
-
const lastOperationName = HOST_BROADCASTED_OPERATIONS.includes(
|
|
2143
|
-
lastAction?.type as any,
|
|
2144
|
-
)
|
|
2145
|
-
? lastAction?.type
|
|
2146
|
-
: undefined;
|
|
2147
|
-
|
|
2148
|
-
console.log(
|
|
2149
|
-
'supriya-state-sync shouldCallAPI',
|
|
2150
|
-
shouldCallAPI,
|
|
2151
|
-
lastAction.type,
|
|
2152
|
-
lastOperationName,
|
|
2153
|
-
);
|
|
2154
|
-
if (shouldCallAPI) {
|
|
2155
|
-
debouncedUpsertAPI('UPDATE', lastOperationName);
|
|
2156
|
-
}
|
|
2157
|
-
}, [lastAction]);
|
|
2158
|
-
|
|
2159
|
-
const _handleBreakoutRoomSyncState = useCallback(
|
|
2160
|
-
async (
|
|
2161
|
-
payload: BreakoutRoomSyncStateEventPayload['data'],
|
|
2162
|
-
timestamp: number,
|
|
2163
|
-
) => {
|
|
2164
|
-
console.log(
|
|
2165
|
-
'supriya-sync-ordering exact response',
|
|
2166
|
-
timestamp,
|
|
2167
|
-
JSON.stringify(payload),
|
|
2168
|
-
);
|
|
2169
|
-
const {srcuid, data} = payload;
|
|
2170
|
-
const {
|
|
2171
|
-
session_id,
|
|
2172
|
-
switch_room,
|
|
2173
|
-
breakout_room,
|
|
2174
|
-
assignment_type,
|
|
2175
|
-
sts = 0,
|
|
2176
|
-
} = data;
|
|
2177
|
-
console.log('supriya-sync-ordering Sync state event received', {
|
|
2178
|
-
sessionId: session_id,
|
|
2179
|
-
incomingRoom: breakout_room || [],
|
|
2180
|
-
currentRoom: stateRef.current.breakoutGroups || [],
|
|
2181
|
-
switchRoom: switch_room,
|
|
2182
|
-
assignmentType: assignment_type,
|
|
2183
|
-
});
|
|
2184
|
-
|
|
2185
|
-
// global server ordering
|
|
2186
|
-
if (sts <= lastProcessedServerTsRef.current) {
|
|
2187
|
-
console.log(
|
|
2188
|
-
`supriya-sync-ordering [BreakoutSync] Ignoring out-of-order state (sts=${sts}, last=${lastProcessedServerTsRef.current})`,
|
|
2189
|
-
);
|
|
2190
|
-
return;
|
|
2191
|
-
}
|
|
2192
|
-
lastProcessedServerTsRef.current = sts;
|
|
2193
|
-
|
|
2194
|
-
// Self-join race protection — ignore stale reverts right after joining
|
|
2195
|
-
if (
|
|
2196
|
-
lastSelfJoinRef.current &&
|
|
2197
|
-
Date.now() - lastSelfJoinRef.current.ts < 2000 && // 2s cooldown
|
|
2198
|
-
!findUserRoomId(localUid, breakout_room)
|
|
2199
|
-
) {
|
|
2200
|
-
console.log(
|
|
2201
|
-
'supriya-sync-ordering [SyncGuard] Ignoring stale sync conflicting with recent self-join to',
|
|
2202
|
-
lastSelfJoinRef.current.roomId,
|
|
2203
|
-
);
|
|
2204
|
-
return;
|
|
2205
|
-
}
|
|
2206
|
-
|
|
2207
|
-
// Local duplicate protection (client-side ordering) Skip events older than the last processed timestamp
|
|
2208
|
-
if (timestamp && timestamp <= lastSyncedTimestampRef.current) {
|
|
2209
|
-
console.log(
|
|
2210
|
-
'supriya-sync-ordering Skipping old breakout room sync event',
|
|
2211
|
-
{
|
|
2212
|
-
timestamp,
|
|
2213
|
-
lastProcessed: lastSyncedTimestampRef.current,
|
|
2214
|
-
},
|
|
2215
|
-
);
|
|
2216
|
-
return;
|
|
2217
|
-
}
|
|
2218
|
-
|
|
2219
|
-
// Snapshot before applying
|
|
2220
|
-
const prevSnapshot = lastSyncedSnapshotRef?.current;
|
|
2221
|
-
const prevGroups = prevSnapshot?.breakout_room || [];
|
|
2222
|
-
const prevSwitchRoom = prevSnapshot?.switch_room ?? true;
|
|
2223
|
-
const prevRoomId = findUserRoomId(localUid, prevGroups);
|
|
2224
|
-
const nextRoomId = findUserRoomId(localUid, breakout_room);
|
|
2225
|
-
|
|
2226
|
-
// 1. !prevRoomId && nextRoomId = Main → Breakout (joining)
|
|
2227
|
-
// 2. prevRoomId && nextRoomId && prevRoomId !== nextRoomId = Breakout A → Breakout B (switching)
|
|
2228
|
-
// 3. prevRoomId && !nextRoomId = Breakout → Main (leaving)
|
|
2229
|
-
// 4. !prevRoomId && !nextRoomId = Main → Main (no change)
|
|
2230
|
-
|
|
2231
|
-
const userMovedBetweenRooms =
|
|
2232
|
-
prevRoomId && nextRoomId && prevRoomId !== nextRoomId;
|
|
2233
|
-
const userLeftBreakoutRoom = prevRoomId && !nextRoomId;
|
|
2234
|
-
|
|
2235
|
-
console.log(
|
|
2236
|
-
'supriya-sync-ordering prevRoomId nextRoomId and new breakout_room',
|
|
2237
|
-
prevRoomId,
|
|
2238
|
-
nextRoomId,
|
|
2239
|
-
breakout_room,
|
|
2240
|
-
);
|
|
2241
|
-
|
|
2242
|
-
const senderName = getDisplayName(srcuid);
|
|
2243
|
-
console.log('supriya-senderName: ', senderName, srcuid);
|
|
2244
|
-
|
|
2245
|
-
// ---- SCREEN SHARE CLEANUP ----
|
|
2246
|
-
// Stop screen share if user is moving between rooms or leaving breakout
|
|
2247
|
-
// if (
|
|
2248
|
-
// (userMovedBetweenRooms || userLeftBreakoutRoom) &&
|
|
2249
|
-
// isScreenshareActive
|
|
2250
|
-
// ) {
|
|
2251
|
-
// console.log(
|
|
2252
|
-
// 'supriya-sync-ordering: stopping screenshare due to room change',
|
|
2253
|
-
// );
|
|
2254
|
-
// stopScreenshare();
|
|
2255
|
-
// }
|
|
2256
|
-
|
|
2257
|
-
// ---- PRIORITY ORDER ----
|
|
2258
|
-
// 1. Room closed
|
|
2259
|
-
if (breakout_room.length === 0 && prevGroups.length > 0) {
|
|
2260
|
-
console.log('supriya-sync-ordering 1. all room closed: ');
|
|
2261
|
-
// 1. User is in breakout toom and the exits
|
|
2262
|
-
if (prevRoomId && isBreakoutMode) {
|
|
2263
|
-
// Don't show toast if the user is the author
|
|
2264
|
-
if (srcuid !== localUid) {
|
|
2265
|
-
showDeduplicatedToast('all-rooms-closed', {
|
|
2266
|
-
leadingIconName: 'close-room',
|
|
2267
|
-
type: 'info',
|
|
2268
|
-
text1: `Host: ${senderName} has closed all breakout rooms.`,
|
|
2269
|
-
text2: 'Returning to the main room...',
|
|
2270
|
-
visibilityTime: 3000,
|
|
2271
|
-
});
|
|
2272
|
-
}
|
|
2273
|
-
// Set transition flag - user will remount in main room and need fresh data
|
|
2274
|
-
sessionStorage.setItem('breakout_room_transition', 'true');
|
|
2275
|
-
lastSyncedSnapshotRef.current = null;
|
|
2276
|
-
return exitRoom(true);
|
|
2277
|
-
} else {
|
|
2278
|
-
// 2. User is in main room recevies just notification
|
|
2279
|
-
// Don't show toast if the user is the author
|
|
2280
|
-
if (srcuid !== localUid) {
|
|
2281
|
-
showDeduplicatedToast('all-rooms-closed', {
|
|
2282
|
-
leadingIconName: 'close-room',
|
|
2283
|
-
type: 'info',
|
|
2284
|
-
text1: `Host: ${senderName} has closed all breakout rooms`,
|
|
2285
|
-
visibilityTime: 4000,
|
|
2286
|
-
});
|
|
2287
|
-
}
|
|
2288
|
-
}
|
|
2289
|
-
}
|
|
2290
|
-
|
|
2291
|
-
// 2. User's room deleted (they were in a room → now not)
|
|
2292
|
-
if (userLeftBreakoutRoom && isBreakoutMode) {
|
|
2293
|
-
console.log('supriya-sync-ordering 2. they were in a room → now not: ');
|
|
2294
|
-
|
|
2295
|
-
const prevRoom = prevGroups.find(r => r.id === prevRoomId);
|
|
2296
|
-
const roomStillExists = breakout_room.some(r => r.id === prevRoomId);
|
|
2297
|
-
// Case A: Room deleted
|
|
2298
|
-
if (!roomStillExists) {
|
|
2299
|
-
// Don't show toast if the user is the author
|
|
2300
|
-
if (srcuid !== localUid) {
|
|
2301
|
-
showDeduplicatedToast(`current-room-closed-${prevRoomId}`, {
|
|
2302
|
-
leadingIconName: 'close-room',
|
|
2303
|
-
type: 'error',
|
|
2304
|
-
text1: `Host: ${senderName} has closed "${
|
|
2305
|
-
prevRoom?.name || ''
|
|
2306
|
-
}" room.`,
|
|
2307
|
-
text2: 'Returning to main room...',
|
|
2308
|
-
visibilityTime: 3000,
|
|
2309
|
-
});
|
|
2310
|
-
}
|
|
2311
|
-
} else {
|
|
2312
|
-
// Host removed user from room (handled here)
|
|
2313
|
-
// (Room still exists for others, but you were unassigned)
|
|
2314
|
-
// Don't show toast if the user is the author
|
|
2315
|
-
if (srcuid !== localUid) {
|
|
2316
|
-
showDeduplicatedToast(`moved-to-main-${prevRoomId}`, {
|
|
2317
|
-
leadingIconName: 'arrow-up',
|
|
2318
|
-
type: 'info',
|
|
2319
|
-
text1: `Host: ${senderName} has moved you to main room.`,
|
|
2320
|
-
visibilityTime: 3000,
|
|
2321
|
-
});
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
// Set transition flag - user will remount in main room and need fresh data
|
|
2326
|
-
sessionStorage.setItem('breakout_room_transition', 'true');
|
|
2327
|
-
lastSyncedSnapshotRef.current = null;
|
|
2328
|
-
return exitRoom(true);
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
// 3. User moved between breakout rooms
|
|
2332
|
-
if (userMovedBetweenRooms) {
|
|
2333
|
-
console.log(
|
|
2334
|
-
'supriya-sync-ordering 3. user moved between breakout rooms',
|
|
2335
|
-
);
|
|
2336
|
-
// const prevRoom = prevGroups.find(r => r.id === prevRoomId);
|
|
2337
|
-
// const nextRoom = breakout_room.find(r => r.id === nextRoomId);
|
|
2338
|
-
|
|
2339
|
-
// showDeduplicatedToast(`user-moved-${prevRoomId}-${nextRoomId}`, {
|
|
2340
|
-
// leadingIconName: 'arrow-right',
|
|
2341
|
-
// type: 'info',
|
|
2342
|
-
// text1: `Host: ${senderName} has moved you to "${
|
|
2343
|
-
// nextRoom?.name || nextRoomId
|
|
2344
|
-
// }".`,
|
|
2345
|
-
// text2: `From "${prevRoom?.name || prevRoomId}"`,
|
|
2346
|
-
// visibilityTime: 3000,
|
|
2347
|
-
// });
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
// 4. Rooms control switched
|
|
2351
|
-
if (switch_room && !prevSwitchRoom) {
|
|
2352
|
-
console.log('supriya-sync-ordering 4. switch_room changed: ');
|
|
2353
|
-
// Don't show toast if the user is the author
|
|
2354
|
-
if (srcuid !== localUid) {
|
|
2355
|
-
showDeduplicatedToast('switch-room-toggle', {
|
|
2356
|
-
leadingIconName: 'open-room',
|
|
2357
|
-
type: 'info',
|
|
2358
|
-
text1: `Host:${senderName} has opened breakout rooms.`,
|
|
2359
|
-
text2: 'Please choose a room to join.',
|
|
2360
|
-
visibilityTime: 3000,
|
|
2361
|
-
});
|
|
2362
|
-
}
|
|
2363
|
-
}
|
|
2364
|
-
|
|
2365
|
-
// 5. Group renamed
|
|
2366
|
-
prevGroups.forEach(prevRoom => {
|
|
2367
|
-
const after = breakout_room.find(r => r.id === prevRoom.id);
|
|
2368
|
-
if (after && after.name !== prevRoom.name) {
|
|
2369
|
-
console.log('supriya-sync-ordering 5. group renamed ');
|
|
2370
|
-
// Don't show toast if the user is the author
|
|
2371
|
-
if (srcuid !== localUid) {
|
|
2372
|
-
showDeduplicatedToast(`room-renamed-${after.id}`, {
|
|
2373
|
-
type: 'info',
|
|
2374
|
-
text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`,
|
|
2375
|
-
visibilityTime: 3000,
|
|
2376
|
-
});
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
|
-
});
|
|
2380
|
-
|
|
2381
|
-
dispatch({
|
|
2382
|
-
type: BreakoutGroupActionTypes.SYNC_STATE,
|
|
2383
|
-
payload: {
|
|
2384
|
-
sessionId: session_id,
|
|
2385
|
-
assignmentStrategy: assignment_type,
|
|
2386
|
-
switchRoom: switch_room,
|
|
2387
|
-
rooms: breakout_room,
|
|
2388
|
-
},
|
|
2389
|
-
});
|
|
2390
|
-
|
|
2391
|
-
// Store the snap of this
|
|
2392
|
-
lastSyncedSnapshotRef.current = payload.data;
|
|
2393
|
-
lastSyncedTimestampRef.current = timestamp || Date.now();
|
|
2394
|
-
},
|
|
2395
|
-
[dispatch, exitRoom, localUid, showDeduplicatedToast, getDisplayName],
|
|
2396
|
-
);
|
|
2397
|
-
|
|
2398
|
-
/**
|
|
2399
|
-
* While Event 1 is processing…
|
|
2400
|
-
* Event 2 arrives (ts=200) and Event 3 arrives (ts=300).
|
|
2401
|
-
* Both will overwrite latestTask:
|
|
2402
|
-
* Now, queue.latestTask only holds event 3, because event 2 was replaced before it could be picked up.
|
|
2403
|
-
*/
|
|
2404
|
-
|
|
2405
|
-
const enqueueBreakoutSyncEvent = useCallback(
|
|
2406
|
-
(payload: BreakoutRoomSyncStateEventPayload['data'], timestamp: number) => {
|
|
2407
|
-
const queue = breakoutSyncQueueRef.current;
|
|
2408
|
-
// Always keep the freshest event only
|
|
2409
|
-
console.log('supriya-sync-queue 1', queue);
|
|
2410
|
-
if (
|
|
2411
|
-
!queue.latestTask ||
|
|
2412
|
-
(timestamp && timestamp > queue.latestTask.timestamp)
|
|
2413
|
-
) {
|
|
2414
|
-
console.log('supriya-sync-queue 2', queue);
|
|
2415
|
-
queue.latestTask = {payload, timestamp};
|
|
2416
|
-
}
|
|
2417
|
-
console.log('supriya-sync-queue 3', queue.latestTask);
|
|
2418
|
-
|
|
2419
|
-
processBreakoutSyncQueue();
|
|
2420
|
-
},
|
|
2421
|
-
[],
|
|
2422
|
-
);
|
|
2423
|
-
|
|
2424
|
-
const processBreakoutSyncQueue = useCallback(async () => {
|
|
2425
|
-
const queue = breakoutSyncQueueRef.current;
|
|
2426
|
-
console.log('supriya-sync-queue 4', queue.latestTask);
|
|
2427
|
-
|
|
2428
|
-
// 1. If the queue is already being processed by another call, exit immediately.
|
|
2429
|
-
if (queue.isProcessing) {
|
|
2430
|
-
console.log('supriya-sync-queue 5 returning ');
|
|
2431
|
-
|
|
2432
|
-
return;
|
|
2433
|
-
}
|
|
2434
|
-
|
|
2435
|
-
try {
|
|
2436
|
-
// 2. "lock" the queue, so no second process can start.
|
|
2437
|
-
queue.isProcessing = true;
|
|
2438
|
-
console.log('supriya-sync-queue 6 lcoked ');
|
|
2439
|
-
// 3. Loop the queue
|
|
2440
|
-
while (queue.latestTask) {
|
|
2441
|
-
const {payload, timestamp} = queue.latestTask;
|
|
2442
|
-
console.log('supriya-sync-queue 7 ', payload, timestamp);
|
|
2443
|
-
queue.latestTask = null;
|
|
2444
|
-
|
|
2445
|
-
try {
|
|
2446
|
-
await _handleBreakoutRoomSyncState(payload, timestamp);
|
|
2447
|
-
} catch (err) {
|
|
2448
|
-
console.error('[BreakoutSync] Error processing sync event', err);
|
|
2449
|
-
// Continue processing other events even if one fails
|
|
2450
|
-
}
|
|
2451
|
-
}
|
|
2452
|
-
} catch (err) {
|
|
2453
|
-
console.error('[BreakoutSync] Critical error in queue processing', err);
|
|
2454
|
-
} finally {
|
|
2455
|
-
// Always unlock the queue, even if there's an error
|
|
2456
|
-
queue.isProcessing = false;
|
|
2457
|
-
}
|
|
2458
|
-
}, []);
|
|
2459
|
-
|
|
2460
|
-
return (
|
|
2461
|
-
<BreakoutRoomContext.Provider
|
|
2462
|
-
value={{
|
|
2463
|
-
mainChannelId: mainChannel,
|
|
2464
|
-
isBreakoutUILocked,
|
|
2465
|
-
breakoutSessionId: state.breakoutSessionId,
|
|
2466
|
-
breakoutGroups: state.breakoutGroups,
|
|
2467
|
-
assignmentStrategy: state.assignmentStrategy,
|
|
2468
|
-
handleAssignParticipants,
|
|
2469
|
-
manualAssignments: state.manualAssignments,
|
|
2470
|
-
setManualAssignments,
|
|
2471
|
-
clearManualAssignments,
|
|
2472
|
-
canUserSwitchRoom: state.canUserSwitchRoom,
|
|
2473
|
-
toggleRoomSwitchingAllowed,
|
|
2474
|
-
unassignedParticipants: state.unassignedParticipants,
|
|
2475
|
-
createBreakoutRoomGroup,
|
|
2476
|
-
checkIfBreakoutRoomSessionExistsAPI,
|
|
2477
|
-
upsertBreakoutRoomAPI,
|
|
2478
|
-
isUserInRoom,
|
|
2479
|
-
joinRoom,
|
|
2480
|
-
exitRoom,
|
|
2481
|
-
closeRoom,
|
|
2482
|
-
closeAllRooms,
|
|
2483
|
-
updateRoomName,
|
|
2484
|
-
getAllRooms,
|
|
2485
|
-
getRoomMemberDropdownOptions,
|
|
2486
|
-
// onMakeMePresenter,
|
|
2487
|
-
// presenters,
|
|
2488
|
-
// clearAllPresenters,
|
|
2489
|
-
handleBreakoutRoomSyncState: enqueueBreakoutSyncEvent,
|
|
2490
|
-
// Multi-host coordination handlers
|
|
2491
|
-
handleHostOperationStart,
|
|
2492
|
-
handleHostOperationEnd,
|
|
2493
|
-
permissions,
|
|
2494
|
-
// Loading states
|
|
2495
|
-
isBreakoutUpdateInFlight,
|
|
2496
|
-
// Multi-host coordination
|
|
2497
|
-
currentOperatingHostName,
|
|
2498
|
-
// State version for forcing re-computation in dependent hooks
|
|
2499
|
-
breakoutRoomVersion,
|
|
2500
|
-
}}>
|
|
2501
|
-
{children}
|
|
2502
|
-
</BreakoutRoomContext.Provider>
|
|
2503
|
-
);
|
|
2504
|
-
};
|
|
2505
|
-
|
|
2506
|
-
const useBreakoutRoom = createHook(BreakoutRoomContext);
|
|
2507
|
-
|
|
2508
|
-
export {useBreakoutRoom, BreakoutRoomProvider};
|