agora-appbuilder-core 4.1.10-beta.1 → 4.1.11

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.
Files changed (132) hide show
  1. package/package.json +2 -2
  2. package/template/agora-rn-uikit/src/Utils/isBotUser.ts +1 -1
  3. package/template/android/app/build.gradle +0 -7
  4. package/template/bridge/rtc/webNg/RtcEngine.ts +2 -2
  5. package/template/bridge/rtm/web/Types.ts +0 -183
  6. package/template/bridge/rtm/web/index.ts +488 -450
  7. package/template/customization-api/typeDefinition.ts +0 -1
  8. package/template/defaultConfig.js +3 -4
  9. package/template/global.d.ts +0 -1
  10. package/template/ios/Podfile +0 -41
  11. package/template/package.json +5 -5
  12. package/template/src/AppRoutes.tsx +3 -3
  13. package/template/src/ai-agent/components/ControlButtons.tsx +1 -1
  14. package/template/src/assets/font-styles.css +1 -33
  15. package/template/src/assets/fonts/icomoon.ttf +0 -0
  16. package/template/src/assets/selection.json +1 -1
  17. package/template/src/atoms/ActionMenu.tsx +93 -13
  18. package/template/src/atoms/CustomIcon.tsx +1 -8
  19. package/template/src/atoms/DropDownMulti.tsx +80 -29
  20. package/template/src/atoms/Dropdown.tsx +0 -5
  21. package/template/src/atoms/Input.tsx +2 -1
  22. package/template/src/atoms/TertiaryButton.tsx +1 -1
  23. package/template/src/atoms/UserAvatar.tsx +1 -1
  24. package/template/src/components/ChatContext.ts +3 -5
  25. package/template/src/components/Controls.tsx +167 -208
  26. package/template/src/components/DeviceConfigure.tsx +1 -1
  27. package/template/src/components/EventsConfigure.tsx +168 -118
  28. package/template/src/components/Navbar.tsx +11 -14
  29. package/template/src/components/RTMConfigure.tsx +819 -32
  30. package/template/src/components/beauty-effect/useBeautyEffects.tsx +13 -50
  31. package/template/src/components/chat/chatConfigure.tsx +1 -7
  32. package/template/src/components/chat-messages/useChatMessages.tsx +11 -43
  33. package/template/src/components/controls/useControlPermissionMatrix.tsx +4 -32
  34. package/template/src/components/participants/AllHostParticipants.tsx +2 -10
  35. package/template/src/components/participants/Participant.tsx +1 -7
  36. package/template/src/components/participants/UserActionMenuOptions.tsx +2 -12
  37. package/template/src/components/precall/joinCallBtn.native.tsx +7 -2
  38. package/template/src/components/precall/joinCallBtn.tsx +7 -2
  39. package/template/src/components/precall/joinWaitingRoomBtn.native.tsx +16 -15
  40. package/template/src/components/precall/joinWaitingRoomBtn.tsx +31 -17
  41. package/template/src/components/precall/textInput.tsx +45 -22
  42. package/template/src/components/precall/usePreCall.tsx +7 -0
  43. package/template/src/components/recordings/RecordingsDateTable.tsx +2 -3
  44. package/template/src/components/room-info/useRoomInfo.tsx +5 -0
  45. package/template/src/components/useUserPreference.tsx +12 -39
  46. package/template/src/components/virtual-background/useVB.tsx +0 -18
  47. package/template/src/components/whiteboard/WhiteboardConfigure.tsx +0 -27
  48. package/template/src/language/default-labels/videoCallScreenLabels.ts +27 -11
  49. package/template/src/logger/AppBuilderLogger.tsx +3 -11
  50. package/template/src/pages/VideoCall.tsx +518 -171
  51. package/template/src/pages/video-call/ActionSheetContent.tsx +77 -77
  52. package/template/src/pages/video-call/SidePanelHeader.tsx +81 -53
  53. package/template/src/pages/video-call/VideoCallScreen.tsx +0 -18
  54. package/template/src/pages/video-call/VideoCallScreenWrapper.tsx +1 -0
  55. package/template/src/rtm/RTMEngine.ts +37 -262
  56. package/template/src/rtm/utils.ts +1 -68
  57. package/template/src/rtm-events/constants.ts +7 -40
  58. package/template/src/rtm-events-api/Events.ts +39 -158
  59. package/template/src/subComponents/ChatBubble.tsx +3 -3
  60. package/template/src/subComponents/ChatContainer.tsx +9 -19
  61. package/template/src/subComponents/LocalAudioMute.tsx +2 -2
  62. package/template/src/subComponents/LocalVideoMute.tsx +2 -2
  63. package/template/src/subComponents/SidePanelEnum.tsx +0 -1
  64. package/template/src/subComponents/caption/Caption.tsx +48 -7
  65. package/template/src/subComponents/caption/CaptionContainer.tsx +324 -51
  66. package/template/src/subComponents/caption/CaptionIcon.tsx +35 -34
  67. package/template/src/subComponents/caption/CaptionText.tsx +103 -2
  68. package/template/src/subComponents/caption/LanguageSelectorPopup.tsx +179 -69
  69. package/template/src/subComponents/caption/Transcript.tsx +46 -11
  70. package/template/src/subComponents/caption/TranscriptIcon.tsx +27 -35
  71. package/template/src/subComponents/caption/TranscriptText.tsx +78 -3
  72. package/template/src/subComponents/caption/proto/ptoto.js +38 -4
  73. package/template/src/subComponents/caption/proto/test.proto +34 -19
  74. package/template/src/subComponents/caption/useCaption.tsx +754 -11
  75. package/template/src/subComponents/caption/useSTTAPI.tsx +118 -205
  76. package/template/src/subComponents/caption/useStreamMessageUtils.native.ts +152 -33
  77. package/template/src/subComponents/caption/useStreamMessageUtils.ts +165 -34
  78. package/template/src/subComponents/caption/utils.ts +171 -3
  79. package/template/src/subComponents/chat/ChatSendButton.tsx +0 -1
  80. package/template/src/subComponents/screenshare/ScreenshareButton.tsx +0 -16
  81. package/template/src/subComponents/screenshare/ScreenshareConfigure.native.tsx +1 -1
  82. package/template/src/subComponents/waiting-rooms/WaitingRoomControls.tsx +4 -7
  83. package/template/src/utils/SdkEvents.ts +3 -0
  84. package/template/src/utils/useEndCall.ts +4 -4
  85. package/template/src/utils/useMuteToggleLocal.ts +10 -14
  86. package/template/src/utils/useSpeechToText.ts +31 -20
  87. package/template/bridge/rtm/web/index-legacy.ts +0 -540
  88. package/template/src/components/RTMConfigure-legacy.tsx +0 -848
  89. package/template/src/components/UserGlobalPreferenceProvider.tsx +0 -227
  90. package/template/src/components/breakout-room/BreakoutRoomPanel.tsx +0 -58
  91. package/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +0 -2508
  92. package/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx +0 -272
  93. package/template/src/components/breakout-room/events/constants.ts +0 -17
  94. package/template/src/components/breakout-room/hoc/BreakoutRoomNameRenderer.tsx +0 -68
  95. package/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts +0 -49
  96. package/template/src/components/breakout-room/state/reducer.ts +0 -522
  97. package/template/src/components/breakout-room/state/types.ts +0 -54
  98. package/template/src/components/breakout-room/ui/BreakoutMeetingTitle.tsx +0 -60
  99. package/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx +0 -136
  100. package/template/src/components/breakout-room/ui/BreakoutRoomAnnouncementModal.tsx +0 -135
  101. package/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +0 -588
  102. package/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx +0 -142
  103. package/template/src/components/breakout-room/ui/BreakoutRoomMemberActionMenu.tsx +0 -122
  104. package/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx +0 -124
  105. package/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx +0 -65
  106. package/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx +0 -227
  107. package/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx +0 -140
  108. package/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx +0 -52
  109. package/template/src/components/breakout-room/ui/BreakoutRoomView.tsx +0 -193
  110. package/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx +0 -79
  111. package/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx +0 -638
  112. package/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx +0 -57
  113. package/template/src/components/common/Dividers.tsx +0 -53
  114. package/template/src/components/controls/toolbar-items/ExitBreakoutRoomToolbarItem.tsx +0 -13
  115. package/template/src/components/raise-hand/RaiseHandButton.tsx +0 -50
  116. package/template/src/components/raise-hand/RaiseHandProvider.tsx +0 -308
  117. package/template/src/components/raise-hand/index.ts +0 -14
  118. package/template/src/components/room-info/useCurrentRoomInfo.tsx +0 -42
  119. package/template/src/components/room-info/useSetBreakoutRoomInfo.tsx +0 -64
  120. package/template/src/pages/video-call/BreakoutVideoCall.tsx +0 -213
  121. package/template/src/pages/video-call/VideoCallContent.tsx +0 -211
  122. package/template/src/pages/video-call/VideoCallStateWrapper.tsx +0 -495
  123. package/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx +0 -882
  124. package/template/src/rtm/RTMConfigureMainRoomProvider.tsx +0 -757
  125. package/template/src/rtm/RTMCoreProvider.tsx +0 -419
  126. package/template/src/rtm/RTMGlobalStateProvider.tsx +0 -706
  127. package/template/src/rtm/RTMStatusBanner.tsx +0 -99
  128. package/template/src/rtm/constants.ts +0 -12
  129. package/template/src/rtm/hooks/useMainRoomUserDisplayName.ts +0 -45
  130. package/template/src/rtm/rtm-presence-utils.ts +0 -344
  131. package/template/src/subComponents/chat/ChatAnnouncementView.tsx +0 -65
  132. 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};