From 4168dc1f545cf7b55f2f8451bbd35eba70adcb12 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 5 Aug 2025 10:09:09 +0530 Subject: [PATCH 01/56] Breakout room poc (#732) - Breakout room wip features --- template/bridge/rtc/webNg/RtcEngine.ts | 4 +- template/bridge/rtm/web/Types.ts | 183 +++ template/bridge/rtm/web/index-legacy.ts | 540 ++++++++ template/bridge/rtm/web/index.ts | 914 +++++++------- template/customization-api/typeDefinition.ts | 1 + template/defaultConfig.js | 1 + template/global.d.ts | 1 + template/package.json | 4 +- template/src/AppRoutes.tsx | 3 +- template/src/atoms/UserAvatar.tsx | 2 +- template/src/components/Controls.tsx | 19 + template/src/components/DeviceConfigure.tsx | 2 +- .../src/components/RTMConfigure-legacy.tsx | 848 +++++++++++++ template/src/components/RTMConfigure.tsx | 1086 ++++++++++------- .../breakout-room/BreakoutRoomPanel.tsx | 151 +++ .../context/BreakoutRoomContext.tsx | 278 +++++ .../context/BreakoutRoomEngineContext.tsx | 118 ++ .../context/BreakoutRoomStateContext.tsx | 39 + .../components/breakout-room/state/reducer.ts | 246 ++++ .../components/breakout-room/state/types.ts | 62 + .../ui/BreakoutRoomGroupSettings.tsx | 311 +++++ .../ui/BreakoutRoomParticipants.tsx | 85 ++ .../breakout-room/ui/BreakoutRoomSettings.tsx | 84 ++ .../SelectParticipantAssignmentStrategy.tsx | 71 ++ template/src/components/common/Dividers.tsx | 53 + .../controls/useControlPermissionMatrix.tsx | 4 +- .../participants/AllHostParticipants.tsx | 12 +- .../components/participants/Participant.tsx | 8 +- .../participants/UserActionMenuOptions.tsx | 35 +- .../default-labels/videoCallScreenLabels.ts | 7 + template/src/logger/AppBuilderLogger.tsx | 3 +- template/src/pages/BreakoutRoomVideoCall.tsx | 239 ++++ template/src/pages/VideoCall.tsx | 67 +- .../src/pages/VideoCallRoomOrchestrator.tsx | 202 +++ .../src/pages/video-call/SidePanelHeader.tsx | 17 + .../src/pages/video-call/VideoCallScreen.tsx | 18 + .../video-call/VideoCallScreenWrapper.tsx | 1 - .../pages/video-call/VideoCallStateSetup.tsx | 47 + template/src/rtm-events-api/Events.ts | 130 +- template/src/rtm-events/constants.ts | 6 + template/src/rtm/RTMEngine.ts | 163 ++- template/src/subComponents/SidePanelEnum.tsx | 1 + .../src/subComponents/caption/useCaption.tsx | 2 +- template/src/utils/useEndCall.ts | 2 +- 44 files changed, 5045 insertions(+), 1025 deletions(-) create mode 100644 template/bridge/rtm/web/index-legacy.ts create mode 100644 template/src/components/RTMConfigure-legacy.tsx create mode 100644 template/src/components/breakout-room/BreakoutRoomPanel.tsx create mode 100644 template/src/components/breakout-room/context/BreakoutRoomContext.tsx create mode 100644 template/src/components/breakout-room/context/BreakoutRoomEngineContext.tsx create mode 100644 template/src/components/breakout-room/context/BreakoutRoomStateContext.tsx create mode 100644 template/src/components/breakout-room/state/reducer.ts create mode 100644 template/src/components/breakout-room/state/types.ts create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx create mode 100644 template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx create mode 100644 template/src/components/common/Dividers.tsx create mode 100644 template/src/pages/BreakoutRoomVideoCall.tsx create mode 100644 template/src/pages/VideoCallRoomOrchestrator.tsx create mode 100644 template/src/pages/video-call/VideoCallStateSetup.tsx diff --git a/template/bridge/rtc/webNg/RtcEngine.ts b/template/bridge/rtc/webNg/RtcEngine.ts index 321a595dc..5f8fe58a9 100644 --- a/template/bridge/rtc/webNg/RtcEngine.ts +++ b/template/bridge/rtc/webNg/RtcEngine.ts @@ -1466,13 +1466,13 @@ export default class RtcEngine { this.client.setEncryptionConfig( mode, config.encryptionKey, - config.encryptionMode === 1? null:config.encryptionKdfSalt, + config.encryptionMode === 1 ? null : config.encryptionKdfSalt, true, // encryptDataStream ), this.screenClient.setEncryptionConfig( mode, config.encryptionKey, - config.encryptionMode === 1? null:config.encryptionKdfSalt, + config.encryptionMode === 1 ? null : config.encryptionKdfSalt, true, // encryptDataStream ), ]); diff --git a/template/bridge/rtm/web/Types.ts b/template/bridge/rtm/web/Types.ts index 8356dbf2f..786ed9b63 100644 --- a/template/bridge/rtm/web/Types.ts +++ b/template/bridge/rtm/web/Types.ts @@ -1,3 +1,5 @@ +import {ChannelType as WebChannelType} from 'agora-rtm-sdk'; + export interface AttributesMap { [key: string]: string; } @@ -11,3 +13,184 @@ export interface ChannelAttributeOptions { */ enableNotificationToChannelMembers?: undefined | false | true; } + +// LINK STATE +export const nativeLinkStateMapping = { + IDLE: 0, + CONNECTING: 1, + CONNECTED: 2, + DISCONNECTED: 3, + SUSPENDED: 4, + FAILED: 5, +}; + +// Create reverse mapping: number -> string +export const webLinkStateMapping = Object.fromEntries( + Object.entries(nativeLinkStateMapping).map(([key, value]) => [value, key]), +); + +export const linkStatusReasonCodeMapping: {[key: string]: number} = { + UNKNOWN: 0, + LOGIN: 1, + LOGIN_SUCCESS: 2, + LOGIN_TIMEOUT: 3, + LOGIN_NOT_AUTHORIZED: 4, + LOGIN_REJECTED: 5, + RELOGIN: 6, + LOGOUT: 7, + AUTO_RECONNECT: 8, + RECONNECT_TIMEOUT: 9, + RECONNECT_SUCCESS: 10, + JOIN: 11, + JOIN_SUCCESS: 12, + JOIN_FAILED: 13, + REJOIN: 14, + LEAVE: 15, + INVALID_TOKEN: 16, + TOKEN_EXPIRED: 17, + INCONSISTENT_APP_ID: 18, + INVALID_CHANNEL_NAME: 19, + INVALID_USER_ID: 20, + NOT_INITIALIZED: 21, + RTM_SERVICE_NOT_CONNECTED: 22, + CHANNEL_INSTANCE_EXCEED_LIMITATION: 23, + OPERATION_RATE_EXCEED_LIMITATION: 24, + CHANNEL_IN_ERROR_STATE: 25, + PRESENCE_NOT_CONNECTED: 26, + SAME_UID_LOGIN: 27, + KICKED_OUT_BY_SERVER: 28, + KEEP_ALIVE_TIMEOUT: 29, + CONNECTION_ERROR: 30, + PRESENCE_NOT_READY: 31, + NETWORK_CHANGE: 32, + SERVICE_NOT_SUPPORTED: 33, + STREAM_CHANNEL_NOT_AVAILABLE: 34, + STORAGE_NOT_AVAILABLE: 35, + LOCK_NOT_AVAILABLE: 36, + LOGIN_TOO_FREQUENT: 37, +}; + +// CHANNEL TYPE +// string -> number +export const nativeChannelTypeMapping = { + NONE: 0, + MESSAGE: 1, + STREAM: 2, + USER: 3, +}; +// number -> string +export const webChannelTypeMapping = Object.fromEntries( + Object.entries(nativeChannelTypeMapping).map(([key, value]) => [value, key]), +); + +// STORAGE TYPE +// string -> number +export const nativeStorageTypeMapping = { + NONE: 0, + /** + * 1: The user storage event. + */ + USER: 1, + /** + * 2: The channel storage event. + */ + CHANNEL: 2, +}; +// number -> string +export const webStorageTypeMapping = Object.fromEntries( + Object.entries(nativeStorageTypeMapping).map(([key, value]) => [value, key]), +); + +// STORAGE EVENT TYPE +export const nativeStorageEventTypeMapping = { + /** + * 0: Unknown event type. + */ + NONE: 0, + /** + * 1: Triggered when user subscribe user metadata state or join channel with options.withMetadata = true + */ + SNAPSHOT: 1, + /** + * 2: Triggered when a remote user set metadata + */ + SET: 2, + /** + * 3: Triggered when a remote user update metadata + */ + UPDATE: 3, + /** + * 4: Triggered when a remote user remove metadata + */ + REMOVE: 4, +}; +// number -> string +export const webStorageEventTypeMapping = Object.fromEntries( + Object.entries(nativeStorageEventTypeMapping).map(([key, value]) => [ + value, + key, + ]), +); + +// PRESENCE EVENT TYPE +export const nativePresenceEventTypeMapping = { + /** + * 0: Unknown event type + */ + NONE: 0, + /** + * 1: The presence snapshot of this channel + */ + SNAPSHOT: 1, + /** + * 2: The presence event triggered in interval mode + */ + INTERVAL: 2, + /** + * 3: Triggered when remote user join channel + */ + REMOTE_JOIN: 3, + /** + * 4: Triggered when remote user leave channel + */ + REMOTE_LEAVE: 4, + /** + * 5: Triggered when remote user's connection timeout + */ + REMOTE_TIMEOUT: 5, + /** + * 6: Triggered when user changed state + */ + REMOTE_STATE_CHANGED: 6, + /** + * 7: Triggered when user joined channel without presence service + */ + ERROR_OUT_OF_SERVICE: 7, +}; +// number -> string +export const webPresenceEventTypeMapping = Object.fromEntries( + Object.entries(nativePresenceEventTypeMapping).map(([key, value]) => [ + value, + key, + ]), +); + +// MESSAGE EVENT TYPE +// string -> number +export const nativeMessageEventTypeMapping = { + /** + * 0: The binary message. + */ + BINARY: 0, + /** + * 1: The ascii message. + */ + STRING: 1, +}; +// number -> string +export const webMessageEventTypeMapping = Object.fromEntries( + Object.entries(nativePresenceEventTypeMapping).map(([key, value]) => [ + value, + key, + ]), +); diff --git a/template/bridge/rtm/web/index-legacy.ts b/template/bridge/rtm/web/index-legacy.ts new file mode 100644 index 000000000..ed092d0c6 --- /dev/null +++ b/template/bridge/rtm/web/index-legacy.ts @@ -0,0 +1,540 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ +// @ts-nocheck +import { + ChannelAttributeOptions, + RtmAttribute, + RtmChannelAttribute, + Subscription, +} from 'agora-react-native-rtm/lib/typescript/src'; +import {RtmClientEvents} from 'agora-react-native-rtm/lib/typescript/src/RtmEngine'; +import AgoraRTM, {VERSION} from 'agora-rtm-sdk'; +import RtmClient from 'agora-react-native-rtm'; +import {LogSource, logger} from '../../../src/logger/AppBuilderLogger'; +// export {RtmAttribute} +// +interface RtmAttributePlaceholder {} +export {RtmAttributePlaceholder as RtmAttribute}; + +type callbackType = (args?: any) => void; + +export default class RtmEngine { + public appId: string; + public client: RtmClient; + public channelMap = new Map([]); + public remoteInvititations = new Map([]); + public localInvititations = new Map([]); + public channelEventsMap = new Map([ + ['channelMessageReceived', () => null], + ['channelMemberJoined', () => null], + ['channelMemberLeft', () => null], + ]); + public clientEventsMap = new Map([ + ['connectionStateChanged', () => null], + ['messageReceived', () => null], + ['remoteInvitationReceived', () => null], + ['tokenExpired', () => null], + ]); + public localInvitationEventsMap = new Map([ + ['localInvitationAccepted', () => null], + ['localInvitationCanceled', () => null], + ['localInvitationFailure', () => null], + ['localInvitationReceivedByPeer', () => null], + ['localInvitationRefused', () => null], + ]); + public remoteInvitationEventsMap = new Map([ + ['remoteInvitationAccepted', () => null], + ['remoteInvitationCanceled', () => null], + ['remoteInvitationFailure', () => null], + ['remoteInvitationRefused', () => null], + ]); + constructor() { + this.appId = ''; + logger.debug(LogSource.AgoraSDK, 'Log', 'Using RTM Bridge'); + } + + on(event: any, listener: any) { + if ( + event === 'channelMessageReceived' || + event === 'channelMemberJoined' || + event === 'channelMemberLeft' + ) { + this.channelEventsMap.set(event, listener); + } else if ( + event === 'connectionStateChanged' || + event === 'messageReceived' || + event === 'remoteInvitationReceived' || + event === 'tokenExpired' + ) { + this.clientEventsMap.set(event, listener); + } else if ( + event === 'localInvitationAccepted' || + event === 'localInvitationCanceled' || + event === 'localInvitationFailure' || + event === 'localInvitationReceivedByPeer' || + event === 'localInvitationRefused' + ) { + this.localInvitationEventsMap.set(event, listener); + } else if ( + event === 'remoteInvitationAccepted' || + event === 'remoteInvitationCanceled' || + event === 'remoteInvitationFailure' || + event === 'remoteInvitationRefused' + ) { + this.remoteInvitationEventsMap.set(event, listener); + } + } + + createClient(APP_ID: string) { + this.appId = APP_ID; + this.client = AgoraRTM.createInstance(this.appId); + + if ($config.GEO_FENCING) { + try { + //include area is comma seperated value + let includeArea = $config.GEO_FENCING_INCLUDE_AREA + ? $config.GEO_FENCING_INCLUDE_AREA + : AREAS.GLOBAL; + + //exclude area is single value + let excludeArea = $config.GEO_FENCING_EXCLUDE_AREA + ? $config.GEO_FENCING_EXCLUDE_AREA + : ''; + + includeArea = includeArea?.split(','); + + //pass excludedArea if only its provided + if (excludeArea) { + AgoraRTM.setArea({ + areaCodes: includeArea, + excludedArea: excludeArea, + }); + } + //otherwise we can pass area directly + else { + AgoraRTM.setArea({areaCodes: includeArea}); + } + } catch (setAeraError) { + console.log('error on RTM setArea', setAeraError); + } + } + + window.rtmClient = this.client; + + this.client.on('ConnectionStateChanged', (state, reason) => { + this.clientEventsMap.get('connectionStateChanged')({state, reason}); + }); + + this.client.on('MessageFromPeer', (msg, uid, msgProps) => { + this.clientEventsMap.get('messageReceived')({ + text: msg.text, + ts: msgProps.serverReceivedTs, + offline: msgProps.isOfflineMessage, + peerId: uid, + }); + }); + + this.client.on('RemoteInvitationReceived', (remoteInvitation: any) => { + this.remoteInvititations.set(remoteInvitation.callerId, remoteInvitation); + this.clientEventsMap.get('remoteInvitationReceived')({ + callerId: remoteInvitation.callerId, + content: remoteInvitation.content, + state: remoteInvitation.state, + channelId: remoteInvitation.channelId, + response: remoteInvitation.response, + }); + + remoteInvitation.on('RemoteInvitationAccepted', () => { + this.remoteInvitationEventsMap.get('RemoteInvitationAccepted')({ + callerId: remoteInvitation.callerId, + content: remoteInvitation.content, + state: remoteInvitation.state, + channelId: remoteInvitation.channelId, + response: remoteInvitation.response, + }); + }); + + remoteInvitation.on('RemoteInvitationCanceled', (content: string) => { + this.remoteInvitationEventsMap.get('remoteInvitationCanceled')({ + callerId: remoteInvitation.callerId, + content: content, + state: remoteInvitation.state, + channelId: remoteInvitation.channelId, + response: remoteInvitation.response, + }); + }); + + remoteInvitation.on('RemoteInvitationFailure', (reason: string) => { + this.remoteInvitationEventsMap.get('remoteInvitationFailure')({ + callerId: remoteInvitation.callerId, + content: remoteInvitation.content, + state: remoteInvitation.state, + channelId: remoteInvitation.channelId, + response: remoteInvitation.response, + code: -1, //Web sends string, RN expect number but can't find enum + }); + }); + + remoteInvitation.on('RemoteInvitationRefused', () => { + this.remoteInvitationEventsMap.get('remoteInvitationRefused')({ + callerId: remoteInvitation.callerId, + content: remoteInvitation.content, + state: remoteInvitation.state, + channelId: remoteInvitation.channelId, + response: remoteInvitation.response, + }); + }); + }); + + this.client.on('TokenExpired', () => { + this.clientEventsMap.get('tokenExpired')({}); //RN expect evt: any + }); + } + + async login(loginParam: {uid: string; token?: string}): Promise { + return this.client.login(loginParam); + } + + async logout(): Promise { + return await this.client.logout(); + } + + async joinChannel(channelId: string): Promise { + this.channelMap.set(channelId, this.client.createChannel(channelId)); + this.channelMap + .get(channelId) + .on('ChannelMessage', (msg: {text: string}, uid: string, messagePros) => { + let text = msg.text; + let ts = messagePros.serverReceivedTs; + this.channelEventsMap.get('channelMessageReceived')({ + uid, + channelId, + text, + ts, + }); + }); + this.channelMap.get(channelId).on('MemberJoined', (uid: string) => { + this.channelEventsMap.get('channelMemberJoined')({uid, channelId}); + }); + this.channelMap.get(channelId).on('MemberLeft', (uid: string) => { + console.log('Member Left', this.channelEventsMap); + this.channelEventsMap.get('channelMemberLeft')({uid}); + }); + this.channelMap + .get(channelId) + .on('AttributesUpdated', (attributes: RtmChannelAttribute) => { + /** + * a) Kindly note the below event listener 'channelAttributesUpdated' expects type + * RtmChannelAttribute[] (array of objects [{key: 'valueOfKey', value: 'valueOfValue}]) + * whereas the above listener 'AttributesUpdated' receives attributes in object form + * {[valueOfKey]: valueOfValue} of type RtmChannelAttribute + * b) Hence in this bridge the data should be modified to keep in sync with both the + * listeners for web and listener for native + */ + /** + * 1. Loop through object + * 2. Create a object {key: "", value: ""} and push into array + * 3. Return the Array + */ + const channelAttributes = Object.keys(attributes).reduce((acc, key) => { + const {value, lastUpdateTs, lastUpdateUserId} = attributes[key]; + acc.push({key, value, lastUpdateTs, lastUpdateUserId}); + return acc; + }, []); + + this.channelEventsMap.get('ChannelAttributesUpdated')( + channelAttributes, + ); + }); + + return this.channelMap.get(channelId).join(); + } + + async leaveChannel(channelId: string): Promise { + if (this.channelMap.get(channelId)) { + return this.channelMap.get(channelId).leave(); + } else { + Promise.reject('Wrong channel'); + } + } + + async sendMessageByChannelId(channel: string, message: string): Promise { + if (this.channelMap.get(channel)) { + return this.channelMap.get(channel).sendMessage({text: message}); + } else { + console.log(this.channelMap, channel); + Promise.reject('Wrong channel'); + } + } + + destroyClient() { + console.log('Destroy called'); + this.channelEventsMap.forEach((callback, event) => { + this.client.off(event, callback); + }); + this.channelEventsMap.clear(); + this.channelMap.clear(); + this.clientEventsMap.clear(); + this.remoteInvitationEventsMap.clear(); + this.localInvitationEventsMap.clear(); + } + + async getChannelMembersBychannelId(channel: string) { + if (this.channelMap.get(channel)) { + let memberArray: Array = []; + let currentChannel = this.channelMap.get(channel); + await currentChannel.getMembers().then((arr: Array) => { + arr.map((elem: number) => { + memberArray.push({ + channelId: channel, + uid: elem, + }); + }); + }); + return {members: memberArray}; + } else { + Promise.reject('Wrong channel'); + } + } + + async queryPeersOnlineStatus(uid: Array) { + let peerArray: Array = []; + await this.client.queryPeersOnlineStatus(uid).then(list => { + Object.entries(list).forEach(value => { + peerArray.push({ + online: value[1], + uid: value[0], + }); + }); + }); + return {items: peerArray}; + } + + async renewToken(token: string) { + return this.client.renewToken(token); + } + + async getUserAttributesByUid(uid: string) { + let response = {}; + await this.client + .getUserAttributes(uid) + .then((attributes: string) => { + response = {attributes, uid}; + }) + .catch((e: any) => { + Promise.reject(e); + }); + return response; + } + + async getChannelAttributes(channelId: string) { + let response = {}; + await this.client + .getChannelAttributes(channelId) + .then((attributes: RtmChannelAttribute) => { + /** + * Here the attributes received are in the format {[valueOfKey]: valueOfValue} of type RtmChannelAttribute + * We need to convert it into (array of objects [{key: 'valueOfKey', value: 'valueOfValue}]) + /** + * 1. Loop through object + * 2. Create a object {key: "", value: ""} and push into array + * 3. Return the Array + */ + const channelAttributes = Object.keys(attributes).reduce((acc, key) => { + const {value, lastUpdateTs, lastUpdateUserId} = attributes[key]; + acc.push({key, value, lastUpdateTs, lastUpdateUserId}); + return acc; + }, []); + response = channelAttributes; + }) + .catch((e: any) => { + Promise.reject(e); + }); + return response; + } + + async removeAllLocalUserAttributes() { + return this.client.clearLocalUserAttributes(); + } + + async removeLocalUserAttributesByKeys(keys: string[]) { + return this.client.deleteLocalUserAttributesByKeys(keys); + } + + async replaceLocalUserAttributes(attributes: string[]) { + let formattedAttributes: any = {}; + attributes.map(attribute => { + let key = Object.values(attribute)[0]; + let value = Object.values(attribute)[1]; + formattedAttributes[key] = value; + }); + return this.client.setLocalUserAttributes({...formattedAttributes}); + } + + async setLocalUserAttributes(attributes: string[]) { + let formattedAttributes: any = {}; + attributes.map(attribute => { + let key = Object.values(attribute)[0]; + let value = Object.values(attribute)[1]; + formattedAttributes[key] = value; + // console.log('!!!!formattedAttributes', formattedAttributes, key, value); + }); + return this.client.setLocalUserAttributes({...formattedAttributes}); + } + + async addOrUpdateLocalUserAttributes(attributes: RtmAttribute[]) { + let formattedAttributes: any = {}; + attributes.map(attribute => { + let key = Object.values(attribute)[0]; + let value = Object.values(attribute)[1]; + formattedAttributes[key] = value; + }); + return this.client.addOrUpdateLocalUserAttributes({...formattedAttributes}); + } + + async addOrUpdateChannelAttributes( + channelId: string, + attributes: RtmChannelAttribute[], + option: ChannelAttributeOptions, + ): Promise { + let formattedAttributes: any = {}; + attributes.map(attribute => { + let key = Object.values(attribute)[0]; + let value = Object.values(attribute)[1]; + formattedAttributes[key] = value; + }); + return this.client.addOrUpdateChannelAttributes( + channelId, + {...formattedAttributes}, + option, + ); + } + + async sendLocalInvitation(invitationProps: any) { + let invite = this.client.createLocalInvitation(invitationProps.uid); + this.localInvititations.set(invitationProps.uid, invite); + invite.content = invitationProps.content; + + invite.on('LocalInvitationAccepted', (response: string) => { + this.localInvitationEventsMap.get('localInvitationAccepted')({ + calleeId: invite.calleeId, + content: invite.content, + state: invite.state, + channelId: invite.channelId, + response, + }); + }); + + invite.on('LocalInvitationCanceled', () => { + this.localInvitationEventsMap.get('localInvitationCanceled')({ + calleeId: invite.calleeId, + content: invite.content, + state: invite.state, + channelId: invite.channelId, + response: invite.response, + }); + }); + + invite.on('LocalInvitationFailure', (reason: string) => { + this.localInvitationEventsMap.get('localInvitationFailure')({ + calleeId: invite.calleeId, + content: invite.content, + state: invite.state, + channelId: invite.channelId, + response: invite.response, + code: -1, //Web sends string, RN expect number but can't find enum + }); + }); + + invite.on('LocalInvitationReceivedByPeer', () => { + this.localInvitationEventsMap.get('localInvitationReceivedByPeer')({ + calleeId: invite.calleeId, + content: invite.content, + state: invite.state, + channelId: invite.channelId, + response: invite.response, + }); + }); + + invite.on('LocalInvitationRefused', (response: string) => { + this.localInvitationEventsMap.get('localInvitationRefused')({ + calleeId: invite.calleeId, + content: invite.content, + state: invite.state, + channelId: invite.channelId, + response: response, + }); + }); + return invite.send(); + } + + async sendMessageToPeer(AgoraPeerMessage: { + peerId: string; + offline: boolean; + text: string; + }) { + return this.client.sendMessageToPeer( + {text: AgoraPeerMessage.text}, + AgoraPeerMessage.peerId, + ); + //check promise result + } + + async acceptRemoteInvitation(remoteInvitationProps: { + uid: string; + response?: string; + channelId: string; + }) { + let invite = this.remoteInvititations.get(remoteInvitationProps.uid); + // console.log(invite); + // console.log(this.remoteInvititations); + // console.log(remoteInvitationProps.uid); + return invite.accept(); + } + + async refuseRemoteInvitation(remoteInvitationProps: { + uid: string; + response?: string; + channelId: string; + }) { + return this.remoteInvititations.get(remoteInvitationProps.uid).refuse(); + } + + async cancelLocalInvitation(LocalInvitationProps: { + uid: string; + content?: string; + channelId?: string; + }) { + console.log(this.localInvititations.get(LocalInvitationProps.uid)); + return this.localInvititations.get(LocalInvitationProps.uid).cancel(); + } + + getSdkVersion(callback: (version: string) => void) { + callback(VERSION); + } + + addListener( + event: EventType, + listener: RtmClientEvents[EventType], + ): Subscription { + if (event === 'ChannelAttributesUpdated') { + this.channelEventsMap.set(event, listener as callbackType); + } + return { + remove: () => { + console.log( + 'Use destroy method to remove all the event listeners from the RtcEngine instead.', + ); + }, + }; + } +} diff --git a/template/bridge/rtm/web/index.ts b/template/bridge/rtm/web/index.ts index ed092d0c6..63c7af031 100644 --- a/template/bridge/rtm/web/index.ts +++ b/template/bridge/rtm/web/index.ts @@ -1,540 +1,472 @@ -/* -******************************************** - Copyright © 2021 Agora Lab, Inc., all rights reserved. - AppBuilder and all associated components, source code, APIs, services, and documentation - (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be - accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. - Use without a license or in violation of any license terms and conditions (including use for - any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more - information visit https://appbuilder.agora.io. -********************************************* -*/ -// @ts-nocheck import { - ChannelAttributeOptions, - RtmAttribute, - RtmChannelAttribute, - Subscription, -} from 'agora-react-native-rtm/lib/typescript/src'; -import {RtmClientEvents} from 'agora-react-native-rtm/lib/typescript/src/RtmEngine'; -import AgoraRTM, {VERSION} from 'agora-rtm-sdk'; -import RtmClient from 'agora-react-native-rtm'; -import {LogSource, logger} from '../../../src/logger/AppBuilderLogger'; -// export {RtmAttribute} -// -interface RtmAttributePlaceholder {} -export {RtmAttributePlaceholder as RtmAttribute}; - -type callbackType = (args?: any) => void; - -export default class RtmEngine { - public appId: string; - public client: RtmClient; - public channelMap = new Map([]); - public remoteInvititations = new Map([]); - public localInvititations = new Map([]); - public channelEventsMap = new Map([ - ['channelMessageReceived', () => null], - ['channelMemberJoined', () => null], - ['channelMemberLeft', () => null], - ]); - public clientEventsMap = new Map([ - ['connectionStateChanged', () => null], - ['messageReceived', () => null], - ['remoteInvitationReceived', () => null], - ['tokenExpired', () => null], - ]); - public localInvitationEventsMap = new Map([ - ['localInvitationAccepted', () => null], - ['localInvitationCanceled', () => null], - ['localInvitationFailure', () => null], - ['localInvitationReceivedByPeer', () => null], - ['localInvitationRefused', () => null], - ]); - public remoteInvitationEventsMap = new Map([ - ['remoteInvitationAccepted', () => null], - ['remoteInvitationCanceled', () => null], - ['remoteInvitationFailure', () => null], - ['remoteInvitationRefused', () => null], + type Metadata as NativeMetadata, + type MetadataItem as NativeMetadataItem, + type GetUserMetadataOptions as NativeGetUserMetadataOptions, + type RtmChannelType as NativeRtmChannelType, + type SetUserMetadataResponse, + type LoginOptions as NativeLoginOptions, + type RTMClientEventMap as NativeRTMClientEventMap, + type GetUserMetadataResponse as NativeGetUserMetadataResponse, + type GetChannelMetadataResponse as NativeGetChannelMetadataResponse, + type SetOrUpdateUserMetadataOptions as NativeSetOrUpdateUserMetadataOptions, + type IMetadataOptions as NativeIMetadataOptions, + type StorageEvent as NativeStorageEvent, + type PresenceEvent as NativePresenceEvent, + type MessageEvent as NativeMessageEvent, + type SubscribeOptions as NativeSubscribeOptions, + type PublishOptions as NativePublishOptions, +} from 'agora-react-native-rtm'; +import AgoraRTM, { + RTMClient, + GetUserMetadataResponse, + GetChannelMetadataResponse, + PublishOptions, + ChannelType, + MetaDataDetail, +} from 'agora-rtm-sdk'; +import { + linkStatusReasonCodeMapping, + nativeChannelTypeMapping, + nativeLinkStateMapping, + nativeMessageEventTypeMapping, + nativePresenceEventTypeMapping, + nativeStorageEventTypeMapping, + nativeStorageTypeMapping, + webChannelTypeMapping, +} from './Types'; + +type CallbackType = (args?: any) => void; + +// Conversion function +const convertWebToNativeMetadata = (webMetadata: any): NativeMetadata => { + // Convert object entries to MetadataItem array + const items: NativeMetadataItem[] = + Object.entries(webMetadata.metadata).map( + ([key, metadataItem]: [string, MetaDataDetail]) => { + return { + key: key, + value: metadataItem.value, + revision: metadataItem.revision, + authorUserId: metadataItem.authorUid, + updateTs: metadataItem.updated, + }; + }, + ) || []; + + // Create native Metadata object + const nativeMetadata: NativeMetadata = { + majorRevision: webMetadata?.revision || -1, // Use first item's revision as major revision + items: items, + itemCount: webMetadata?.totalCount || 0, + }; + + return nativeMetadata; +}; + +export class RTMWebClient { + private client: RTMClient; + private appId: string; + private userId: string; + private eventsMap = new Map([ + ['linkState', () => null], + ['storage', () => null], + ['presence', () => null], + ['message', () => null], ]); - constructor() { - this.appId = ''; - logger.debug(LogSource.AgoraSDK, 'Log', 'Using RTM Bridge'); - } - - on(event: any, listener: any) { - if ( - event === 'channelMessageReceived' || - event === 'channelMemberJoined' || - event === 'channelMemberLeft' - ) { - this.channelEventsMap.set(event, listener); - } else if ( - event === 'connectionStateChanged' || - event === 'messageReceived' || - event === 'remoteInvitationReceived' || - event === 'tokenExpired' - ) { - this.clientEventsMap.set(event, listener); - } else if ( - event === 'localInvitationAccepted' || - event === 'localInvitationCanceled' || - event === 'localInvitationFailure' || - event === 'localInvitationReceivedByPeer' || - event === 'localInvitationRefused' - ) { - this.localInvitationEventsMap.set(event, listener); - } else if ( - event === 'remoteInvitationAccepted' || - event === 'remoteInvitationCanceled' || - event === 'remoteInvitationFailure' || - event === 'remoteInvitationRefused' - ) { - this.remoteInvitationEventsMap.set(event, listener); - } - } - createClient(APP_ID: string) { - this.appId = APP_ID; - this.client = AgoraRTM.createInstance(this.appId); - - if ($config.GEO_FENCING) { - try { - //include area is comma seperated value - let includeArea = $config.GEO_FENCING_INCLUDE_AREA - ? $config.GEO_FENCING_INCLUDE_AREA - : AREAS.GLOBAL; - - //exclude area is single value - let excludeArea = $config.GEO_FENCING_EXCLUDE_AREA - ? $config.GEO_FENCING_EXCLUDE_AREA - : ''; - - includeArea = includeArea?.split(','); - - //pass excludedArea if only its provided - if (excludeArea) { - AgoraRTM.setArea({ - areaCodes: includeArea, - excludedArea: excludeArea, - }); - } - //otherwise we can pass area directly - else { - AgoraRTM.setArea({areaCodes: includeArea}); - } - } catch (setAeraError) { - console.log('error on RTM setArea', setAeraError); - } - } - - window.rtmClient = this.client; - - this.client.on('ConnectionStateChanged', (state, reason) => { - this.clientEventsMap.get('connectionStateChanged')({state, reason}); - }); - - this.client.on('MessageFromPeer', (msg, uid, msgProps) => { - this.clientEventsMap.get('messageReceived')({ - text: msg.text, - ts: msgProps.serverReceivedTs, - offline: msgProps.isOfflineMessage, - peerId: uid, + constructor(appId: string, userId: string) { + this.appId = appId; + this.userId = `${userId}`; + try { + // Create the actual web RTM client + this.client = new AgoraRTM.RTM(this.appId, this.userId); + + this.client.addEventListener('linkState', data => { + const nativeState = { + ...data, + currentState: + nativeLinkStateMapping[data.currentState] || + nativeLinkStateMapping.IDLE, + previousState: + nativeLinkStateMapping[data.previousState] || + nativeLinkStateMapping.IDLE, + reasonCode: linkStatusReasonCodeMapping[data.reasonCode] || 0, + }; + (this.eventsMap.get('linkState') ?? (() => {}))(nativeState); }); - }); - this.client.on('RemoteInvitationReceived', (remoteInvitation: any) => { - this.remoteInvititations.set(remoteInvitation.callerId, remoteInvitation); - this.clientEventsMap.get('remoteInvitationReceived')({ - callerId: remoteInvitation.callerId, - content: remoteInvitation.content, - state: remoteInvitation.state, - channelId: remoteInvitation.channelId, - response: remoteInvitation.response, + this.client.addEventListener('storage', data => { + const nativeStorageEvent: NativeStorageEvent = { + channelType: nativeChannelTypeMapping[data.channelType], + storageType: nativeStorageTypeMapping[data.storageType], + eventType: nativeStorageEventTypeMapping[data.eventType], + data: convertWebToNativeMetadata(data.data), + timestamp: data.timestamp, + }; + (this.eventsMap.get('storage') ?? (() => {}))(nativeStorageEvent); }); - remoteInvitation.on('RemoteInvitationAccepted', () => { - this.remoteInvitationEventsMap.get('RemoteInvitationAccepted')({ - callerId: remoteInvitation.callerId, - content: remoteInvitation.content, - state: remoteInvitation.state, - channelId: remoteInvitation.channelId, - response: remoteInvitation.response, - }); + this.client.addEventListener('presence', data => { + const nativePresenceEvent: NativePresenceEvent = { + channelName: data.channelName, + channelType: nativeChannelTypeMapping[data.channelType], + type: nativePresenceEventTypeMapping[data.eventType], + publisher: data.publisher, + timestamp: data.timestamp, + }; + (this.eventsMap.get('presence') ?? (() => {}))(nativePresenceEvent); }); - remoteInvitation.on('RemoteInvitationCanceled', (content: string) => { - this.remoteInvitationEventsMap.get('remoteInvitationCanceled')({ - callerId: remoteInvitation.callerId, - content: content, - state: remoteInvitation.state, - channelId: remoteInvitation.channelId, - response: remoteInvitation.response, - }); + this.client.addEventListener('message', data => { + const nativeMessageEvent: NativeMessageEvent = { + ...data, + channelType: nativeChannelTypeMapping[data.channelType], + messageType: nativeMessageEventTypeMapping[data.messageType], + message: `${data.message}`, + }; + (this.eventsMap.get('message') ?? (() => {}))(nativeMessageEvent); }); + } catch (error) { + const contextError = new Error( + `Failed to create RTMWebClient for appId: ${this.appId}, userId: ${ + this.userId + }. Error: ${error.message || error}`, + ); + console.error('RTMWebClient constructor error:', contextError); + throw contextError; + } + } - remoteInvitation.on('RemoteInvitationFailure', (reason: string) => { - this.remoteInvitationEventsMap.get('remoteInvitationFailure')({ - callerId: remoteInvitation.callerId, - content: remoteInvitation.content, - state: remoteInvitation.state, - channelId: remoteInvitation.channelId, - response: remoteInvitation.response, - code: -1, //Web sends string, RN expect number but can't find enum + // Storage methods + get storage() { + return { + setUserMetadata: ( + data: NativeMetadata, + options?: NativeSetOrUpdateUserMetadataOptions, + ): Promise => { + // 1. Validate input parameters + if (!data) { + throw new Error('setUserMetadata: data parameter is required'); + } + if (!data.items || !Array.isArray(data.items)) { + throw new Error( + 'setUserMetadata: data.items must be a non-empty array', + ); + } + if (data.items.length === 0) { + throw new Error('setUserMetadata: data.items cannot be empty'); + } + // 2. Make sure key is present as this is mandatory + // https://docs.agora.io/en/signaling/reference/api?platform=web#storagesetuserpropsag_platform + const validatedItems = data.items.map((item, index) => { + if (!item.key || typeof item.key !== 'string') { + throw new Error( + `setUserMetadata: item at index ${index} missing required 'key' property`, + ); + } + return { + key: item.key, + value: item.value || '', // Default to empty string if not provided + revision: item.revision || -1, // Default to -1 if not provided + }; }); - }); - - remoteInvitation.on('RemoteInvitationRefused', () => { - this.remoteInvitationEventsMap.get('remoteInvitationRefused')({ - callerId: remoteInvitation.callerId, - content: remoteInvitation.content, - state: remoteInvitation.state, - channelId: remoteInvitation.channelId, - response: remoteInvitation.response, + // Map native signature to web signature + return this.client.storage.setUserMetadata(validatedItems, { + addTimeStamp: options?.addTimeStamp || true, + addUserId: options?.addUserId || true, }); - }); - }); - - this.client.on('TokenExpired', () => { - this.clientEventsMap.get('tokenExpired')({}); //RN expect evt: any - }); - } - - async login(loginParam: {uid: string; token?: string}): Promise { - return this.client.login(loginParam); - } - - async logout(): Promise { - return await this.client.logout(); - } + }, - async joinChannel(channelId: string): Promise { - this.channelMap.set(channelId, this.client.createChannel(channelId)); - this.channelMap - .get(channelId) - .on('ChannelMessage', (msg: {text: string}, uid: string, messagePros) => { - let text = msg.text; - let ts = messagePros.serverReceivedTs; - this.channelEventsMap.get('channelMessageReceived')({ - uid, - channelId, - text, - ts, - }); - }); - this.channelMap.get(channelId).on('MemberJoined', (uid: string) => { - this.channelEventsMap.get('channelMemberJoined')({uid, channelId}); - }); - this.channelMap.get(channelId).on('MemberLeft', (uid: string) => { - console.log('Member Left', this.channelEventsMap); - this.channelEventsMap.get('channelMemberLeft')({uid}); - }); - this.channelMap - .get(channelId) - .on('AttributesUpdated', (attributes: RtmChannelAttribute) => { - /** - * a) Kindly note the below event listener 'channelAttributesUpdated' expects type - * RtmChannelAttribute[] (array of objects [{key: 'valueOfKey', value: 'valueOfValue}]) - * whereas the above listener 'AttributesUpdated' receives attributes in object form - * {[valueOfKey]: valueOfValue} of type RtmChannelAttribute - * b) Hence in this bridge the data should be modified to keep in sync with both the - * listeners for web and listener for native - */ + getUserMetadata: async (options: NativeGetUserMetadataOptions) => { + // Validate input parameters + if (!options) { + throw new Error('getUserMetadata: options parameter is required'); + } + if ( + !options.userId || + typeof options.userId !== 'string' || + options.userId.trim() === '' + ) { + throw new Error( + 'getUserMetadata: options.userId must be a non-empty string', + ); + } + const webResponse: GetUserMetadataResponse = + await this.client.storage.getUserMetadata({ + userId: options.userId, + }); /** - * 1. Loop through object - * 2. Create a object {key: "", value: ""} and push into array - * 3. Return the Array + * majorRevision : 13483783553 + * metadata : + * { + * isHost: {authorUid: "", revision: 13483783553, updated: 0, value : "true"}, + * screenUid: {…}} + * } + * timestamp: 0 + * totalCount: 2 + * userId: "xxx" */ - const channelAttributes = Object.keys(attributes).reduce((acc, key) => { - const {value, lastUpdateTs, lastUpdateUserId} = attributes[key]; - acc.push({key, value, lastUpdateTs, lastUpdateUserId}); - return acc; - }, []); - - this.channelEventsMap.get('ChannelAttributesUpdated')( - channelAttributes, + const items = Object.entries(webResponse.metadata).map( + ([key, metadataItem]) => ({ + key: key, + value: metadataItem.value, + }), ); - }); - - return this.channelMap.get(channelId).join(); - } - - async leaveChannel(channelId: string): Promise { - if (this.channelMap.get(channelId)) { - return this.channelMap.get(channelId).leave(); - } else { - Promise.reject('Wrong channel'); - } - } - - async sendMessageByChannelId(channel: string, message: string): Promise { - if (this.channelMap.get(channel)) { - return this.channelMap.get(channel).sendMessage({text: message}); - } else { - console.log(this.channelMap, channel); - Promise.reject('Wrong channel'); - } - } - - destroyClient() { - console.log('Destroy called'); - this.channelEventsMap.forEach((callback, event) => { - this.client.off(event, callback); - }); - this.channelEventsMap.clear(); - this.channelMap.clear(); - this.clientEventsMap.clear(); - this.remoteInvitationEventsMap.clear(); - this.localInvitationEventsMap.clear(); - } + const nativeResponse: NativeGetUserMetadataResponse = { + items: [...items], + itemCount: webResponse.totalCount, + userId: webResponse.userId, + timestamp: webResponse.timestamp, + }; + return nativeResponse; + }, - async getChannelMembersBychannelId(channel: string) { - if (this.channelMap.get(channel)) { - let memberArray: Array = []; - let currentChannel = this.channelMap.get(channel); - await currentChannel.getMembers().then((arr: Array) => { - arr.map((elem: number) => { - memberArray.push({ - channelId: channel, - uid: elem, - }); + setChannelMetadata: async ( + channelName: string, + channelType: NativeRtmChannelType, + data: NativeMetadata, + options?: NativeIMetadataOptions, + ) => { + // Validate input parameters + if ( + !channelName || + typeof channelName !== 'string' || + channelName.trim() === '' + ) { + throw new Error( + 'setChannelMetadata: channelName must be a non-empty string', + ); + } + if (typeof channelType !== 'number') { + throw new Error('setChannelMetadata: channelType must be a number'); + } + if (!data) { + throw new Error('setChannelMetadata: data parameter is required'); + } + if (!data.items || !Array.isArray(data.items)) { + throw new Error('setChannelMetadata: data.items must be an array'); + } + if (data.items.length === 0) { + throw new Error('setChannelMetadata: data.items cannot be empty'); + } + // 2. Make sure key is present as this is mandatory + // https://docs.agora.io/en/signaling/reference/api?platform=web#storagesetuserpropsag_platform + const validatedItems = data.items.map((item, index) => { + if (!item.key || typeof item.key !== 'string') { + throw new Error( + `setChannelMetadata: item at index ${index} missing required 'key' property`, + ); + } + return { + key: item.key, + value: item.value || '', // Default to empty string if not provided + revision: item.revision || -1, // Default to -1 if not provided + }; }); - }); - return {members: memberArray}; - } else { - Promise.reject('Wrong channel'); - } - } + return this.client.storage.setChannelMetadata( + channelName, + (webChannelTypeMapping[channelType] as ChannelType) || 'MESSAGE', + validatedItems, + { + addUserId: options?.addUserId || true, + addTimeStamp: options?.addTimeStamp || true, + }, + ); + }, - async queryPeersOnlineStatus(uid: Array) { - let peerArray: Array = []; - await this.client.queryPeersOnlineStatus(uid).then(list => { - Object.entries(list).forEach(value => { - peerArray.push({ - online: value[1], - uid: value[0], - }); - }); - }); - return {items: peerArray}; + getChannelMetadata: async ( + channelName: string, + channelType: NativeRtmChannelType, + ) => { + try { + const webResponse: GetChannelMetadataResponse = + await this.client.storage.getChannelMetadata( + channelName, + (webChannelTypeMapping[channelType] as ChannelType) || 'MESSAGE', + ); + + const items = Object.entries(webResponse.metadata).map( + ([key, metadataItem]) => ({ + key: key, + value: metadataItem.value, + }), + ); + const nativeResponse: NativeGetChannelMetadataResponse = { + items: [...items], + itemCount: webResponse.totalCount, + timestamp: webResponse.timestamp, + channelName: webResponse.channelName, + channelType: nativeChannelTypeMapping.MESSAGE, + }; + return nativeResponse; + } catch (error) { + const contextError = new Error( + `Failed to get channel metadata for channel '${channelName}' with type ${channelType}: ${ + error.message || error + }`, + ); + console.error('BRIDGE getChannelMetadata error:', contextError); + throw contextError; + } + }, + }; } - async renewToken(token: string) { - return this.client.renewToken(token); - } + get presence() { + return { + getOnlineUsers: async ( + channelName: string, + channelType: NativeRtmChannelType, + ) => { + // Validate input parameters + if ( + !channelName || + typeof channelName !== 'string' || + channelName.trim() === '' + ) { + throw new Error( + 'getOnlineUsers: channelName must be a non-empty string', + ); + } + if (typeof channelType !== 'number') { + throw new Error('getOnlineUsers: channelType must be a number'); + } - async getUserAttributesByUid(uid: string) { - let response = {}; - await this.client - .getUserAttributes(uid) - .then((attributes: string) => { - response = {attributes, uid}; - }) - .catch((e: any) => { - Promise.reject(e); - }); - return response; - } + try { + // Call web SDK's presence method + const result = await this.client.presence.getOnlineUsers( + channelName, + (webChannelTypeMapping[channelType] as ChannelType) || 'MESSAGE', + ); + return result; + } catch (error) { + const contextError = new Error( + `Failed to get online users for channel '${channelName}' with type ${channelType}: ${ + error.message || error + }`, + ); + console.error('BRIDGE presence error:', contextError); + throw contextError; + } + }, - async getChannelAttributes(channelId: string) { - let response = {}; - await this.client - .getChannelAttributes(channelId) - .then((attributes: RtmChannelAttribute) => { - /** - * Here the attributes received are in the format {[valueOfKey]: valueOfValue} of type RtmChannelAttribute - * We need to convert it into (array of objects [{key: 'valueOfKey', value: 'valueOfValue}]) - /** - * 1. Loop through object - * 2. Create a object {key: "", value: ""} and push into array - * 3. Return the Array - */ - const channelAttributes = Object.keys(attributes).reduce((acc, key) => { - const {value, lastUpdateTs, lastUpdateUserId} = attributes[key]; - acc.push({key, value, lastUpdateTs, lastUpdateUserId}); - return acc; - }, []); - response = channelAttributes; - }) - .catch((e: any) => { - Promise.reject(e); - }); - return response; - } + whoNow: async ( + channelName: string, + channelType?: NativeRtmChannelType, + ) => { + const webChannelType = channelType + ? (webChannelTypeMapping[channelType] as ChannelType) + : 'MESSAGE'; + return this.client.presence.whoNow(channelName, webChannelType); + }, - async removeAllLocalUserAttributes() { - return this.client.clearLocalUserAttributes(); + whereNow: async (userId: string) => { + return this.client.presence.whereNow(userId); + }, + }; } - async removeLocalUserAttributesByKeys(keys: string[]) { - return this.client.deleteLocalUserAttributesByKeys(keys); + addEventListener( + event: keyof NativeRTMClientEventMap, + listener: (event: any) => void, + ) { + if (this.client) { + // Simply replace the handler in our map - web client listeners are fixed in constructor + this.eventsMap.set(event, listener as CallbackType); + } } - async replaceLocalUserAttributes(attributes: string[]) { - let formattedAttributes: any = {}; - attributes.map(attribute => { - let key = Object.values(attribute)[0]; - let value = Object.values(attribute)[1]; - formattedAttributes[key] = value; - }); - return this.client.setLocalUserAttributes({...formattedAttributes}); + removeEventListener( + event: keyof NativeRTMClientEventMap, + _listener: (event: any) => void, + ) { + if (this.client && this.eventsMap.has(event)) { + const prevListener = this.eventsMap.get(event); + if (prevListener) { + this.client.removeEventListener(event, prevListener); + } + this.eventsMap.set(event, () => null); // reset to no-op + } } - async setLocalUserAttributes(attributes: string[]) { - let formattedAttributes: any = {}; - attributes.map(attribute => { - let key = Object.values(attribute)[0]; - let value = Object.values(attribute)[1]; - formattedAttributes[key] = value; - // console.log('!!!!formattedAttributes', formattedAttributes, key, value); - }); - return this.client.setLocalUserAttributes({...formattedAttributes}); + // Core RTM methods - direct delegation to web SDK + async login(options?: NativeLoginOptions) { + if (!options?.token) { + throw new Error('login: token is required in options'); + } + return this.client.login({token: options.token}); } - async addOrUpdateLocalUserAttributes(attributes: RtmAttribute[]) { - let formattedAttributes: any = {}; - attributes.map(attribute => { - let key = Object.values(attribute)[0]; - let value = Object.values(attribute)[1]; - formattedAttributes[key] = value; - }); - return this.client.addOrUpdateLocalUserAttributes({...formattedAttributes}); + async logout() { + return this.client.logout(); } - async addOrUpdateChannelAttributes( - channelId: string, - attributes: RtmChannelAttribute[], - option: ChannelAttributeOptions, - ): Promise { - let formattedAttributes: any = {}; - attributes.map(attribute => { - let key = Object.values(attribute)[0]; - let value = Object.values(attribute)[1]; - formattedAttributes[key] = value; - }); - return this.client.addOrUpdateChannelAttributes( - channelId, - {...formattedAttributes}, - option, - ); + async subscribe(channelName: string, options?: NativeSubscribeOptions) { + if ( + !channelName || + typeof channelName !== 'string' || + channelName.trim() === '' + ) { + throw new Error('subscribe: channelName must be a non-empty string'); + } + return this.client.subscribe(channelName, options); } - async sendLocalInvitation(invitationProps: any) { - let invite = this.client.createLocalInvitation(invitationProps.uid); - this.localInvititations.set(invitationProps.uid, invite); - invite.content = invitationProps.content; - - invite.on('LocalInvitationAccepted', (response: string) => { - this.localInvitationEventsMap.get('localInvitationAccepted')({ - calleeId: invite.calleeId, - content: invite.content, - state: invite.state, - channelId: invite.channelId, - response, - }); - }); - - invite.on('LocalInvitationCanceled', () => { - this.localInvitationEventsMap.get('localInvitationCanceled')({ - calleeId: invite.calleeId, - content: invite.content, - state: invite.state, - channelId: invite.channelId, - response: invite.response, - }); - }); - - invite.on('LocalInvitationFailure', (reason: string) => { - this.localInvitationEventsMap.get('localInvitationFailure')({ - calleeId: invite.calleeId, - content: invite.content, - state: invite.state, - channelId: invite.channelId, - response: invite.response, - code: -1, //Web sends string, RN expect number but can't find enum - }); - }); - - invite.on('LocalInvitationReceivedByPeer', () => { - this.localInvitationEventsMap.get('localInvitationReceivedByPeer')({ - calleeId: invite.calleeId, - content: invite.content, - state: invite.state, - channelId: invite.channelId, - response: invite.response, - }); - }); - - invite.on('LocalInvitationRefused', (response: string) => { - this.localInvitationEventsMap.get('localInvitationRefused')({ - calleeId: invite.calleeId, - content: invite.content, - state: invite.state, - channelId: invite.channelId, - response: response, - }); - }); - return invite.send(); + async unsubscribe(channelName: string) { + return this.client.unsubscribe(channelName); } - async sendMessageToPeer(AgoraPeerMessage: { - peerId: string; - offline: boolean; - text: string; - }) { - return this.client.sendMessageToPeer( - {text: AgoraPeerMessage.text}, - AgoraPeerMessage.peerId, - ); - //check promise result - } + async publish( + channelName: string, + message: string, + options?: NativePublishOptions, + ) { + // Validate input parameters + if ( + !channelName || + typeof channelName !== 'string' || + channelName.trim() === '' + ) { + throw new Error('publish: channelName must be a non-empty string'); + } + if (typeof message !== 'string') { + throw new Error('publish: message must be a string'); + } - async acceptRemoteInvitation(remoteInvitationProps: { - uid: string; - response?: string; - channelId: string; - }) { - let invite = this.remoteInvititations.get(remoteInvitationProps.uid); - // console.log(invite); - // console.log(this.remoteInvititations); - // console.log(remoteInvitationProps.uid); - return invite.accept(); + const webOptions: PublishOptions = { + ...options, + channelType: + (webChannelTypeMapping[options?.channelType] as ChannelType) || + 'MESSAGE', + }; + return this.client.publish(channelName, message, webOptions); } - async refuseRemoteInvitation(remoteInvitationProps: { - uid: string; - response?: string; - channelId: string; - }) { - return this.remoteInvititations.get(remoteInvitationProps.uid).refuse(); + async renewToken(token: string) { + return this.client.renewToken(token); } - async cancelLocalInvitation(LocalInvitationProps: { - uid: string; - content?: string; - channelId?: string; - }) { - console.log(this.localInvititations.get(LocalInvitationProps.uid)); - return this.localInvititations.get(LocalInvitationProps.uid).cancel(); + removeAllListeners() { + this.eventsMap = new Map([ + ['linkState', () => null], + ['storage', () => null], + ['presence', () => null], + ['message', () => null], + ]); + return this.client.removeAllListeners(); } +} - getSdkVersion(callback: (version: string) => void) { - callback(VERSION); - } +export class RtmConfig { + public appId: string; + public userId: string; - addListener( - event: EventType, - listener: RtmClientEvents[EventType], - ): Subscription { - if (event === 'ChannelAttributesUpdated') { - this.channelEventsMap.set(event, listener as callbackType); - } - return { - remove: () => { - console.log( - 'Use destroy method to remove all the event listeners from the RtcEngine instead.', - ); - }, - }; + constructor(config: {appId: string; userId: string}) { + this.appId = config.appId; + this.userId = config.userId; } } +// Factory function to create RTM client +export function createAgoraRtmClient(config: RtmConfig): RTMWebClient { + return new RTMWebClient(config.appId, config.userId); +} diff --git a/template/customization-api/typeDefinition.ts b/template/customization-api/typeDefinition.ts index 50745ca57..3750f3f43 100644 --- a/template/customization-api/typeDefinition.ts +++ b/template/customization-api/typeDefinition.ts @@ -86,6 +86,7 @@ export interface VideoCallInterface extends BeforeAndAfterInterface { captionPanel?: React.ComponentType; transcriptPanel?: React.ComponentType; virtualBackgroundPanel?: React.ComponentType; + breakoutRoomPanel?: React.ComponentType; customLayout?: (layouts: LayoutItem[]) => LayoutItem[]; wrapper?: React.ComponentType; customAgentInterface?: React.ComponentType; diff --git a/template/defaultConfig.js b/template/defaultConfig.js index 0098d55f2..67546b855 100644 --- a/template/defaultConfig.js +++ b/template/defaultConfig.js @@ -91,6 +91,7 @@ const DefaultConfig = { ENABLE_WAITING_ROOM_AUTO_APPROVAL: false, ENABLE_WAITING_ROOM_AUTO_REQUEST: false, ENABLE_TEXT_TRACKS: false, + ENABLE_BREAKOUT_ROOM: false, }; module.exports = DefaultConfig; diff --git a/template/global.d.ts b/template/global.d.ts index cf136cf05..4bc494e7c 100644 --- a/template/global.d.ts +++ b/template/global.d.ts @@ -178,6 +178,7 @@ interface ConfigInterface { ENABLE_WAITING_ROOM_AUTO_APPROVAL: boolean; ENABLE_WAITING_ROOM_AUTO_REQUEST: boolean; ENABLE_TEXT_TRACKS: boolean; + ENABLE_BREAKOUT_ROOM: boolean; } declare var $config: ConfigInterface; declare module 'customization' { diff --git a/template/package.json b/template/package.json index 445b26a24..2fc25b8f1 100644 --- a/template/package.json +++ b/template/package.json @@ -63,9 +63,9 @@ "agora-extension-ai-denoiser": "1.1.0", "agora-extension-beauty-effect": "^1.0.2-beta", "agora-extension-virtual-background": "^1.1.3", - "agora-react-native-rtm": "1.5.1", + "agora-react-native-rtm": "2.2.4", "agora-rtc-sdk-ng": "4.23.4", - "agora-rtm-sdk": "1.5.1", + "agora-rtm-sdk": "2.2.2", "buffer": "^6.0.3", "electron-log": "4.3.5", "electron-squirrel-startup": "1.0.0", diff --git a/template/src/AppRoutes.tsx b/template/src/AppRoutes.tsx index 18e3b9281..17f0cf2d9 100644 --- a/template/src/AppRoutes.tsx +++ b/template/src/AppRoutes.tsx @@ -25,6 +25,7 @@ import {useIsRecordingBot} from './subComponents/recording/useIsRecordingBot'; import {isValidReactComponent} from './utils/common'; import ErrorBoundary from './components/ErrorBoundary'; import {ErrorBoundaryFallback} from './components/ErrorBoundaryFallback'; +import VideoCallRoomOrchestrator from './pages/VideoCallRoomOrchestrator'; function VideoCallWrapper(props) { const {isRecordingBot} = useIsRecordingBot(); @@ -38,7 +39,7 @@ function VideoCallWrapper(props) { ) : ( - + ); diff --git a/template/src/atoms/UserAvatar.tsx b/template/src/atoms/UserAvatar.tsx index 57f850efe..4a226b95e 100644 --- a/template/src/atoms/UserAvatar.tsx +++ b/template/src/atoms/UserAvatar.tsx @@ -10,7 +10,7 @@ function getInitials(name: string) { return 'U'; } -const UserAvatar = ({name, containerStyle, textStyle}) => { +const UserAvatar = ({name, containerStyle = {}, textStyle = {}}) => { return ( { const virtualBackgroundLabel = useString(toolbarItemVirtualBackgroundText)(); const chatLabel = useString(toolbarItemChatText)(); const inviteLabel = useString(toolbarItemInviteText)(); + const breakoutRoomLabel = useString(toolbarItemBreakoutRoomText)(); const peopleLabel = useString(toolbarItemPeopleText)(); const layoutLabel = useString(toolbarItemLayoutText)(); const {dispatch} = useContext(DispatchContext); @@ -834,6 +836,23 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { }); } + // 14. Breakout Room + const canAccessBreakoutRoom = useControlPermissionMatrix('breakoutRoom'); + if (canAccessBreakoutRoom) { + actionMenuitems.push({ + componentName: 'breakoutRoom', + order: 14, + icon: 'participants', + iconColor: $config.SECONDARY_ACTION_COLOR, + textColor: $config.FONT_COLOR, + title: breakoutRoomLabel, + onPress: () => { + setActionMenuVisible(false); + setSidePanel(SidePanelType.BreakoutRoom); + }, + }); + } + useEffect(() => { if (isHovered) { setActionMenuVisible(true); diff --git a/template/src/components/DeviceConfigure.tsx b/template/src/components/DeviceConfigure.tsx index f6bfc3082..d3a9089a8 100644 --- a/template/src/components/DeviceConfigure.tsx +++ b/template/src/components/DeviceConfigure.tsx @@ -50,7 +50,7 @@ const log = (...args: any[]) => { type WebRtcEngineInstance = InstanceType; interface Props { - userRole: ClientRoleType; + userRole?: ClientRoleType; } export type deviceInfo = MediaDeviceInfo; export type deviceId = deviceInfo['deviceId']; diff --git a/template/src/components/RTMConfigure-legacy.tsx b/template/src/components/RTMConfigure-legacy.tsx new file mode 100644 index 000000000..53010409d --- /dev/null +++ b/template/src/components/RTMConfigure-legacy.tsx @@ -0,0 +1,848 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ +// @ts-nocheck +import React, {useState, useContext, useEffect, useRef} from 'react'; +import RtmEngine, {RtmChannelAttribute} from 'agora-react-native-rtm'; +import { + ContentInterface, + DispatchContext, + PropsContext, + useLocalUid, +} from '../../agora-rn-uikit'; +import ChatContext from './ChatContext'; +import {Platform} from 'react-native'; +import {backOff} from 'exponential-backoff'; +import {useString} from '../utils/useString'; +import {isAndroid, isIOS, isWeb, isWebInternal} from '../utils/common'; +import {useContent, useIsAttendee, useUserName} from 'customization-api'; +import { + safeJsonParse, + timeNow, + hasJsonStructure, + getMessageTime, + get32BitUid, +} from '../rtm/utils'; +import {EventUtils, EventsQueue, EventNames} from '../rtm-events'; +import events, {PersistanceLevel} from '../rtm-events-api'; +import RTMEngine from '../rtm/RTMEngine'; +import {filterObject} from '../utils'; +import SDKEvents from '../utils/SdkEvents'; +import isSDK from '../utils/isSDK'; +import {useAsyncEffect} from '../utils/useAsyncEffect'; +import { + WaitingRoomStatus, + useRoomInfo, +} from '../components/room-info/useRoomInfo'; +import LocalEventEmitter, { + LocalEventsEnum, +} from '../rtm-events-api/LocalEvents'; +import {PSTNUserLabel} from '../language/default-labels/videoCallScreenLabels'; +import {controlMessageEnum} from '../components/ChatContext'; +import {LogSource, logger} from '../logger/AppBuilderLogger'; +import {RECORDING_BOT_UID} from '../utils/constants'; + +export enum UserType { + ScreenShare = 'screenshare', +} + +const RtmConfigure = (props: any) => { + const rtmInitTimstamp = new Date().getTime(); + const localUid = useLocalUid(); + const {callActive} = props; + const {rtcProps} = useContext(PropsContext); + const {dispatch} = useContext(DispatchContext); + const {defaultContent, activeUids} = useContent(); + const defaultContentRef = useRef({defaultContent: defaultContent}); + const activeUidsRef = useRef({activeUids: activeUids}); + + const { + waitingRoomStatus, + data: {isHost}, + } = useRoomInfo(); + const waitingRoomStatusRef = useRef({waitingRoomStatus: waitingRoomStatus}); + + const isHostRef = useRef({isHost: isHost}); + + useEffect(() => { + isHostRef.current.isHost = isHost; + }, [isHost]); + + useEffect(() => { + waitingRoomStatusRef.current.waitingRoomStatus = waitingRoomStatus; + }, [waitingRoomStatus]); + + /** + * inside event callback state won't have latest value. + * so creating ref to access the state + */ + useEffect(() => { + activeUidsRef.current.activeUids = activeUids; + }, [activeUids]); + + useEffect(() => { + defaultContentRef.current.defaultContent = defaultContent; + }, [defaultContent]); + + const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); + const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); + const [onlineUsersCount, setTotalOnlineUsers] = useState(0); + + let engine = useRef(null!); + const timerValueRef: any = useRef(5); + + React.useEffect(() => { + setTotalOnlineUsers( + Object.keys( + filterObject( + defaultContent, + ([k, v]) => + v?.type === 'rtc' && + !v.offline && + activeUids.indexOf(v?.uid) !== -1, + ), + ).length, + ); + }, [defaultContent]); + + React.useEffect(() => { + if (!$config.ENABLE_CONVERSATIONAL_AI) { + const handBrowserClose = ev => { + ev.preventDefault(); + return (ev.returnValue = 'Are you sure you want to exit?'); + }; + const logoutRtm = () => { + engine.current.leaveChannel(rtcProps.channel); + }; + + if (!isWebInternal()) return; + window.addEventListener( + 'beforeunload', + isWeb() && !isSDK() ? handBrowserClose : () => {}, + ); + + window.addEventListener('pagehide', logoutRtm); + // cleanup this component + return () => { + window.removeEventListener( + 'beforeunload', + isWeb() && !isSDK() ? handBrowserClose : () => {}, + ); + window.removeEventListener('pagehide', logoutRtm); + }; + } + }, []); + + const doLoginAndSetupRTM = async () => { + try { + logger.log(LogSource.AgoraSDK, 'API', 'RTM login starts'); + await engine.current.login({ + uid: localUid.toString(), + token: rtcProps.rtm, + }); + logger.log(LogSource.AgoraSDK, 'API', 'RTM login done'); + RTMEngine.getInstance().setLocalUID(localUid.toString()); + logger.log(LogSource.AgoraSDK, 'API', 'RTM local Uid set'); + timerValueRef.current = 5; + await setAttribute(); + logger.log(LogSource.AgoraSDK, 'Log', 'RTM setting attribute done'); + } catch (error) { + logger.error(LogSource.AgoraSDK, 'Log', 'RTM login failed..Trying again'); + setTimeout(async () => { + timerValueRef.current = timerValueRef.current + timerValueRef.current; + doLoginAndSetupRTM(); + }, timerValueRef.current * 1000); + } + }; + + const setAttribute = async () => { + const rtmAttributes = [ + {key: 'screenUid', value: String(rtcProps.screenShareUid)}, + {key: 'isHost', value: String(isHostRef.current.isHost)}, + ]; + try { + await engine.current.setLocalUserAttributes(rtmAttributes); + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM setting local user attributes', + { + attr: rtmAttributes, + }, + ); + timerValueRef.current = 5; + await joinChannel(); + logger.log(LogSource.AgoraSDK, 'Log', 'RTM join channel done', { + data: rtmAttributes, + }); + setHasUserJoinedRTM(true); + await runQueuedEvents(); + setIsInitialQueueCompleted(true); + logger.log( + LogSource.AgoraSDK, + 'Log', + 'RTM queued events finished running', + { + attr: rtmAttributes, + }, + ); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + 'RTM setAttribute failed..Trying again', + ); + setTimeout(async () => { + timerValueRef.current = timerValueRef.current + timerValueRef.current; + setAttribute(); + }, timerValueRef.current * 1000); + } + }; + + const joinChannel = async () => { + try { + if (RTMEngine.getInstance().channelUid !== rtcProps.channel) { + await engine.current.joinChannel(rtcProps.channel); + logger.log(LogSource.AgoraSDK, 'API', 'RTM joinChannel', { + data: rtcProps.channel, + }); + RTMEngine.getInstance().setChannelId(rtcProps.channel); + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM setChannelId', + rtcProps.channel, + ); + logger.debug( + LogSource.SDK, + 'Event', + 'Emitting rtm joined', + rtcProps.channel, + ); + SDKEvents.emit('_rtm-joined', rtcProps.channel); + } else { + logger.debug( + LogSource.AgoraSDK, + 'Log', + 'RTM already joined channel skipping', + rtcProps.channel, + ); + } + timerValueRef.current = 5; + await getMembers(); + await readAllChannelAttributes(); + logger.log(LogSource.AgoraSDK, 'Log', 'RTM getMembers done'); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + 'RTM joinChannel failed..Trying again', + ); + setTimeout(async () => { + timerValueRef.current = timerValueRef.current + timerValueRef.current; + joinChannel(); + }, timerValueRef.current * 1000); + } + }; + + const updateRenderListState = ( + uid: number, + data: Partial, + ) => { + dispatch({type: 'UpdateRenderList', value: [uid, data]}); + }; + + const getMembers = async () => { + try { + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM getChannelMembersByID(getMembers) start', + ); + await engine.current + .getChannelMembersBychannelId(rtcProps.channel) + .then(async data => { + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM getChannelMembersByID data received', + data, + ); + await Promise.all( + data.members.map(async (member: any) => { + const backoffAttributes = backOff( + async () => { + logger.log( + LogSource.AgoraSDK, + 'API', + `RTM fetching getUserAttributesByUid for member ${member.uid}`, + ); + const attr = await engine.current.getUserAttributesByUid( + member.uid, + ); + if (!attr || !attr.attributes) { + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM attributes for member not found', + ); + throw attr; + } + logger.log( + LogSource.AgoraSDK, + 'API', + `RTM getUserAttributesByUid for member ${member.uid} received`, + { + attr, + }, + ); + for (const key in attr.attributes) { + if ( + attr.attributes.hasOwnProperty(key) && + attr.attributes[key] + ) { + return attr; + } else { + throw attr; + } + } + }, + { + retry: (e, idx) => { + logger.debug( + LogSource.AgoraSDK, + 'Log', + `[retrying] Attempt ${idx}. Fetching ${member.uid}'s name`, + e, + ); + return true; + }, + }, + ); + try { + const attr = await backoffAttributes; + console.log('[user attributes]:', {attr}); + //RTC layer uid type is number. so doing the parseInt to convert to number + //todo hari check android uid comparsion + const uid = parseInt(member.uid); + const screenUid = parseInt(attr?.attributes?.screenUid); + //start - updating user data in rtc + const userData = { + screenUid: screenUid, + //below thing for livestreaming + type: uid === parseInt(RECORDING_BOT_UID) ? 'bot' : 'rtc', + uid, + offline: false, + isHost: attr?.attributes?.isHost, + lastMessageTimeStamp: 0, + }; + updateRenderListState(uid, userData); + //end- updating user data in rtc + + //start - updating screenshare data in rtc + const screenShareUser = { + type: UserType.ScreenShare, + parentUid: uid, + }; + updateRenderListState(screenUid, screenShareUser); + //end - updating screenshare data in rtc + // setting screenshare data + // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) + // isActive to identify all active screenshare users in the call + for (const [key, value] of Object.entries(attr?.attributes)) { + if (hasJsonStructure(value as string)) { + const data = { + evt: key, + value: value, + }; + // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events + EventsQueue.enqueue({ + data: data, + uid: member.uid, + ts: timeNow(), + }); + } + } + } catch (e) { + logger.error( + LogSource.AgoraSDK, + 'Log', + `Could not retrieve name of ${member.uid}`, + e, + ); + } + }), + ); + logger.debug( + LogSource.AgoraSDK, + 'Log', + 'RTM fetched all data and user attr...RTM init done', + ); + }); + timerValueRef.current = 5; + } catch (error) { + setTimeout(async () => { + timerValueRef.current = timerValueRef.current + timerValueRef.current; + await getMembers(); + }, timerValueRef.current * 1000); + } + }; + + const readAllChannelAttributes = async () => { + try { + await engine.current + .getChannelAttributes(rtcProps.channel) + .then(async data => { + for (const item of data) { + const {key, value, lastUpdateTs, lastUpdateUserId} = item; + if (hasJsonStructure(value as string)) { + const evtData = { + evt: key, + value, + }; + // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events + EventsQueue.enqueue({ + data: evtData, + uid: lastUpdateUserId, + ts: lastUpdateTs, + }); + } + } + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM getChannelAttributes data received', + data, + ); + }); + timerValueRef.current = 5; + } catch (error) { + setTimeout(async () => { + timerValueRef.current = timerValueRef.current + timerValueRef.current; + await readAllChannelAttributes(); + }, timerValueRef.current * 1000); + } + }; + + const init = async () => { + //on sdk due to multiple re-render we are getting rtm error code 8 + //you are joining the same channel too frequently, exceeding the allowed rate of joining the same channel multiple times within a short period + //so checking rtm connection state before proceed + if (engine?.current?.client?.connectionState === 'CONNECTED') { + return; + } + logger.log(LogSource.AgoraSDK, 'Log', 'RTM creating engine...'); + engine.current = RTMEngine.getInstance().engine; + RTMEngine.getInstance(); + logger.log(LogSource.AgoraSDK, 'Log', 'RTM engine creation done'); + + engine.current.on('connectionStateChanged', (evt: any) => { + //console.log(evt); + }); + engine.current.on('error', (evt: any) => { + // console.log(evt); + }); + engine.current.on('channelMemberJoined', (data: any) => { + logger.log(LogSource.AgoraSDK, 'Event', 'channelMemberJoined', data); + const backoffAttributes = backOff( + async () => { + logger.log( + LogSource.AgoraSDK, + 'API', + `RTM fetching getUserAttributesByUid for member ${data.uid}`, + ); + const attr = await engine.current.getUserAttributesByUid(data.uid); + if (!attr || !attr.attributes) { + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM attributes for member not found', + ); + throw attr; + } + logger.log( + LogSource.AgoraSDK, + 'API', + `RTM getUserAttributesByUid for member ${data.uid} received`, + { + attr, + }, + ); + for (const key in attr.attributes) { + if (attr.attributes.hasOwnProperty(key) && attr.attributes[key]) { + return attr; + } else { + throw attr; + } + } + }, + { + retry: (e, idx) => { + logger.debug( + LogSource.AgoraSDK, + 'Log', + `[retrying] Attempt ${idx}. Fetching ${data.uid}'s name`, + e, + ); + return true; + }, + }, + ); + async function getname() { + try { + const attr = await backoffAttributes; + console.log('[user attributes]:', {attr}); + const uid = parseInt(data.uid); + const screenUid = parseInt(attr?.attributes?.screenUid); + + //start - updating user data in rtc + const userData = { + screenUid: screenUid, + //below thing for livestreaming + type: uid === parseInt(RECORDING_BOT_UID) ? 'bot' : 'rtc', + uid, + offline: false, + lastMessageTimeStamp: 0, + isHost: attr?.attributes?.isHost, + }; + updateRenderListState(uid, userData); + //end- updating user data in rtc + + //start - updating screenshare data in rtc + const screenShareUser = { + type: UserType.ScreenShare, + parentUid: uid, + }; + updateRenderListState(screenUid, screenShareUser); + //end - updating screenshare data in rtc + } catch (e) { + logger.error( + LogSource.AgoraSDK, + 'Event', + `Failed to retrive name of ${data.uid}`, + e, + ); + } + } + getname(); + }); + + engine.current.on('channelMemberLeft', (data: any) => { + logger.debug(LogSource.AgoraSDK, 'Event', 'channelMemberLeft', data); + // Chat of left user becomes undefined. So don't cleanup + const uid = data?.uid ? parseInt(data?.uid) : undefined; + if (!uid) return; + SDKEvents.emit('_rtm-left', uid); + // updating the rtc data + updateRenderListState(uid, { + offline: true, + }); + }); + + engine.current.addListener( + 'ChannelAttributesUpdated', + (attributeList: RtmChannelAttribute[]) => { + try { + attributeList.map((attribute: RtmChannelAttribute) => { + const {key, value, lastUpdateTs, lastUpdateUserId} = attribute; + const timestamp = getMessageTime(lastUpdateTs); + const sender = Platform.OS + ? get32BitUid(lastUpdateUserId) + : parseInt(lastUpdateUserId); + eventDispatcher( + { + evt: key, + value, + }, + sender, + timestamp, + ); + }); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + error, + ); + } + }, + ); + + engine.current.on('messageReceived', (evt: any) => { + logger.debug(LogSource.Events, 'CUSTOM_EVENTS', 'messageReceived', evt); + const {peerId, ts, text} = evt; + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + err, + ); + } + + const timestamp = getMessageTime(ts); + + const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId); + + try { + eventDispatcher(msg, sender, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + err, + ); + } + }); + + engine.current.on('channelMessageReceived', evt => { + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'channelMessageReceived', + evt, + ); + + const {uid, channelId, text, ts} = evt; + //whiteboard upload + if (uid == 1010101) { + const [err, res] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + err, + ); + } + + if (res?.data?.data?.images) { + LocalEventEmitter.emit( + LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, + res?.data?.data?.images, + ); + } + } else { + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + err, + ); + } + + const timestamp = getMessageTime(ts); + + const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid); + + if (channelId === rtcProps.channel) { + try { + eventDispatcher(msg, sender, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + error, + ); + } + } + } + }); + + await doLoginAndSetupRTM(); + }; + + const runQueuedEvents = async () => { + try { + while (!EventsQueue.isEmpty()) { + const currEvt = EventsQueue.dequeue(); + await eventDispatcher(currEvt.data, currEvt.uid, currEvt.ts); + } + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while running queue events', + error, + ); + } + }; + + const eventDispatcher = async ( + data: { + evt: string; + value: string; + }, + sender: string, + ts: number, + ) => { + console.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'inside eventDispatcher ', + data, + ); + + let evt = '', + value = {}; + + if (data.feat === 'WAITING_ROOM') { + if (data.etyp === 'REQUEST') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + attendee_uid: data.data.data.attendee_uid, + attendee_screenshare_uid: data.data.data.attendee_screenshare_uid, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; //rename if client side RTM meessage is to be sent for approval + value = formattedData; + } + if (data.etyp === 'RESPONSE') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + approved: data.data.data.approved, + channelName: data.data.data.channel_name, + mainUser: data.data.data.mainUser, + screenShare: data.data.data.screenShare, + whiteboard: data.data.data.whiteboard, + chat: data.data.data?.chat, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } + } else { + if ( + $config.ENABLE_WAITING_ROOM && + !isHostRef.current?.isHost && + waitingRoomStatusRef.current?.waitingRoomStatus !== + WaitingRoomStatus.APPROVED + ) { + if ( + data.evt === controlMessageEnum.muteAudio || + data.evt === controlMessageEnum.muteVideo + ) { + return; + } else { + evt = data.evt; + value = data.value; + } + } else { + evt = data.evt; + value = data.value; + } + } + + try { + const {payload, persistLevel, source} = JSON.parse(value); + // Step 1: Set local attributes + if (persistLevel === PersistanceLevel.Session) { + const rtmAttribute = {key: evt, value: value}; + await engine.current.addOrUpdateLocalUserAttributes([rtmAttribute]); + } + // Step 2: Emit the event + console.debug(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: '); + EventUtils.emitEvent(evt, source, {payload, persistLevel, sender, ts}); + // Because async gets evaluated in a different order when in an sdk + if (evt === 'name') { + setTimeout(() => { + EventUtils.emitEvent(evt, source, { + payload, + persistLevel, + sender, + ts, + }); + }, 200); + } + } catch (error) { + console.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while emiting event:', + error, + ); + } + }; + + const end = async () => { + if (!callActive) { + return; + } + await RTMEngine.getInstance().destroy(); + logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); + if (isIOS() || isAndroid()) { + EventUtils.clear(); + } + setHasUserJoinedRTM(false); + logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); + }; + + useAsyncEffect(async () => { + //waiting room attendee -> rtm login will happen on page load + if ($config.ENABLE_WAITING_ROOM) { + //attendee + //for waiting room attendee rtm login will happen on mount + if (!isHost && !callActive) { + await init(); + } + //host + if ( + isHost && + ($config.AUTO_CONNECT_RTM || (!$config.AUTO_CONNECT_RTM && callActive)) + ) { + await init(); + } + } else { + //non waiting room case + //host and attendee + if ( + $config.AUTO_CONNECT_RTM || + (!$config.AUTO_CONNECT_RTM && callActive) + ) { + await init(); + } + } + return async () => { + await end(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [rtcProps.channel, rtcProps.appId, callActive]); + + return ( + + {props.children} + + ); +}; + +export default RtmConfigure; diff --git a/template/src/components/RTMConfigure.tsx b/template/src/components/RTMConfigure.tsx index 53010409d..42e1e4c49 100644 --- a/template/src/components/RTMConfigure.tsx +++ b/template/src/components/RTMConfigure.tsx @@ -9,21 +9,32 @@ information visit https://appbuilder.agora.io. ********************************************* */ -// @ts-nocheck + import React, {useState, useContext, useEffect, useRef} from 'react'; -import RtmEngine, {RtmChannelAttribute} from 'agora-react-native-rtm'; +import { + type GetChannelMetadataResponse, + type GetOnlineUsersResponse, + type LinkStateEvent, + type MessageEvent, + type Metadata, + type PresenceEvent, + type SetOrUpdateUserMetadataOptions, + type StorageEvent, + type RTMClient, + type GetUserMetadataResponse, +} from 'agora-react-native-rtm'; import { ContentInterface, DispatchContext, PropsContext, + UidType, useLocalUid, } from '../../agora-rn-uikit'; import ChatContext from './ChatContext'; import {Platform} from 'react-native'; import {backOff} from 'exponential-backoff'; -import {useString} from '../utils/useString'; import {isAndroid, isIOS, isWeb, isWebInternal} from '../utils/common'; -import {useContent, useIsAttendee, useUserName} from 'customization-api'; +import {useContent} from 'customization-api'; import { safeJsonParse, timeNow, @@ -31,8 +42,8 @@ import { getMessageTime, get32BitUid, } from '../rtm/utils'; -import {EventUtils, EventsQueue, EventNames} from '../rtm-events'; -import events, {PersistanceLevel} from '../rtm-events-api'; +import {EventUtils, EventsQueue} from '../rtm-events'; +import {PersistanceLevel} from '../rtm-events-api'; import RTMEngine from '../rtm/RTMEngine'; import {filterObject} from '../utils'; import SDKEvents from '../utils/SdkEvents'; @@ -45,60 +56,79 @@ import { import LocalEventEmitter, { LocalEventsEnum, } from '../rtm-events-api/LocalEvents'; -import {PSTNUserLabel} from '../language/default-labels/videoCallScreenLabels'; import {controlMessageEnum} from '../components/ChatContext'; import {LogSource, logger} from '../logger/AppBuilderLogger'; import {RECORDING_BOT_UID} from '../utils/constants'; +import { + nativeChannelTypeMapping, + nativeLinkStateMapping, + nativePresenceEventTypeMapping, + nativeStorageEventTypeMapping, +} from '../../bridge/rtm/web/Types'; export enum UserType { ScreenShare = 'screenshare', } +const eventTimeouts = new Map>(); + const RtmConfigure = (props: any) => { + let engine = useRef(null!); const rtmInitTimstamp = new Date().getTime(); const localUid = useLocalUid(); const {callActive} = props; const {rtcProps} = useContext(PropsContext); const {dispatch} = useContext(DispatchContext); const {defaultContent, activeUids} = useContent(); - const defaultContentRef = useRef({defaultContent: defaultContent}); - const activeUidsRef = useRef({activeUids: activeUids}); - const { waitingRoomStatus, data: {isHost}, } = useRoomInfo(); - const waitingRoomStatusRef = useRef({waitingRoomStatus: waitingRoomStatus}); + const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); + const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); + const [onlineUsersCount, setTotalOnlineUsers] = useState(0); + const timerValueRef: any = useRef(5); + // Track RTM connection state (equivalent to v1.5x connectionState check) + const [rtmConnectionState, setRtmConnectionState] = useState(0); // 0=IDLE, 2=CONNECTED + /** + * inside event callback state won't have latest value. + * so creating ref to access the state + */ const isHostRef = useRef({isHost: isHost}); - useEffect(() => { isHostRef.current.isHost = isHost; }, [isHost]); + const waitingRoomStatusRef = useRef({waitingRoomStatus: waitingRoomStatus}); useEffect(() => { waitingRoomStatusRef.current.waitingRoomStatus = waitingRoomStatus; }, [waitingRoomStatus]); - /** - * inside event callback state won't have latest value. - * so creating ref to access the state - */ + const activeUidsRef = useRef({activeUids: activeUids}); useEffect(() => { activeUidsRef.current.activeUids = activeUids; }, [activeUids]); + const defaultContentRef = useRef({defaultContent: defaultContent}); useEffect(() => { defaultContentRef.current.defaultContent = defaultContent; }, [defaultContent]); - const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); - const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); - const [onlineUsersCount, setTotalOnlineUsers] = useState(0); - - let engine = useRef(null!); - const timerValueRef: any = useRef(5); + // Eventdispatcher timeout refs clean + const isRTMMounted = useRef(true); + useEffect(() => { + return () => { + isRTMMounted.current = false; + // Clear all pending timeouts on unmount + for (const timeout of eventTimeouts.values()) { + clearTimeout(timeout); + } + eventTimeouts.clear(); + }; + }, []); + // Set online users React.useEffect(() => { setTotalOnlineUsers( Object.keys( @@ -107,31 +137,42 @@ const RtmConfigure = (props: any) => { ([k, v]) => v?.type === 'rtc' && !v.offline && - activeUids.indexOf(v?.uid) !== -1, + activeUidsRef.current.activeUids.indexOf(v?.uid) !== -1, ), ).length, ); }, [defaultContent]); React.useEffect(() => { - if (!$config.ENABLE_CONVERSATIONAL_AI) { + // If its not a convo ai project and + // the platform is web execute the window listeners + if (!$config.ENABLE_CONVERSATIONAL_AI && isWebInternal()) { const handBrowserClose = ev => { ev.preventDefault(); return (ev.returnValue = 'Are you sure you want to exit?'); }; const logoutRtm = () => { - engine.current.leaveChannel(rtcProps.channel); + try { + if (engine.current && RTMEngine.getInstance().channelUid) { + // First unsubscribe from channel (like v1.5x leaveChannel) + engine.current.unsubscribe(RTMEngine.getInstance().channelUid); + // Then logout + engine.current.logout(); + } + } catch (error) { + console.error('Error during browser close RTM cleanup:', error); + } }; - if (!isWebInternal()) return; + // Set up window listeners window.addEventListener( 'beforeunload', isWeb() && !isSDK() ? handBrowserClose : () => {}, ); window.addEventListener('pagehide', logoutRtm); - // cleanup this component return () => { + // Remove listeners on unmount window.removeEventListener( 'beforeunload', isWeb() && !isSDK() ? handBrowserClose : () => {}, @@ -141,23 +182,304 @@ const RtmConfigure = (props: any) => { } }, []); + const init = async (rtcUid: UidType) => { + //on sdk due to multiple re-render we are getting rtm error code 8 + //you are joining the same channel too frequently, exceeding the allowed rate of joining the same channel multiple times within a short period + //so checking rtm connection state before proceed + + // Check if already connected (equivalent to v1.5x connectionState === 'CONNECTED') + if ( + rtmConnectionState === nativeLinkStateMapping.CONNECTED && + RTMEngine.getInstance().isEngineReady + ) { + logger.log( + LogSource.AgoraSDK, + 'Log', + '🚫 RTM already connected, skipping initialization', + ); + return; + } + + try { + if (!RTMEngine.getInstance().isEngineReady) { + RTMEngine.getInstance().setLocalUID(rtcUid); + logger.log(LogSource.AgoraSDK, 'API', 'RTM local Uid set', rtcUid); + } + engine.current = RTMEngine.getInstance().engine; + // Logout any opened sessions if any + engine.current.logout(); + logger.log(LogSource.AgoraSDK, 'Log', 'RTM client creation done'); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + 'RTM engine initialization failed:', + {error}, + ); + throw error; + } + + engine.current.addEventListener( + 'linkState', + async (data: LinkStateEvent) => { + // Update connection state for duplicate initialization prevention + setRtmConnectionState(data.currentState); + logger.log( + LogSource.AgoraSDK, + 'Event', + `RTM linkState changed: ${data.previousState} -> ${data.currentState}`, + data, + ); + if (data.currentState === nativeLinkStateMapping.CONNECTED) { + // CONNECTED state + logger.log(LogSource.AgoraSDK, 'Event', 'RTM connected', { + previousState: data.previousState, + currentState: data.currentState, + }); + } + if (data.currentState === nativeLinkStateMapping.FAILED) { + // FAILED state + logger.error(LogSource.AgoraSDK, 'Event', 'RTM connection failed', { + error: { + reasonCode: data.reasonCode, + currentState: data.currentState, + }, + }); + } + }, + ); + + engine.current.addEventListener('storage', (storage: StorageEvent) => { + // when remote user sets/updates metadata - 3 + if ( + storage.eventType === nativeStorageEventTypeMapping.SET || + storage.eventType === nativeStorageEventTypeMapping.UPDATE + ) { + const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; + const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; + logger.log( + LogSource.AgoraSDK, + 'Event', + `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, + storage, + ); + try { + if (storage.data?.items && Array.isArray(storage.data.items)) { + storage.data.items.forEach(item => { + try { + if (!item || !item.key) { + logger.warn( + LogSource.Events, + 'CUSTOM_EVENTS', + 'Invalid storage item:', + item, + ); + return; + } + + const {key, value, authorUserId, updateTs} = item; + const timestamp = getMessageTime(updateTs); + const sender = Platform.OS + ? get32BitUid(authorUserId) + : parseInt(authorUserId, 10); + eventDispatcher( + { + evt: key, + value, + }, + `${sender}`, + timestamp, + ); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + `Failed to process storage item: ${JSON.stringify(item)}`, + {error}, + ); + } + }); + } + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + }); + + engine.current.addEventListener( + 'presence', + async (presence: PresenceEvent) => { + if (`${localUid}` === presence.publisher) { + return; + } + // remoteJoinChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { + logger.log( + LogSource.AgoraSDK, + 'Event', + 'RTM presenceEvent of type [3 - remoteJoin] (channelMemberJoined)', + ); + const backoffAttributes = await fetchUserAttributesWithBackoffRetry( + presence.publisher, + ); + await processUserUidAttributes(backoffAttributes, presence.publisher); + } + // remoteLeaveChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { + logger.log( + LogSource.AgoraSDK, + 'Event', + 'RTM presenceEvent of type [4 - remoteLeave] (channelMemberLeft)', + presence, + ); + // Chat of left user becomes undefined. So don't cleanup + const uid = presence?.publisher + ? parseInt(presence.publisher, 10) + : undefined; + + if (!uid) { + return; + } + SDKEvents.emit('_rtm-left', uid); + // updating the rtc data + updateRenderListState(uid, { + offline: true, + }); + } + }, + ); + + engine.current.addEventListener('message', (message: MessageEvent) => { + if (`${localUid}` === message.publisher) { + return; + } + // message - 1 (channel) + if (message.channelType === nativeChannelTypeMapping.MESSAGE) { + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', + message, + ); + const { + publisher: uid, + channelName: channelId, + message: text, + timestamp: ts, + } = message; + //whiteboard upload + if (parseInt(uid, 10) === 1010101) { + const [err, res] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + if (res?.data?.data?.images) { + LocalEventEmitter.emit( + LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, + res?.data?.data?.images, + ); + } + } else { + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); + + if (channelId === rtcProps.channel) { + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + } + } + + // message - 3 (user) + if (message.channelType === nativeChannelTypeMapping.USER) { + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [3- USER] (messageReceived)', + message, + ); + const {publisher: peerId, timestamp: ts, message: text} = message; + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + + const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); + + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + }); + + await doLoginAndSetupRTM(); + }; + const doLoginAndSetupRTM = async () => { try { logger.log(LogSource.AgoraSDK, 'API', 'RTM login starts'); + console.log('supriya[RTM] localUid', localUid, rtcProps, rtcProps.rtm); await engine.current.login({ - uid: localUid.toString(), + // @ts-ignore token: rtcProps.rtm, }); logger.log(LogSource.AgoraSDK, 'API', 'RTM login done'); - RTMEngine.getInstance().setLocalUID(localUid.toString()); - logger.log(LogSource.AgoraSDK, 'API', 'RTM local Uid set'); timerValueRef.current = 5; + // waiting for login to be fully connected + await new Promise(resolve => setTimeout(resolve, 500)); await setAttribute(); - logger.log(LogSource.AgoraSDK, 'Log', 'RTM setting attribute done'); } catch (error) { - logger.error(LogSource.AgoraSDK, 'Log', 'RTM login failed..Trying again'); + logger.error( + LogSource.AgoraSDK, + 'Log', + 'RTM login failed..Trying again', + {error}, + ); setTimeout(async () => { - timerValueRef.current = timerValueRef.current + timerValueRef.current; + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); doLoginAndSetupRTM(); }, timerValueRef.current * 1000); } @@ -169,7 +491,13 @@ const RtmConfigure = (props: any) => { {key: 'isHost', value: String(isHostRef.current.isHost)}, ]; try { - await engine.current.setLocalUserAttributes(rtmAttributes); + const data: Metadata = { + items: rtmAttributes, + }; + const options: SetOrUpdateUserMetadataOptions = { + userId: `${localUid}`, + }; + await engine.current.storage.setUserMetadata(data, options); logger.log( LogSource.AgoraSDK, 'API', @@ -179,10 +507,15 @@ const RtmConfigure = (props: any) => { }, ); timerValueRef.current = 5; - await joinChannel(); - logger.log(LogSource.AgoraSDK, 'Log', 'RTM join channel done', { - data: rtmAttributes, - }); + await subscribeChannel(); + logger.log( + LogSource.AgoraSDK, + 'Log', + 'RTM subscribe, fetch members, reading channel atrributes all done', + { + data: rtmAttributes, + }, + ); setHasUserJoinedRTM(true); await runQueuedEvents(); setIsInitialQueueCompleted(true); @@ -199,184 +532,138 @@ const RtmConfigure = (props: any) => { LogSource.AgoraSDK, 'Log', 'RTM setAttribute failed..Trying again', + {error}, ); setTimeout(async () => { - timerValueRef.current = timerValueRef.current + timerValueRef.current; + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); setAttribute(); }, timerValueRef.current * 1000); } }; - const joinChannel = async () => { + const subscribeChannel = async () => { try { - if (RTMEngine.getInstance().channelUid !== rtcProps.channel) { - await engine.current.joinChannel(rtcProps.channel); - logger.log(LogSource.AgoraSDK, 'API', 'RTM joinChannel', { + if (RTMEngine.getInstance().channelUid === rtcProps.channel) { + logger.debug( + LogSource.AgoraSDK, + 'Log', + '🚫 RTM already subscribed channel skipping', + rtcProps.channel, + ); + } else { + await engine.current.subscribe(rtcProps.channel, { + withMessage: true, + withPresence: true, + withMetadata: true, + withLock: false, + }); + logger.log(LogSource.AgoraSDK, 'API', 'RTM subscribeChannel', { data: rtcProps.channel, }); + + // Set channel ID AFTER successful subscribe (like v1.5x) RTMEngine.getInstance().setChannelId(rtcProps.channel); logger.log( LogSource.AgoraSDK, 'API', - 'RTM setChannelId', + 'RTM setChannelId as subscribe is successful', rtcProps.channel, ); + logger.debug( LogSource.SDK, 'Event', 'Emitting rtm joined', rtcProps.channel, ); + // @ts-ignore SDKEvents.emit('_rtm-joined', rtcProps.channel); - } else { - logger.debug( + timerValueRef.current = 5; + await getMembers(); + await readAllChannelAttributes(); + logger.log( LogSource.AgoraSDK, 'Log', - 'RTM already joined channel skipping', - rtcProps.channel, + 'RTM readAllChannelAttributes and getMembers done', ); } - timerValueRef.current = 5; - await getMembers(); - await readAllChannelAttributes(); - logger.log(LogSource.AgoraSDK, 'Log', 'RTM getMembers done'); } catch (error) { logger.error( LogSource.AgoraSDK, 'Log', - 'RTM joinChannel failed..Trying again', + 'RTM subscribeChannel failed..Trying again', + {error}, ); setTimeout(async () => { - timerValueRef.current = timerValueRef.current + timerValueRef.current; - joinChannel(); + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); + subscribeChannel(); }, timerValueRef.current * 1000); } }; - const updateRenderListState = ( - uid: number, - data: Partial, - ) => { - dispatch({type: 'UpdateRenderList', value: [uid, data]}); - }; - const getMembers = async () => { try { logger.log( LogSource.AgoraSDK, 'API', - 'RTM getChannelMembersByID(getMembers) start', + 'RTM presence.getOnlineUsers(getMembers) start', ); - await engine.current - .getChannelMembersBychannelId(rtcProps.channel) - .then(async data => { + await engine.current.presence + .getOnlineUsers(rtcProps.channel, 1) + .then(async (data: GetOnlineUsersResponse) => { logger.log( LogSource.AgoraSDK, 'API', - 'RTM getChannelMembersByID data received', + 'RTM presence.getOnlineUsers data received', data, ); await Promise.all( - data.members.map(async (member: any) => { - const backoffAttributes = backOff( - async () => { - logger.log( - LogSource.AgoraSDK, - 'API', - `RTM fetching getUserAttributesByUid for member ${member.uid}`, - ); - const attr = await engine.current.getUserAttributesByUid( - member.uid, - ); - if (!attr || !attr.attributes) { - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM attributes for member not found', - ); - throw attr; - } - logger.log( - LogSource.AgoraSDK, - 'API', - `RTM getUserAttributesByUid for member ${member.uid} received`, - { - attr, - }, - ); - for (const key in attr.attributes) { - if ( - attr.attributes.hasOwnProperty(key) && - attr.attributes[key] - ) { - return attr; - } else { - throw attr; - } - } - }, - { - retry: (e, idx) => { - logger.debug( - LogSource.AgoraSDK, - 'Log', - `[retrying] Attempt ${idx}. Fetching ${member.uid}'s name`, - e, - ); - return true; - }, - }, - ); + data.occupants?.map(async member => { try { - const attr = await backoffAttributes; - console.log('[user attributes]:', {attr}); - //RTC layer uid type is number. so doing the parseInt to convert to number - //todo hari check android uid comparsion - const uid = parseInt(member.uid); - const screenUid = parseInt(attr?.attributes?.screenUid); - //start - updating user data in rtc - const userData = { - screenUid: screenUid, - //below thing for livestreaming - type: uid === parseInt(RECORDING_BOT_UID) ? 'bot' : 'rtc', - uid, - offline: false, - isHost: attr?.attributes?.isHost, - lastMessageTimeStamp: 0, - }; - updateRenderListState(uid, userData); - //end- updating user data in rtc + const backoffAttributes = + await fetchUserAttributesWithBackoffRetry(member.userId); - //start - updating screenshare data in rtc - const screenShareUser = { - type: UserType.ScreenShare, - parentUid: uid, - }; - updateRenderListState(screenUid, screenShareUser); - //end - updating screenshare data in rtc + await processUserUidAttributes( + backoffAttributes, + member.userId, + ); // setting screenshare data // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) // isActive to identify all active screenshare users in the call - for (const [key, value] of Object.entries(attr?.attributes)) { - if (hasJsonStructure(value as string)) { - const data = { - evt: key, - value: value, - }; - // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events - EventsQueue.enqueue({ - data: data, - uid: member.uid, - ts: timeNow(), - }); + backoffAttributes?.items?.forEach(item => { + try { + if (hasJsonStructure(item.value as string)) { + const data = { + evt: item.key, // Use item.key instead of key + value: item.value, // Use item.value instead of value + }; + // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events + EventsQueue.enqueue({ + data: data, + uid: member.userId, + ts: timeNow(), + }); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + `RTM Failed to process user attribute item for ${ + member.userId + }: ${JSON.stringify(item)}`, + {error}, + ); + // Continue processing other items } - } + }); } catch (e) { logger.error( LogSource.AgoraSDK, 'Log', - `Could not retrieve name of ${member.uid}`, - e, + `RTM Could not retrieve name of ${member.userId}`, + {error: e}, ); } }), @@ -390,7 +677,8 @@ const RtmConfigure = (props: any) => { timerValueRef.current = 5; } catch (error) { setTimeout(async () => { - timerValueRef.current = timerValueRef.current + timerValueRef.current; + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); await getMembers(); }, timerValueRef.current * 1000); } @@ -398,286 +686,169 @@ const RtmConfigure = (props: any) => { const readAllChannelAttributes = async () => { try { - await engine.current - .getChannelAttributes(rtcProps.channel) - .then(async data => { - for (const item of data) { - const {key, value, lastUpdateTs, lastUpdateUserId} = item; - if (hasJsonStructure(value as string)) { - const evtData = { - evt: key, - value, - }; - // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events - EventsQueue.enqueue({ - data: evtData, - uid: lastUpdateUserId, - ts: lastUpdateTs, - }); + await engine.current.storage + .getChannelMetadata(rtcProps.channel, 1) + .then(async (data: GetChannelMetadataResponse) => { + for (const item of data.items) { + try { + const {key, value, authorUserId, updateTs} = item; + if (hasJsonStructure(value as string)) { + const evtData = { + evt: key, + value, + }; + // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events + EventsQueue.enqueue({ + data: evtData, + uid: authorUserId, + ts: updateTs, + }); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + `RTM Failed to process channel attribute item: ${JSON.stringify( + item, + )}`, + {error}, + ); + // Continue processing other items } } logger.log( LogSource.AgoraSDK, 'API', - 'RTM getChannelAttributes data received', + 'RTM storage.getChannelMetadata data received', data, ); }); timerValueRef.current = 5; } catch (error) { setTimeout(async () => { - timerValueRef.current = timerValueRef.current + timerValueRef.current; + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); await readAllChannelAttributes(); }, timerValueRef.current * 1000); } }; - const init = async () => { - //on sdk due to multiple re-render we are getting rtm error code 8 - //you are joining the same channel too frequently, exceeding the allowed rate of joining the same channel multiple times within a short period - //so checking rtm connection state before proceed - if (engine?.current?.client?.connectionState === 'CONNECTED') { - return; - } - logger.log(LogSource.AgoraSDK, 'Log', 'RTM creating engine...'); - engine.current = RTMEngine.getInstance().engine; - RTMEngine.getInstance(); - logger.log(LogSource.AgoraSDK, 'Log', 'RTM engine creation done'); + const fetchUserAttributesWithBackoffRetry = async ( + userId: string, + ): Promise => { + return backOff( + async () => { + logger.log( + LogSource.AgoraSDK, + 'API', + `RTM fetching getUserMetadata for member ${userId}`, + ); - engine.current.on('connectionStateChanged', (evt: any) => { - //console.log(evt); - }); - engine.current.on('error', (evt: any) => { - // console.log(evt); - }); - engine.current.on('channelMemberJoined', (data: any) => { - logger.log(LogSource.AgoraSDK, 'Event', 'channelMemberJoined', data); - const backoffAttributes = backOff( - async () => { - logger.log( - LogSource.AgoraSDK, - 'API', - `RTM fetching getUserAttributesByUid for member ${data.uid}`, - ); - const attr = await engine.current.getUserAttributesByUid(data.uid); - if (!attr || !attr.attributes) { - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM attributes for member not found', - ); - throw attr; - } + const attr: GetUserMetadataResponse = + await engine.current.storage.getUserMetadata({ + userId: userId, + }); + + if (!attr || !attr.items) { logger.log( LogSource.AgoraSDK, 'API', - `RTM getUserAttributesByUid for member ${data.uid} received`, - { - attr, - }, - ); - for (const key in attr.attributes) { - if (attr.attributes.hasOwnProperty(key) && attr.attributes[key]) { - return attr; - } else { - throw attr; - } - } - }, - { - retry: (e, idx) => { - logger.debug( - LogSource.AgoraSDK, - 'Log', - `[retrying] Attempt ${idx}. Fetching ${data.uid}'s name`, - e, - ); - return true; - }, - }, - ); - async function getname() { - try { - const attr = await backoffAttributes; - console.log('[user attributes]:', {attr}); - const uid = parseInt(data.uid); - const screenUid = parseInt(attr?.attributes?.screenUid); - - //start - updating user data in rtc - const userData = { - screenUid: screenUid, - //below thing for livestreaming - type: uid === parseInt(RECORDING_BOT_UID) ? 'bot' : 'rtc', - uid, - offline: false, - lastMessageTimeStamp: 0, - isHost: attr?.attributes?.isHost, - }; - updateRenderListState(uid, userData); - //end- updating user data in rtc - - //start - updating screenshare data in rtc - const screenShareUser = { - type: UserType.ScreenShare, - parentUid: uid, - }; - updateRenderListState(screenUid, screenShareUser); - //end - updating screenshare data in rtc - } catch (e) { - logger.error( - LogSource.AgoraSDK, - 'Event', - `Failed to retrive name of ${data.uid}`, - e, + 'RTM attributes for member not found', ); + throw attr; } - } - getname(); - }); - engine.current.on('channelMemberLeft', (data: any) => { - logger.debug(LogSource.AgoraSDK, 'Event', 'channelMemberLeft', data); - // Chat of left user becomes undefined. So don't cleanup - const uid = data?.uid ? parseInt(data?.uid) : undefined; - if (!uid) return; - SDKEvents.emit('_rtm-left', uid); - // updating the rtc data - updateRenderListState(uid, { - offline: true, - }); - }); + logger.log( + LogSource.AgoraSDK, + 'API', + `RTM getUserMetadata for member ${userId} received`, + {attr}, + ); - engine.current.addListener( - 'ChannelAttributesUpdated', - (attributeList: RtmChannelAttribute[]) => { - try { - attributeList.map((attribute: RtmChannelAttribute) => { - const {key, value, lastUpdateTs, lastUpdateUserId} = attribute; - const timestamp = getMessageTime(lastUpdateTs); - const sender = Platform.OS - ? get32BitUid(lastUpdateUserId) - : parseInt(lastUpdateUserId); - eventDispatcher( - { - evt: key, - value, - }, - sender, - timestamp, - ); - }); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - error, - ); + if (attr.items && attr.items.length > 0) { + return attr; + } else { + throw attr; } }, + { + retry: (e, idx) => { + logger.debug( + LogSource.AgoraSDK, + 'Log', + `RTM [retrying] Attempt ${idx}. Fetching ${userId}'s attributes`, + e, + ); + return true; + }, + }, ); + }; - engine.current.on('messageReceived', (evt: any) => { - logger.debug(LogSource.Events, 'CUSTOM_EVENTS', 'messageReceived', evt); - const {peerId, ts, text} = evt; - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - err, - ); - } - - const timestamp = getMessageTime(ts); + const processUserUidAttributes = async ( + attr: GetUserMetadataResponse, + userId: string, + ) => { + try { + console.log('[user attributes]:', {attr}); + const uid = parseInt(userId, 10); + const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); + const isHostItem = attr?.items?.find(item => item.key === 'isHost'); + const screenUid = screenUidItem?.value + ? parseInt(screenUidItem.value, 10) + : undefined; - const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId); + //start - updating user data in rtc + const userData = { + screenUid: screenUid, + //below thing for livestreaming + type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', + uid, + offline: false, + isHost: isHostItem?.value || false, + lastMessageTimeStamp: 0, + }; + updateRenderListState(uid, userData); + //end- updating user data in rtc - try { - eventDispatcher(msg, sender, timestamp); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - err, - ); + //start - updating screenshare data in rtc + if (screenUid) { + const screenShareUser = { + type: UserType.ScreenShare, + parentUid: uid, + }; + updateRenderListState(screenUid, screenShareUser); } - }); - - engine.current.on('channelMessageReceived', evt => { - logger.debug( - LogSource.Events, - 'CUSTOM_EVENTS', - 'channelMessageReceived', - evt, + //end - updating screenshare data in rtc + } catch (e) { + logger.error( + LogSource.AgoraSDK, + 'Event', + `RTM Failed to process user data for ${userId}`, + {error: e}, ); + } + }; - const {uid, channelId, text, ts} = evt; - //whiteboard upload - if (uid == 1010101) { - const [err, res] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - err, - ); - } - - if (res?.data?.data?.images) { - LocalEventEmitter.emit( - LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, - res?.data?.data?.images, - ); - } - } else { - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - err, - ); - } - - const timestamp = getMessageTime(ts); - - const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid); - - if (channelId === rtcProps.channel) { - try { - eventDispatcher(msg, sender, timestamp); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - error, - ); - } - } - } - }); - - await doLoginAndSetupRTM(); + const updateRenderListState = ( + uid: number, + data: Partial, + ) => { + dispatch({type: 'UpdateRenderList', value: [uid, data]}); }; const runQueuedEvents = async () => { try { while (!EventsQueue.isEmpty()) { const currEvt = EventsQueue.dequeue(); - await eventDispatcher(currEvt.data, currEvt.uid, currEvt.ts); + await eventDispatcher(currEvt.data, `${currEvt.uid}`, currEvt.ts); } } catch (error) { logger.error( LogSource.Events, 'CUSTOM_EVENTS', 'error while running queue events', - error, + {error}, ); } }; @@ -686,11 +857,13 @@ const RtmConfigure = (props: any) => { data: { evt: string; value: string; + feat?: string; + etyp?: string; }, sender: string, ts: number, ) => { - console.debug( + console.log( LogSource.Events, 'CUSTOM_EVENTS', 'inside eventDispatcher ', @@ -698,10 +871,23 @@ const RtmConfigure = (props: any) => { ); let evt = '', - value = {}; + value = ''; - if (data.feat === 'WAITING_ROOM') { - if (data.etyp === 'REQUEST') { + if (data?.feat === 'BREAKOUT_ROOM') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + data: data.data, + action: data.act, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } else if (data?.feat === 'WAITING_ROOM') { + if (data?.etyp === 'REQUEST') { const outputData = { evt: `${data.feat}_${data.etyp}`, payload: JSON.stringify({ @@ -712,10 +898,10 @@ const RtmConfigure = (props: any) => { source: 'core', }; const formattedData = JSON.stringify(outputData); - evt = data.feat + '_' + data.etyp; //rename if client side RTM meessage is to be sent for approval + evt = data.feat + '_' + data.etyp; value = formattedData; } - if (data.etyp === 'RESPONSE') { + if (data?.etyp === 'RESPONSE') { const outputData = { evt: `${data.feat}_${data.etyp}`, payload: JSON.stringify({ @@ -756,32 +942,65 @@ const RtmConfigure = (props: any) => { } try { - const {payload, persistLevel, source} = JSON.parse(value); + let parsedValue; + try { + parsedValue = typeof value === 'string' ? JSON.parse(value) : value; + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'RTM Failed to parse event value in event dispatcher:', + {error}, + ); + return; + } + const {payload, persistLevel, source} = parsedValue; // Step 1: Set local attributes if (persistLevel === PersistanceLevel.Session) { const rtmAttribute = {key: evt, value: value}; - await engine.current.addOrUpdateLocalUserAttributes([rtmAttribute]); + const options: SetOrUpdateUserMetadataOptions = { + userId: `${localUid}`, + }; + await engine.current.storage.setUserMetadata( + { + items: [rtmAttribute], + }, + options, + ); } // Step 2: Emit the event - console.debug(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: '); + console.log(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: '); EventUtils.emitEvent(evt, source, {payload, persistLevel, sender, ts}); // Because async gets evaluated in a different order when in an sdk if (evt === 'name') { - setTimeout(() => { + // 1. Cancel existing timeout for this sender + if (eventTimeouts.has(sender)) { + clearTimeout(eventTimeouts.get(sender)!); + } + // 2. Create new timeout with tracking + const timeout = setTimeout(() => { + // 3. Guard against unmounted component + if (!isRTMMounted.current) { + return; + } EventUtils.emitEvent(evt, source, { payload, persistLevel, sender, ts, }); + // 4. Clean up after execution + eventTimeouts.delete(sender); }, 200); + // 5. Track the timeout for cleanup + eventTimeouts.set(sender, timeout); } } catch (error) { console.error( LogSource.Events, 'CUSTOM_EVENTS', 'error while emiting event:', - error, + {error}, ); } }; @@ -790,45 +1009,58 @@ const RtmConfigure = (props: any) => { if (!callActive) { return; } + // Destroy and clean up RTM state await RTMEngine.getInstance().destroy(); + // Set the engine as null + engine.current = null; logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); if (isIOS() || isAndroid()) { EventUtils.clear(); } setHasUserJoinedRTM(false); + setIsInitialQueueCompleted(false); logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); }; useAsyncEffect(async () => { //waiting room attendee -> rtm login will happen on page load - if ($config.ENABLE_WAITING_ROOM) { - //attendee - //for waiting room attendee rtm login will happen on mount - if (!isHost && !callActive) { - await init(); - } - //host - if ( - isHost && - ($config.AUTO_CONNECT_RTM || (!$config.AUTO_CONNECT_RTM && callActive)) - ) { - await init(); - } - } else { - //non waiting room case - //host and attendee - if ( - $config.AUTO_CONNECT_RTM || - (!$config.AUTO_CONNECT_RTM && callActive) - ) { - await init(); + try { + if ($config.ENABLE_WAITING_ROOM) { + //attendee + //for waiting room attendee rtm login will happen on mount + if (!isHost && !callActive) { + await init(localUid); + } + //host + if ( + isHost && + ($config.AUTO_CONNECT_RTM || + (!$config.AUTO_CONNECT_RTM && callActive)) + ) { + await init(localUid); + } + } else { + //non waiting room case + //host and attendee + if ( + $config.AUTO_CONNECT_RTM || + (!$config.AUTO_CONNECT_RTM && callActive) + ) { + await init(localUid); + } } + } catch (error) { + logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); } return async () => { + logger.log( + LogSource.AgoraSDK, + 'Log', + 'RTM unmounting calling end(destroy) ', + ); await end(); }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [rtcProps.channel, rtcProps.appId, callActive]); + }, [rtcProps.channel, rtcProps.appId, callActive, localUid]); return ( { + const {showHeader = true} = props; + const isSmall = useIsSmall(); + const {currentLayout} = useLayout(); + const {transcriptHeight} = useCaptionWidth(); + const { + data: {isHost}, + } = useRoomInfo(); + + const { + breakoutSessionId, + checkIfBreakoutRoomSessionExistsAPI, + createBreakoutRoomGroup, + breakoutGroups, + startBreakoutRoomAPI, + closeBreakoutRoomAPI, + } = useBreakoutRoom(); + + useEffect(() => { + const init = async () => { + try { + const activeSession = await checkIfBreakoutRoomSessionExistsAPI(); + if (!activeSession) { + startBreakoutRoomAPI(); + } + } catch (error) { + console.error('Failed to check breakout session:', error); + } + }; + init(); + }, []); + + return ( + + {showHeader && } + + + + + createBreakoutRoomGroup()} + /> + + + {isHost && breakoutSessionId ? ( + + + { + closeBreakoutRoomAPI(); + }} + text={'Close All Rooms'} + /> + + + ) : ( + <> + )} + + ); +}; + +const style = StyleSheet.create({ + footer: { + width: '100%', + padding: 12, + height: 'auto', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: $config.CARD_LAYER_2_COLOR, + }, + pannelOuterBody: { + display: 'flex', + flex: 1, + }, + panelInnerBody: { + display: 'flex', + flex: 1, + padding: 12, + gap: 12, + }, + fullWidth: { + display: 'flex', + flex: 1, + }, + createBtnContainer: { + backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR, + borderColor: $config.INPUT_FIELD_BORDER_COLOR, + borderRadius: 8, + }, + createBtnText: { + color: $config.PRIMARY_ACTION_BRAND_COLOR, + lineHeight: 20, + fontWeight: '500', + fontSize: ThemeConfig.FontSize.normal, + }, +}); + +export default BreakoutRoomPanel; diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx new file mode 100644 index 000000000..3aa8530b1 --- /dev/null +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -0,0 +1,278 @@ +import React, {useContext, useReducer, useEffect} from 'react'; +import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; +import {createHook} from 'customization-implementation'; +import {randomNameGenerator} from '../../../utils'; +import StorageContext from '../../StorageContext'; +import getUniqueID from '../../../utils/getUniqueID'; +import {logger} from '../../../logger/AppBuilderLogger'; +import {useRoomInfo} from 'customization-api'; +import { + BreakoutGroupActionTypes, + BreakoutGroup, + BreakoutRoomState, + breakoutRoomReducer, + initialBreakoutRoomState, + RoomAssignmentStrategy, +} from '../state/reducer'; +import {useLocalUid} from '../../../../agora-rn-uikit'; +import {useContent} from '../../../../customization-api'; + +const getSanitizedPayload = (payload: BreakoutGroup[]) => { + return payload.map(({id, ...rest}) => { + if (typeof id === 'string' && id.startsWith('temp')) { + return rest; + } + return id !== undefined ? {...rest, id} : rest; + }); +}; + +interface BreakoutRoomContextValue { + breakoutSessionId: BreakoutRoomState['breakoutSessionId']; + breakoutGroups: BreakoutRoomState['breakoutGroups']; + assignmentStrategy: RoomAssignmentStrategy; + setStrategy: (strategy: RoomAssignmentStrategy) => void; + unsassignedParticipants: {uid: UidType; user: ContentInterface}[]; + createBreakoutRoomGroup: (name?: string) => void; + addUserIntoGroup: ( + uid: UidType, + selectGroupId: string, + isHost: boolean, + ) => void; + startBreakoutRoomAPI: () => void; + closeBreakoutRoomAPI: () => void; + checkIfBreakoutRoomSessionExistsAPI: () => Promise; + assignParticipants: () => void; +} + +const BreakoutRoomContext = React.createContext({ + breakoutSessionId: undefined, + unsassignedParticipants: [], + breakoutGroups: [], + assignmentStrategy: RoomAssignmentStrategy.NO_ASSIGN, + setStrategy: () => {}, + assignParticipants: () => {}, + createBreakoutRoomGroup: () => {}, + addUserIntoGroup: () => {}, + startBreakoutRoomAPI: () => {}, + closeBreakoutRoomAPI: () => {}, + checkIfBreakoutRoomSessionExistsAPI: async () => false, +}); + +const BreakoutRoomProvider = ({children}: {children: React.ReactNode}) => { + const {store} = useContext(StorageContext); + const {defaultContent, activeUids} = useContent(); + const localUid = useLocalUid(); + const [state, dispatch] = useReducer( + breakoutRoomReducer, + initialBreakoutRoomState, + ); + const { + data: {roomId}, + } = useRoomInfo(); + + // Update unassigned participants whenever defaultContent or activeUids change + useEffect(() => { + // Get currently assigned participants from all rooms + // Filter active UIDs to exclude: + // 1. Custom content (not type 'rtc') + // 2. Screenshare UIDs + // 3. Offline users + const filteredParticipants = activeUids + .filter(uid => { + const user = defaultContent[uid]; + if (!user) { + return false; + } + // Only include RTC users + if (user.type !== 'rtc') { + return false; + } + // Exclude offline users + if (user.offline) { + return false; + } + // Exclude screenshare UIDs (they typically have a parentUid) + if (user.parentUid) { + return false; + } + return true; + }) + .map(uid => ({ + uid, + user: defaultContent[uid], + })); + + // Sort participants with local user first + const sortedParticipants = filteredParticipants.sort((a, b) => { + if (a.uid === localUid) { + return -1; + } + if (b.uid === localUid) { + return 1; + } + return 0; + }); + + dispatch({ + type: BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS, + payload: { + unassignedParticipants: sortedParticipants, + }, + }); + }, [defaultContent, activeUids, localUid]); + + const checkIfBreakoutRoomSessionExistsAPI = async (): Promise => { + try { + const requestId = getUniqueID(); + const response = await fetch( + `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room?passphrase=${roomId.host}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + authorization: store.token ? `Bearer ${store.token}` : '', + 'X-Request-Id': requestId, + 'X-Session-Id': logger.getSessionId(), + }, + }, + ); + + if (response.status === 204) { + // No active breakout session + console.log('No active breakout room session (204)'); + return false; + } + + if (!response.ok) { + throw new Error(`Failed with status ${response.status}`); + } + + const data = await response.json(); + + if (data?.session_id) { + dispatch({ + type: BreakoutGroupActionTypes.SET_SESSION_ID, + payload: {sessionId: data.session_id}, + }); + + if (data?.breakout_room) { + dispatch({ + type: BreakoutGroupActionTypes.SET_GROUPS, + payload: data.breakout_room, + }); + } + return true; + } + + return false; + } catch (error) { + console.error('Error checking active breakout room:', error); + return false; + } + }; + + const startBreakoutRoomAPI = () => { + const startReqTs = Date.now(); + const requestId = getUniqueID(); + + fetch(`${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: store.token ? `Bearer ${store.token}` : '', + 'X-Request-Id': requestId, + 'X-Session-Id': logger.getSessionId(), + }, + body: JSON.stringify({ + passphrase: roomId.host, + switch_room: false, + session_id: state.breakoutSessionId || randomNameGenerator(6), + breakout_room: getSanitizedPayload(state.breakoutGroups), + }), + }) + .then(async response => { + const endRequestTs = Date.now(); + const latency = endRequestTs - startReqTs; + if (!response.ok) { + const msg = await response.text(); + throw new Error(`Breakout room creation failed: ${msg}`); + } else { + const data = await response.json(); + console.log('supriya res', response); + + if (data?.session_id) { + dispatch({ + type: BreakoutGroupActionTypes.SET_SESSION_ID, + payload: {sessionId: data.session_id}, + }); + + if (data?.breakout_room) { + dispatch({ + type: BreakoutGroupActionTypes.SET_GROUPS, + payload: data.breakout_room, + }); + } + } + } + }) + .catch(err => { + console.log('debugging err', err); + }); + }; + + const closeBreakoutRoomAPI = () => { + console.log('supriya close breakout room API not yet implemented'); + }; + + const setStrategy = (strategy: RoomAssignmentStrategy) => { + dispatch({ + type: BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY, + payload: {strategy}, + }); + }; + const createBreakoutRoomGroup = () => { + dispatch({ + type: BreakoutGroupActionTypes.CREATE_GROUP, + }); + }; + + const addUserIntoGroup = ( + uid: UidType, + toGroupId: string, + isHost: boolean, + ) => { + dispatch({ + type: BreakoutGroupActionTypes.MOVE_PARTICIPANT, + payload: {uid, fromGroupId: null, toGroupId, isHost}, + }); + }; + + const assignParticipants = () => { + dispatch({ + type: BreakoutGroupActionTypes.ASSIGN_PARTICPANTS, + }); + }; + + return ( + + {children} + + ); +}; + +const useBreakoutRoom = createHook(BreakoutRoomContext); + +export {useBreakoutRoom, BreakoutRoomProvider}; diff --git a/template/src/components/breakout-room/context/BreakoutRoomEngineContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomEngineContext.tsx new file mode 100644 index 000000000..7a723be0c --- /dev/null +++ b/template/src/components/breakout-room/context/BreakoutRoomEngineContext.tsx @@ -0,0 +1,118 @@ +import React, {createContext, useCallback, useRef} from 'react'; +import RtcEngine, {createAgoraRtcEngine} from '../../../../bridge/rtc/webNg'; +import {ChannelProfileType} from '../../../../agora-rn-uikit'; +import {ConnectionState} from 'agora-rtc-sdk-ng'; +import {RtcConnection} from 'react-native-agora'; +import {createHook} from 'customization-implementation'; +import {BreakoutGroupActionTypes} from '../state/reducer'; +import { + useBreakoutRoomDispatch, + useBreakoutRoomState, +} from './BreakoutRoomStateContext'; + +// Context +const BreakoutRoomEngineContext = createContext<{ + joinRtcChannel: ( + roomId: string, + config: { + token: string; + channelName: string; + optionalUid: number; + }, + ) => Promise; + leaveRtcChannel: () => Promise; +} | null>(null); + +// Provider +const BreakoutRoomEngineProvider: React.FC<{ + children: React.ReactNode; +}> = ({children}) => { + const state = useBreakoutRoomState(); + const dispatch = useBreakoutRoomDispatch(); + + const breakoutEngineRf = useRef(null); + + const onBreakoutRoomChannelStateChanged = useCallback( + (_connection: RtcConnection, currState: ConnectionState) => { + dispatch({ + type: BreakoutGroupActionTypes.ENGINE_SET_CHANNEL_STATUS, + payload: {status: currState}, + }); + }, + [], + ); + + const joinRtcChannel = useCallback( + async ( + roomId: string, + { + token, + channelName, + optionalUid = null, + }: { + token: string; + channelName: string; + optionalUid?: number | null; + }, + ) => { + let appId = $config.APP_ID; + let channelProfile = ChannelProfileType.ChannelProfileLiveBroadcasting; + if (!breakoutEngineRf.current) { + let engine = createAgoraRtcEngine(); + engine.addListener( + 'onConnectionStateChanged', + onBreakoutRoomChannelStateChanged, + ); + breakoutEngineRf.current = engine; // ✅ set ref + + dispatch({ + type: BreakoutGroupActionTypes.ENGINE_INIT, + payload: {engine}, + }); + // Add listeners here + } + console.log('supriya 3'); + try { + // Initialize RtcEngine + await breakoutEngineRf.current.initialize({appId}); + await breakoutEngineRf.current.setChannelProfile(channelProfile); + // Join RtcChannel + await breakoutEngineRf.current.joinChannel( + token, + channelName, + optionalUid, + {}, + ); + } catch (e) { + console.error(`[${roomId}] Failed to join channel`, e); + throw e; + } + }, + [dispatch, onBreakoutRoomChannelStateChanged], + ); + + const leaveRtcChannel = useCallback(async () => { + if (state.breakoutGroupRtc.engine) { + await state.breakoutGroupRtc.engine.leaveChannel(); + await state.breakoutGroupRtc.engine.release(); + dispatch({ + type: BreakoutGroupActionTypes.ENGINE_LEAVE_AND_DESTROY, + }); + } + }, [dispatch, state.breakoutGroupRtc.engine]); + + const value = { + joinRtcChannel, + leaveRtcChannel, + }; + + return ( + + {children} + + ); +}; + +const useBreakoutRoomEngine = createHook(BreakoutRoomEngineContext); + +export {useBreakoutRoomEngine, BreakoutRoomEngineProvider}; diff --git a/template/src/components/breakout-room/context/BreakoutRoomStateContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomStateContext.tsx new file mode 100644 index 000000000..5f07d5273 --- /dev/null +++ b/template/src/components/breakout-room/context/BreakoutRoomStateContext.tsx @@ -0,0 +1,39 @@ +import React, {createContext, useReducer} from 'react'; +import { + breakoutRoomReducer, + initialBreakoutRoomState, + BreakoutRoomAction, + BreakoutRoomState, +} from '../state/reducer'; +import {createHook} from 'customization-implementation'; + +export const BreakoutRoomStateContext = createContext< + BreakoutRoomState | undefined +>(undefined); +export const BreakoutRoomDispatchContext = createContext< + React.Dispatch | undefined +>(undefined); + +const BreakoutRoomStateProvider = ({children}: {children: React.ReactNode}) => { + const [state, dispatch] = useReducer( + breakoutRoomReducer, + initialBreakoutRoomState, + ); + + return ( + + + {children} + + + ); +}; + +const useBreakoutRoomState = createHook(BreakoutRoomStateContext); +const useBreakoutRoomDispatch = createHook(BreakoutRoomDispatchContext); + +export { + useBreakoutRoomState, + useBreakoutRoomDispatch, + BreakoutRoomStateProvider, +}; diff --git a/template/src/components/breakout-room/state/reducer.ts b/template/src/components/breakout-room/state/reducer.ts new file mode 100644 index 000000000..8cd71baa5 --- /dev/null +++ b/template/src/components/breakout-room/state/reducer.ts @@ -0,0 +1,246 @@ +import {ContentInterface, UidType} from '../../../../agora-rn-uikit/src'; +import {BreakoutChannelJoinEventPayload} from '../state/types'; +import {randomNameGenerator} from '../../../utils'; + +export enum RoomAssignmentStrategy { + AUTO_ASSIGN = 'auto-assign', + MANUAL_ASSIGN = 'manual-assign', + NO_ASSIGN = 'no-assign', +} + +export interface BreakoutGroup { + id: string; + name: string; + participants: { + hosts: UidType[]; + attendees: UidType[]; + }; +} +export interface BreakoutRoomState { + breakoutSessionId: string; + breakoutGroups: BreakoutGroup[]; + unassignedParticipants: {uid: UidType; user: ContentInterface}[]; + assignmentStrategy: RoomAssignmentStrategy; + activeBreakoutGroup: { + id: number | string; + name: string; + channelInfo: BreakoutChannelJoinEventPayload['data']['data']; + }; +} + +export const initialBreakoutRoomState: BreakoutRoomState = { + breakoutSessionId: '', + assignmentStrategy: RoomAssignmentStrategy.AUTO_ASSIGN, + unassignedParticipants: [], + breakoutGroups: [ + { + name: 'Room 1', + id: `temp_${randomNameGenerator(6)}`, + participants: {hosts: [], attendees: []}, + }, + { + name: 'Room 2', + id: `temp_${randomNameGenerator(6)}`, + participants: {hosts: [], attendees: []}, + }, + ], + activeBreakoutGroup: { + id: undefined, + name: '', + channelInfo: undefined, + }, +}; + +export const BreakoutGroupActionTypes = { + // session + SET_SESSION_ID: 'BREAKOUT_ROOM/SET_SESSION_ID', + // strategy + SET_ASSIGNMENT_STRATEGY: 'BREAKOUT_ROOM/SET_ASSIGNMENT_STRATEGY', + // Group management + SET_GROUPS: 'BREAKOUT_ROOM/SET_GROUPS', + CREATE_GROUP: 'BREAKOUT_ROOM/CREATE_GROUP', + // Participants Assignment + UPDATE_UNASSIGNED_PARTICIPANTS: + 'BREAKOUT_ROOM/UPDATE_UNASSIGNED_PARTICIPANTS', + ASSIGN_PARTICPANTS: 'BREAKOUT_ROOM/ASSIGN_PARTICPANTS', + MOVE_PARTICIPANT: 'BREAKOUT_ROOM/MOVE_PARTICIPANT', +} as const; + +export type BreakoutRoomAction = + | { + type: typeof BreakoutGroupActionTypes.SET_SESSION_ID; + payload: {sessionId: string}; + } + | { + type: typeof BreakoutGroupActionTypes.SET_GROUPS; + payload: BreakoutGroup[]; + } + | { + type: typeof BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY; + payload: { + strategy: RoomAssignmentStrategy; + }; + } + | {type: typeof BreakoutGroupActionTypes.CREATE_GROUP} + | { + type: typeof BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS; + payload: { + unassignedParticipants: {uid: UidType; user: ContentInterface}[]; + }; + } + | { + type: typeof BreakoutGroupActionTypes.ASSIGN_PARTICPANTS; + } + | { + type: typeof BreakoutGroupActionTypes.MOVE_PARTICIPANT; + payload: { + uid: UidType; + fromGroupId: string; + toGroupId: string; + isHost: boolean; + }; + }; + +export const breakoutRoomReducer = ( + state: BreakoutRoomState, + action: BreakoutRoomAction, +): BreakoutRoomState => { + switch (action.type) { + // group management cases + case BreakoutGroupActionTypes.SET_SESSION_ID: { + return {...state, breakoutSessionId: action.payload.sessionId}; + } + + case BreakoutGroupActionTypes.SET_GROUPS: { + return { + ...state, + breakoutGroups: action.payload.map(group => ({ + ...group, + participants: { + hosts: group.participants?.hosts ?? [], + attendees: group.participants?.attendees ?? [], + }, + })), + }; + } + + case BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS: { + return { + ...state, + unassignedParticipants: action.payload.unassignedParticipants || [], + }; + } + + case BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY: { + return { + ...state, + assignmentStrategy: action.payload.strategy, + }; + } + + case BreakoutGroupActionTypes.ASSIGN_PARTICPANTS: { + const selectedStrategy = state.assignmentStrategy; + const roomAssignments = new Map(); + + // Initialize empty arrays for each room + state.breakoutGroups.forEach(room => { + roomAssignments.set(room.id, []); + }); + + let assignedParticipantUids: UidType[] = []; + // AUTO ASSIGN Simple round-robin assignment (no capacity limits) + if (selectedStrategy === RoomAssignmentStrategy.AUTO_ASSIGN) { + let roomIndex = 0; + const roomIds = state.breakoutGroups.map(room => room.id); + state.unassignedParticipants.forEach(participant => { + const currentRoomId = roomIds[roomIndex]; + // Assign participant to current room + roomAssignments.get(currentRoomId)!.push(participant.uid); + // Move it to assigned list + assignedParticipantUids.push(participant.uid); + // Move to next room for round-robin + roomIndex = (roomIndex + 1) % roomIds.length; + }); + } + // Update breakoutGroups with new assignments + const updatedBreakoutGroups = state.breakoutGroups.map(group => { + const roomParticipants = roomAssignments.get(group.id) || []; + return { + ...group, + participants: { + hosts: [], + attendees: roomParticipants || [], + }, + }; + }); + + // Remove assigned participants from unassignedParticipants + const updatedUnassignedParticipants = state.unassignedParticipants.filter( + participant => !assignedParticipantUids.includes(participant.uid), + ); + + return { + ...state, + unassignedParticipants: updatedUnassignedParticipants, + breakoutGroups: updatedBreakoutGroups, + }; + } + + case BreakoutGroupActionTypes.CREATE_GROUP: { + return { + ...state, + breakoutGroups: [ + ...state.breakoutGroups, + { + name: `Room ${state.breakoutGroups.length + 1}`, + id: `temp_${randomNameGenerator(6)}`, + participants: {hosts: [], attendees: []}, + }, + ], + }; + } + + case BreakoutGroupActionTypes.MOVE_PARTICIPANT: { + const {uid, fromGroupId, toGroupId, isHost} = action.payload; + return { + ...state, + breakoutGroups: state.breakoutGroups.map(group => { + // Remove from source group (if fromGroupId exists) + if (fromGroupId && group.id === fromGroupId) { + return { + ...group, + participants: { + ...group.participants, + hosts: isHost + ? group.participants.hosts.filter(id => id !== uid) + : group.participants.hosts, + attendees: !isHost + ? group.participants.attendees.filter(id => id !== uid) + : group.participants.attendees, + }, + }; + } + // Add to target group + if (group.id === toGroupId) { + return { + ...group, + participants: { + ...group.participants, + hosts: isHost + ? [...group.participants.hosts, uid] + : group.participants.hosts, + attendees: !isHost + ? [...group.participants.attendees, uid] + : group.participants.attendees, + }, + }; + } + return group; + }), + }; + } + + default: + return state; + } +}; diff --git a/template/src/components/breakout-room/state/types.ts b/template/src/components/breakout-room/state/types.ts new file mode 100644 index 000000000..6049b5ac1 --- /dev/null +++ b/template/src/components/breakout-room/state/types.ts @@ -0,0 +1,62 @@ +import {BreakoutGroup} from './reducer'; + +export type BreakoutGroupAssignStrategy = 'auto' | 'manual' | 'self-select'; + +export interface AssignOption { + label: string; + value: BreakoutGroupAssignStrategy; + description: string; +} + +export interface BreakoutChannelJoinEventPayload { + data: { + data: { + room_id: number; + room_name: string; + channel_name: string; + mainUser: { + rtc: string; + uid: number; + rtm: string; + }; + screenShare: { + rtc: string; + uid: number; + }; + chat: { + isGroupOwner: boolean; + groupId: string; + userToken: string; + }; + }; + act: 'CHAN_JOIN'; // e.g., "CHAN_JOIN" + }; +} + +export interface BreakoutRoomStateEventPayload { + data: { + data: { + switch_room: boolean; + session_id: string; + breakout_room: BreakoutGroup; + }; + act: 'SYNC_STATE'; // e.g., "CHAN_JOIN" + }; +} +// | {type: 'DELETE_GROUP'; payload: {groupId: string}} +// | { +// type: 'ADD_PARTICIPANT'; +// payload: {uid: UidType; groupId: string; isHost: boolean}; +// } +// | { +// type: 'MOVE_PARTICIPANT'; +// payload: { +// uid: UidType; +// fromGroupId: string; +// toGroupId: string; +// isHost: boolean; +// }; +// } +// | {type: 'RESET_ALL_PARTICIPANTS'} +// | {type: 'SET_GROUPS'; payload: BreakoutRoomInfo[]} +// | {type: 'RESET_ALL'}; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx new file mode 100644 index 000000000..3090394ae --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -0,0 +1,311 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useState} from 'react'; +import {View, Text, StyleSheet, TouchableOpacity} from 'react-native'; +import IconButton from '../../../atoms/IconButton'; +import ThemeConfig from '../../../theme'; +import {UidType} from 'agora-rn-uikit'; +import UserAvatar from '../../../atoms/UserAvatar'; +import {BreakoutGroup} from '../state/reducer'; +import {useContent} from 'customization-api'; +import {videoRoomUserFallbackText} from '../../../language/default-labels/videoCallScreenLabels'; +import {useString} from '../../../utils/useString'; + +interface Props { + groups: BreakoutGroup[]; +} +const BreakoutRoomGroupSettings: React.FC = ({groups}) => { + // Render room card + const {defaultContent} = useContent(); + const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)(); + + const getName = (uid: UidType) => { + return defaultContent[uid]?.name || remoteUserDefaultLabel; + }; + + const [expandedRooms, setExpandedRooms] = useState>(new Set()); + + const toggleRoomExpansion = (roomId: string) => { + const newExpanded = new Set(expandedRooms); + if (newExpanded.has(roomId)) { + newExpanded.delete(roomId); + } else { + newExpanded.add(roomId); + } + setExpandedRooms(newExpanded); + }; + + const renderMember = (memberUId: UidType) => ( + + + + + {getName(memberUId)} + + + + {}}> + + + + ); + + const renderRoom = (room: BreakoutGroup) => { + const isExpanded = expandedRooms.has(room.id); + const memberCount = room.participants.attendees.length || 0; + + return ( + + + + toggleRoomExpansion(room.id)} + /> + + {room.name} + + {memberCount > 0 ? memberCount : 'No'} Member + {memberCount !== 1 ? 's' : ''} + + + + {/* + {}} + /> + */} + + + {/* Room Members (Expanded) */} + {isExpanded && ( + + {room.participants.attendees.length > 0 ? ( + room.participants.attendees.map(member => renderMember(member)) + ) : ( + + + No members in this room + + + )} + + )} + + ); + }; + + return ( + + + All Rooms + {/* */} + + {groups.map(renderRoom)} + + ); +}; + +const styles = StyleSheet.create({ + container: { + display: 'flex', + flexDirection: 'column', + // border: '2px solid red', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingHorizontal: 20, + paddingVertical: 16, + // border: '1px solid yellow', + }, + headerTitle: { + fontWeight: '600', + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + fontFamily: ThemeConfig.FontFamily.sansPro, + }, + headerActions: { + flexDirection: 'row', + gap: 6, + alignItems: 'center', + }, + body: { + display: 'flex', + flexDirection: 'column', + gap: 12, + // border: '1px solid yellow', + }, + roomGroupCard: { + display: 'flex', + flexDirection: 'column', + borderWidth: 1, + borderColor: $config.CARD_LAYER_3_COLOR, + borderRadius: 8, + }, + roomHeader: { + display: 'flex', + flexDirection: 'row', + borderColor: $config.CARD_LAYER_3_COLOR, + backgroundColor: $config.CARD_LAYER_2_COLOR, + alignItems: 'center', + padding: 12, + borderTopLeftRadius: 8, + borderTopRightRadius: 8, + }, + roomHeaderLeft: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + }, + roomHeaderInfo: { + display: 'flex', + flexDirection: 'column', + gap: 4, + }, + roomName: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 14, + fontWeight: '600', + }, + roomMemberCount: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, + fontSize: ThemeConfig.FontSize.tiny, + lineHeight: 12, + fontWeight: '600', + }, + roomHeaderRight: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + roomMembers: { + paddingHorizontal: 8, + paddingVertical: 12, + display: 'flex', + gap: 4, + alignSelf: 'stretch', + backgroundColor: $config.CARD_LAYER_1_COLOR, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, + memberItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 8, + paddingRight: 16, + borderRadius: 9, + backgroundColor: $config.CARD_LAYER_3_COLOR, + minHeight: 40, + gap: 8, + }, + memberInfo: { + flexDirection: 'row', + alignItems: 'center', + flex: 1, + gap: 8, + }, + memberDragHandle: { + marginRight: 12, + width: 16, + alignItems: 'center', + }, + dragDots: { + width: 4, + height: 12, + borderRadius: 2, + }, + memberAvatar: { + width: 32, + height: 32, + borderRadius: 16, + alignItems: 'center', + justifyContent: 'center', + marginRight: 12, + }, + hostAvatar: {}, + memberInitial: { + fontSize: 14, + fontWeight: '600', + }, + memberName: { + flex: 1, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 20, + fontWeight: '400', + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + }, + memberMenu: { + padding: 8, + }, + memberMenuText: { + fontSize: 16, + }, + emptyRoom: { + alignItems: 'center', + paddingVertical: 16, + }, + emptyRoomText: { + fontSize: ThemeConfig.FontSize.small, + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, + fontStyle: 'italic', + }, + userAvatarContainer: { + backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR, + width: 24, + height: 24, + borderRadius: 12, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + userAvatarText: { + fontSize: ThemeConfig.FontSize.tiny, + lineHeight: 12, + fontWeight: '600', + color: $config.BACKGROUND_COLOR, + }, + expandIcon: { + width: 32, + height: 32, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + shadowColor: $config.HARD_CODED_BLACK_COLOR, + }, +}); + +export default BreakoutRoomGroupSettings; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx b/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx new file mode 100644 index 000000000..b973d5839 --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx @@ -0,0 +1,85 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React from 'react'; +import {View, Text, StyleSheet} from 'react-native'; +import UserAvatar from '../../../atoms/UserAvatar'; +import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; +import ThemeConfig from '../../../theme'; + +interface Props { + participants?: {uid: UidType; user: ContentInterface}[]; +} + +const BreakoutRoomParticipants: React.FC = ({participants}) => { + return ( + <> + + Main Room ({participants.length} unassigned) + + + {participants.length > 0 ? ( + participants.map(item => ( + + + + )) + ) : ( + + No participants available for breakout rooms + + )} + + + ); +}; + +const styles = StyleSheet.create({ + title: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + fontWeight: '500', + opacity: 0.2, + }, + participantContainer: { + display: 'flex', + flexDirection: 'row', + gap: 5, + }, + participantItem: {}, + userAvatarContainer: { + backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR, + width: 24, + height: 24, + borderRadius: 12, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + userAvatarText: { + fontSize: ThemeConfig.FontSize.tiny, + lineHeight: 12, + fontWeight: '600', + color: $config.BACKGROUND_COLOR, + }, + emptyStateText: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, + fontSize: ThemeConfig.FontSize.tiny, + }, +}); + +export default BreakoutRoomParticipants; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx new file mode 100644 index 000000000..2eaa3c430 --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import {Text, View, StyleSheet} from 'react-native'; +import BreakoutRoomParticipants from './BreakoutRoomParticipants'; +import SelectParticipantAssignmentStrategy from './SelectParticipantAssignmentStrategy'; +import Divider from '../../common/Dividers'; +import Toggle from '../../../atoms/Toggle'; +import ThemeConfig from '../../../theme'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; + +export default function BreakoutRoomSettings() { + const { + unsassignedParticipants, + assignmentStrategy, + setStrategy, + assignParticipants, + } = useBreakoutRoom(); + + const disableAssignment = unsassignedParticipants.length === 0; + + return ( + + {/* Avatar list */} + + + + + + + + + {/* + + Allow people to switch rooms + {}} + circleColor={$config.FONT_COLOR} + /> + + */} + + ); +} + +const style = StyleSheet.create({ + card: { + width: '100%', + padding: 16, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 8, + backgroundColor: $config.CARD_LAYER_2_COLOR, + borderColor: $config.CARD_LAYER_3_COLOR, + gap: 12, + }, + section: { + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: 12, + }, + switchSection: { + width: '100%', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + label: { + fontWeight: '400', + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + color: $config.FONT_COLOR, + fontFamily: ThemeConfig.FontFamily.sansPro, + }, +}); diff --git a/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx b/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx new file mode 100644 index 000000000..23878afb3 --- /dev/null +++ b/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import {Text, StyleSheet} from 'react-native'; +import {Dropdown} from 'customization-api'; +import ThemeConfig from '../../../theme'; +import TertiaryButton from '../../../atoms/TertiaryButton'; +import {RoomAssignmentStrategy} from '../state/reducer'; + +interface Props { + assignParticipants: () => void; + selectedStrategy: RoomAssignmentStrategy; + onStrategyChange: (strategy: RoomAssignmentStrategy) => void; + disabled: boolean; +} + +const strategyList = [ + { + label: 'Auto-assign people to all rooms', + value: RoomAssignmentStrategy.AUTO_ASSIGN, + }, + { + label: 'Manually Assign participants', + value: RoomAssignmentStrategy.MANUAL_ASSIGN, + }, + { + label: 'Let people choose their rooms', + value: RoomAssignmentStrategy.NO_ASSIGN, + }, +]; +const SelectParticipantAssignmentStrategy: React.FC = ({ + selectedStrategy, + onStrategyChange, + disabled = false, + assignParticipants, +}) => { + return ( + <> + Assign participants to breakout rooms + { + onStrategyChange(value as RoomAssignmentStrategy); + }} + /> + { + assignParticipants(); + }} + text={'Assign participants'} + /> + + ); +}; + +const style = StyleSheet.create({ + label: { + fontWeight: '400', + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + color: $config.FONT_COLOR, + fontFamily: ThemeConfig.FontFamily.sansPro, + }, +}); +export default SelectParticipantAssignmentStrategy; diff --git a/template/src/components/common/Dividers.tsx b/template/src/components/common/Dividers.tsx new file mode 100644 index 000000000..185931ea8 --- /dev/null +++ b/template/src/components/common/Dividers.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import {View, StyleSheet} from 'react-native'; +import ThemeConfig from '../../theme'; + +interface DividerProps { + orientation?: 'horizontal' | 'vertical'; + marginTop?: number; + marginBottom?: number; + marginLeft?: number; + marginRight?: number; + thickness?: number; + color?: string; + length?: number | string; // only for vertical dividers +} + +const Divider: React.FC = ({ + orientation = 'horizontal', + marginTop = 8, + marginBottom = 8, + marginLeft = 0, + marginRight = 0, + thickness = 1, + color = $config.CARD_LAYER_4_COLOR, + length = '100%', +}) => { + const isHorizontal = orientation === 'horizontal'; + + const style = isHorizontal + ? { + height: thickness, + width: '100%', + backgroundColor: color, + marginTop, + marginBottom, + } + : { + width: thickness, + height: length, + backgroundColor: color, + marginLeft, + marginRight, + }; + + return ; +}; + +const styles = StyleSheet.create({ + base: { + backgroundColor: $config.CARD_LAYER_4_COLOR, + }, +}); + +export default Divider; diff --git a/template/src/components/controls/useControlPermissionMatrix.tsx b/template/src/components/controls/useControlPermissionMatrix.tsx index 2daa52cf0..49ac211e3 100644 --- a/template/src/components/controls/useControlPermissionMatrix.tsx +++ b/template/src/components/controls/useControlPermissionMatrix.tsx @@ -15,7 +15,8 @@ export type ControlPermissionKey = | 'participantControl' | 'screenshareControl' | 'settingsControl' - | 'viewAllTextTracks'; + | 'viewAllTextTracks' + | 'breakoutRoom'; /** * ControlPermissionRule defines the properties used to evaluate permission rules. @@ -42,6 +43,7 @@ export const controlPermissionMatrix: Record< $config.ENABLE_MEETING_TRANSCRIPT && $config.ENABLE_TEXT_TRACKS && isWeb(), + breakoutRoom: ({isHost}) => $config.ENABLE_BREAKOUT_ROOM, }; export const useControlPermissionMatrix = ( diff --git a/template/src/components/participants/AllHostParticipants.tsx b/template/src/components/participants/AllHostParticipants.tsx index 4d537d7d5..73d992d02 100644 --- a/template/src/components/participants/AllHostParticipants.tsx +++ b/template/src/components/participants/AllHostParticipants.tsx @@ -73,12 +73,14 @@ export default function AllHostParticipants(props: any) { isAudienceUser={false} name={getParticipantName(localUid)} user={defaultContent[localUid]} - showControls={true} + showControls={props?.hideControls ? false : true} isHostUser={hostUids.indexOf(localUid) !== -1} key={localUid} isMobile={isMobile} handleClose={handleClose} updateActionSheet={updateActionSheet} + showBreakoutRoomMenu={props?.showBreakoutRoomMenu} + from={props?.from} /> {renderScreenShare(defaultContent[localUid])} @@ -104,12 +106,18 @@ export default function AllHostParticipants(props: any) { isAudienceUser={false} name={getParticipantName(uid)} user={defaultContent[uid]} - showControls={defaultContent[uid]?.type === 'rtc' && isHost} + showControls={ + defaultContent[uid]?.type === 'rtc' && + isHost && + (props?.hideControls ? false : true) + } isHostUser={hostUids.indexOf(uid) !== -1} key={uid} isMobile={isMobile} handleClose={handleClose} updateActionSheet={updateActionSheet} + showBreakoutRoomMenu={props?.showBreakoutRoomMenu} + from={props?.from} /> {renderScreenShare(defaultContent[uid])} diff --git a/template/src/components/participants/Participant.tsx b/template/src/components/participants/Participant.tsx index 3414521da..1cf416880 100644 --- a/template/src/components/participants/Participant.tsx +++ b/template/src/components/participants/Participant.tsx @@ -66,6 +66,12 @@ interface ParticipantInterface { updateActionSheet: (screenName: 'chat' | 'participants' | 'settings') => {}; uid?: UidType; screenUid?: UidType; + showBreakoutRoomMenu?: boolean; + from?: + | 'breakout-room' + | 'partcipant' + | 'screenshare-participant' + | 'video-tile'; } const Participant = (props: ParticipantInterface) => { @@ -106,7 +112,7 @@ const Participant = (props: ParticipantInterface) => { setActionMenuVisible={setActionMenuVisible} user={props.user} btnRef={moreIconRef} - from={'partcipant'} + from={props?.from || 'partcipant'} spotlightUid={spotlightUid} setSpotlightUid={setSpotlightUid} /> diff --git a/template/src/components/participants/UserActionMenuOptions.tsx b/template/src/components/participants/UserActionMenuOptions.tsx index 1e3343661..dcdb26390 100644 --- a/template/src/components/participants/UserActionMenuOptions.tsx +++ b/template/src/components/participants/UserActionMenuOptions.tsx @@ -77,13 +77,18 @@ import { DEFAULT_ACTION_KEYS, UserActionMenuItemsConfig, } from '../../atoms/UserActionMenuPreset'; +import {useBreakoutRoom} from '../breakout-room/context/BreakoutRoomContext'; interface UserActionMenuOptionsOptionsProps { user: ContentInterface; actionMenuVisible: boolean; setActionMenuVisible: (actionMenuVisible: boolean) => void; btnRef: any; - from: 'partcipant' | 'screenshare-participant' | 'video-tile'; + from: + | 'partcipant' + | 'screenshare-participant' + | 'video-tile' + | 'breakout-room'; spotlightUid?: UidType; setSpotlightUid?: (uid: UidType) => void; items?: UserActionMenuItemsConfig; @@ -102,7 +107,8 @@ export default function UserActionMenuOptionsOptions( useState(false); const [actionMenuitems, setActionMenuitems] = useState([]); const {setSidePanel} = useSidePanel(); - const {user, actionMenuVisible, setActionMenuVisible, spotlightUid} = props; + const {user, actionMenuVisible, setActionMenuVisible, spotlightUid, from} = + props; const {currentLayout} = useLayout(); const {pinnedUid, activeUids, customContent, secondaryPinnedUid} = useContent(); @@ -146,7 +152,7 @@ export default function UserActionMenuOptionsOptions( const moreBtnSpotlightLabel = useString(moreBtnSpotlight); const {chatConnectionStatus} = useChatUIControls(); const chatErrNotConnectedText = useString(chatErrorNotConnected)(); - + const {breakoutGroups, addUserIntoGroup} = useBreakoutRoom(); useEffect(() => { customEvents.on('DisableChat', data => { // for other users @@ -166,6 +172,27 @@ export default function UserActionMenuOptionsOptions( useEffect(() => { const items: ActionMenuItem[] = []; + if (from === 'breakout-room' && $config.ENABLE_BREAKOUT_ROOM) { + if (breakoutGroups && breakoutGroups?.length) { + breakoutGroups.map(({name, id}, index) => { + items.push({ + order: index + 1, + icon: 'add', + onHoverIcon: 'add', + iconColor: $config.SECONDARY_ACTION_COLOR, + textColor: $config.SECONDARY_ACTION_COLOR, + title: `Move to ${name}`, + onPress: () => { + setActionMenuVisible(false); + addUserIntoGroup(user.uid, id, false); + }, + }); + }); + setActionMenuitems(items); + } + return; + } + //Context of current user role const isSelf = user.uid === localuid; const isRemote = !isSelf; @@ -729,6 +756,8 @@ export default function UserActionMenuOptionsOptions( secondaryPinnedUid, currentLayout, spotlightUid, + from, + breakoutGroups, ]); const {width: globalWidth, height: globalHeight} = useWindowDimensions(); diff --git a/template/src/language/default-labels/videoCallScreenLabels.ts b/template/src/language/default-labels/videoCallScreenLabels.ts index fe1e96211..daf245daf 100644 --- a/template/src/language/default-labels/videoCallScreenLabels.ts +++ b/template/src/language/default-labels/videoCallScreenLabels.ts @@ -92,6 +92,7 @@ export const toolbarItemLayoutOptionGridText = export const toolbarItemLayoutOptionSidebarText = 'toolbarItemLayoutOptionSidebarText'; export const toolbarItemInviteText = 'toolbarItemInviteText'; +export const toolbarItemBreakoutRoomText = 'toolbarItemBreakoutRoomText'; export const toolbarItemMicrophoneText = 'toolbarItemMicrophoneText'; export const toolbarItemMicrophoneTooltipText = @@ -213,6 +214,7 @@ export const chatPanelPrivateTabText = 'chatPanelPrivateTabText'; export const groupChatWelcomeContent = 'groupChatWelcomeContent'; export const peoplePanelHeaderText = 'peoplePanelHeaderText'; +export const breakoutRoomPanelHeaderText = 'breakoutRoomPanelHeaderText'; export const groupChatMeetingInputPlaceHolderText = 'groupChatMeetingInputPlaceHolderText'; @@ -557,6 +559,8 @@ export interface I18nVideoCallScreenLabelsInterface { [toolbarItemLayoutText]?: I18nBaseType; [toolbarItemInviteText]?: I18nBaseType; + [toolbarItemBreakoutRoomText]?: I18nBaseType; + [toolbarItemMicrophoneText]?: I18nBaseType; [toolbarItemMicrophoneTooltipText]?: I18nBaseType; [toolbarItemCameraText]?: I18nBaseType; @@ -644,6 +648,7 @@ export interface I18nVideoCallScreenLabelsInterface { [sttLanguageChangeInProgress]?: I18nBaseType; [peoplePanelHeaderText]?: I18nBaseType; + [breakoutRoomPanelHeaderText]?: I18nBaseType; [chatPanelGroupTabText]?: I18nBaseType; [chatPanelPrivateTabText]?: I18nBaseType; @@ -879,6 +884,7 @@ export const VideoCallScreenLabels: I18nVideoCallScreenLabelsInterface = { [toolbarItemSettingText]: 'Settings', [toolbarItemLayoutText]: 'Layout', [toolbarItemInviteText]: 'Invite', + [toolbarItemBreakoutRoomText]: 'Create Breakout Rooms', [toolbarItemMicrophoneText]: deviceStatus => { switch (deviceStatus) { @@ -1044,6 +1050,7 @@ export const VideoCallScreenLabels: I18nVideoCallScreenLabelsInterface = { [sttLanguageChangeInProgress]: 'Language Change is in progress...', [peoplePanelHeaderText]: 'People', + [breakoutRoomPanelHeaderText]: 'Breakout Room', [chatPanelGroupTabText]: 'Public', [chatPanelPrivateTabText]: 'Private', diff --git a/template/src/logger/AppBuilderLogger.tsx b/template/src/logger/AppBuilderLogger.tsx index 98161371b..1f47bcaa0 100644 --- a/template/src/logger/AppBuilderLogger.tsx +++ b/template/src/logger/AppBuilderLogger.tsx @@ -105,7 +105,8 @@ type LogType = { | 'recording_stop' | 'recordings_get' | 'recording_delete' - | 'ban_user'; + | 'ban_user' + | 'breakout-room'; [LogSource.Events]: 'CUSTOM_EVENTS' | 'RTM_EVENTS'; [LogSource.CustomizationAPI]: | 'Log' diff --git a/template/src/pages/BreakoutRoomVideoCall.tsx b/template/src/pages/BreakoutRoomVideoCall.tsx new file mode 100644 index 000000000..273cd2d28 --- /dev/null +++ b/template/src/pages/BreakoutRoomVideoCall.tsx @@ -0,0 +1,239 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ +import React, {useState, useContext} from 'react'; +import {StyleSheet} from 'react-native'; +import { + RtcConfigure, + PropsProvider, + ClientRoleType, + ChannelProfileType, + LocalUserContext, +} from '../../agora-rn-uikit'; +import RtmConfigure from '../components/RTMConfigure'; +import DeviceConfigure from '../components/DeviceConfigure'; +import {LiveStreamContextProvider} from '../components/livestream'; +import ScreenshareConfigure from '../subComponents/screenshare/ScreenshareConfigure'; +import {isMobileUA} from '../utils/common'; +import {LayoutProvider} from '../utils/useLayout'; +import {RecordingProvider} from '../subComponents/recording/useRecording'; +import {SidePanelProvider} from '../utils/useSidePanel'; +import {NetworkQualityProvider} from '../components/NetworkQualityContext'; +import {ChatNotificationProvider} from '../components/chat-notification/useChatNotification'; +import {ChatUIControlsProvider} from '../components/chat-ui/useChatUIControls'; +import {ScreenShareProvider} from '../components/contexts/ScreenShareContext'; +import {LiveStreamDataProvider} from '../components/contexts/LiveStreamDataContext'; +import {VideoMeetingDataProvider} from '../components/contexts/VideoMeetingDataContext'; +import {UserPreferenceProvider} from '../components/useUserPreference'; +import EventsConfigure from '../components/EventsConfigure'; +import {FocusProvider} from '../utils/useFocus'; +import {VideoCallProvider} from '../components/useVideoCall'; +import {CaptionProvider} from '../subComponents/caption/useCaption'; +import SdkMuteToggleListener from '../components/SdkMuteToggleListener'; +import {NoiseSupressionProvider} from '../app-state/useNoiseSupression'; +import {VideoQualityContextProvider} from '../app-state/useVideoQuality'; +import {VBProvider} from '../components/virtual-background/useVB'; +import {DisableChatProvider} from '../components/disable-chat/useDisableChat'; +import {WaitingRoomProvider} from '../components/contexts/WaitingRoomContext'; +import PermissionHelper from '../components/precall/PermissionHelper'; +import {ChatMessagesProvider} from '../components/chat-messages/useChatMessages'; +import VideoCallScreenWrapper from './video-call/VideoCallScreenWrapper'; +import {BeautyEffectProvider} from '../components/beauty-effect/useBeautyEffects'; +import {UserActionMenuProvider} from '../components/useUserActionMenu'; +import {VideoRoomOrchestratorState} from './VideoCallRoomOrchestrator'; +import StorageContext from '../components/StorageContext'; +import {SdkApiContext} from '../components/SdkApiContext'; +import {RnEncryptionEnum} from './VideoCall'; +import VideoCallStateSetup from './video-call/VideoCallStateSetup'; +import {BreakoutRoomProvider} from '../components/breakout-room/context/BreakoutRoomContext'; + +interface BreakoutVideoCallProps { + setBreakoutRtcEngine?: ( + engine: VideoRoomOrchestratorState['rtcEngine'], + ) => void; + storedBreakoutChannelDetails: VideoRoomOrchestratorState['channelDetails']; +} + +const BreakoutRoomVideoCall = ( + breakoutVideoCallProps: BreakoutVideoCallProps, +) => { + const {storedBreakoutChannelDetails} = breakoutVideoCallProps; + const {store} = useContext(StorageContext); + const [isRecordingActive, setRecordingActive] = useState(false); + const [recordingAutoStarted, setRecordingAutoStarted] = useState(false); + const [sttAutoStarted, setSttAutoStarted] = useState(false); + + const { + join: SdkJoinState, + microphoneDevice: sdkMicrophoneDevice, + cameraDevice: sdkCameraDevice, + } = useContext(SdkApiContext); + + const callActive = true; + + const [rtcProps, setRtcProps] = React.useState({ + appId: $config.APP_ID, + channel: storedBreakoutChannelDetails.channel, + uid: storedBreakoutChannelDetails.uid as number, + token: storedBreakoutChannelDetails.token, + rtm: storedBreakoutChannelDetails.rtmToken, + screenShareUid: storedBreakoutChannelDetails?.screenShareUid as number, + screenShareToken: storedBreakoutChannelDetails?.screenShareToken || '', + profile: $config.PROFILE, + screenShareProfile: $config.SCREEN_SHARE_PROFILE, + dual: true, + encryption: $config.ENCRYPTION_ENABLED + ? {key: null, mode: RnEncryptionEnum.AES128GCM2, screenKey: null} + : false, + role: ClientRoleType.ClientRoleBroadcaster, + geoFencing: $config.GEO_FENCING, + audioRoom: $config.AUDIO_ROOM, + activeSpeaker: $config.ACTIVE_SPEAKER, + preferredCameraId: + sdkCameraDevice.deviceId || store?.activeDeviceId?.videoinput || null, + preferredMicrophoneId: + sdkMicrophoneDevice.deviceId || store?.activeDeviceId?.audioinput || null, + recordingBot: false, + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + {!isMobileUA() && ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const styleProps = StyleSheet.create({ + errorContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#1a1a1a', + padding: 20, + }, + errorTitle: { + color: 'white', + fontSize: 24, + fontWeight: 'bold', + marginBottom: 16, + }, + errorMessage: { + color: '#cccccc', + fontSize: 16, + textAlign: 'center', + marginBottom: 24, + }, + returnButton: { + minWidth: 200, + }, + loadingContainer: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#1a1a1a', + }, + loadingText: { + color: 'white', + fontSize: 18, + marginBottom: 20, + }, +}); + +export default BreakoutRoomVideoCall; diff --git a/template/src/pages/VideoCall.tsx b/template/src/pages/VideoCall.tsx index 39bee5268..81499e34d 100644 --- a/template/src/pages/VideoCall.tsx +++ b/template/src/pages/VideoCall.tsx @@ -10,7 +10,7 @@ ********************************************* */ // @ts-nocheck -import React, {useState, useContext, useEffect, useRef} from 'react'; +import React, {useState, useContext, useEffect, useRef, useMemo} from 'react'; import {View, StyleSheet, Text} from 'react-native'; import { RtcConfigure, @@ -81,8 +81,11 @@ import {BeautyEffectProvider} from '../components/beauty-effect/useBeautyEffects import {UserActionMenuProvider} from '../components/useUserActionMenu'; import Toast from '../../react-native-toast-message'; import {AuthErrorCodes} from '../utils/common'; +import {VideoRoomOrchestratorState} from './VideoCallRoomOrchestrator'; +import VideoCallStateSetup from './video-call/VideoCallStateSetup'; +import {BreakoutRoomProvider} from '../components/breakout-room/context/BreakoutRoomContext'; -enum RnEncryptionEnum { +export enum RnEncryptionEnum { /** * @deprecated * 0: This mode is deprecated. @@ -127,7 +130,16 @@ enum RnEncryptionEnum { AES256GCM2 = 8, } -const VideoCall: React.FC = () => { +export interface VideoCallProps { + setMainRtcEngine?: (engine: IRtcEngine) => void; + setMainChannelDetails?: ( + details: VideoRoomOrchestratorState['channelDetails'], + ) => void; + storedEngine?: IRtcEngine | null; + storedChannelDetails?: ChannelDetails; +} + +const VideoCall = (videoCallProps: VideoCallProps) => { const hasBrandLogo = useHasBrandLogo(); const joiningLoaderLabel = useString(videoRoomStartingCallText)(); const bannedUserText = useString(userBannedText)(); @@ -478,6 +490,7 @@ const VideoCall: React.FC = () => { : ChannelProfileType.ChannelProfileCommunication, }}> + @@ -527,35 +540,37 @@ const VideoCall: React.FC = () => { - - - {callActive ? ( - - - + }> */} + + {callActive ? ( + + + + - - - - ) : $config.PRECALL ? ( - - - - ) : ( - <> - )} - - + + + + + ) : $config.PRECALL ? ( + + + + ) : ( + <> + )} + + {/* */} diff --git a/template/src/pages/VideoCallRoomOrchestrator.tsx b/template/src/pages/VideoCallRoomOrchestrator.tsx new file mode 100644 index 000000000..ab3cb755c --- /dev/null +++ b/template/src/pages/VideoCallRoomOrchestrator.tsx @@ -0,0 +1,202 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useState, useEffect, useContext, useCallback} from 'react'; +import {IRtcEngine} from 'react-native-agora'; +import VideoCall from '../pages/VideoCall'; +import BreakoutRoomVideoCall from './BreakoutRoomVideoCall'; +import {useParams, useHistory, useLocation} from '../components/Router'; +import events from '../rtm-events-api'; +import {EventNames} from '../rtm-events'; +import {BreakoutChannelJoinEventPayload} from '../components/breakout-room/state/types'; + +export interface VideoRoomOrchestratorState { + rtcEngine: IRtcEngine | null; + channelDetails: { + channel: string; + uid: number | string; + token: string; + screenShareUid: number | string; + screenShareToken: string; + rtmToken: string; + }; +} + +const VideoCallRoomOrchestrator: React.FC = () => { + const {phrase} = useParams<{phrase: string}>(); + const history = useHistory(); + const location = useLocation(); + + // Parse query parameters from location.search + const searchParams = new URLSearchParams(location.search); + const callActive = searchParams.get('call') === 'true'; + const isBreakoutRoomActive = searchParams.get('breakout') === 'true'; + + // Main room state + const [mainRoomState, setMainRoomState] = + useState({ + rtcEngine: null, + channelDetails: { + channel: null, + uid: null, + token: null, + screenShareUid: null, + screenShareToken: null, + rtmToken: null, + }, + }); + + // Breakout room state + const [breakoutRoomState, setBreakoutRoomState] = + useState({ + rtcEngine: null, + channelDetails: { + channel: null, + uid: null, + token: null, + screenShareUid: null, + screenShareToken: null, + rtmToken: null, + }, + }); + + useEffect(() => { + const handleBreakoutJoin = evtData => { + const {payload, sender, ts, source} = evtData; + const data: BreakoutChannelJoinEventPayload = JSON.parse(payload); + console.log( + 'supriya [VideoCallRoomOrchestrator] onBreakoutRoomJoinDetailsReceived data: ', + data, + ); + const {channel_name, mainUser, screenShare, chat} = data.data.data; + + try { + setBreakoutRoomState(prev => ({ + ...prev, + channelDetails: { + channel: channel_name, + token: mainUser.rtc, + uid: mainUser?.uid || 0, + screenShareToken: screenShare.rtc, + screenShareUid: screenShare.uid, + rtmToken: mainUser.rtm, + }, + })); + // Navigate to breakout roo + history.push(`/${phrase}?call=true&breakout=true`); + } catch (error) { + console.log( + ' handleBreakoutJoin [VideoCallRoomOrchestrator] error: ', + error, + ); + } + }; + events.on(EventNames.BREAKOUT_ROOM_JOIN_DETAILS, handleBreakoutJoin); + return () => { + events.off(EventNames.BREAKOUT_ROOM_JOIN_DETAILS, handleBreakoutJoin); + }; + }, [history, phrase]); + + // // RTM listeners for breakout events + // useEffect(() => { + // const handleBreakoutLeave = () => { + // console.log( + // `[VideoCallRoomOrchestrator] Leaving breakout room, returning to main`, + // ); + + // // Return to main room + // history.push(`/${phrase}?call=true`); + // }; + + // // TODO: Implement RTM event listeners + // // RTMManager.on('BREAKOUT_JOIN', handleBreakoutJoin); + // // RTMManager.on('BREAKOUT_LEAVE', handleBreakoutLeave); + + // // For now, we'll expose these functions globally for testing + // if (typeof window !== 'undefined') { + // (window as any).joinBreakoutRoom = handleBreakoutJoin; + // (window as any).leaveBreakoutRoom = handleBreakoutLeave; + // } + + // return () => { + // // RTMManager.off('BREAKOUT_JOIN', handleBreakoutJoin); + // // RTMManager.off('BREAKOUT_LEAVE', handleBreakoutLeave); + + // if (typeof window !== 'undefined') { + // delete (window as any).joinBreakoutRoom; + // delete (window as any).leaveBreakoutRoom; + // } + // }; + // }, [phrase, history, roomInfo]); + + // Helper functions to update RTC engines + const setMainRtcEngine = useCallback((engine: IRtcEngine) => { + console.log('supriya [VideoCallRoomOrchestrator] Setting main RTC engine'); + setMainRoomState(prev => ({ + ...prev, + rtcEngine: engine, + })); + }, []); + + const setMainChannelDetails = useCallback( + (channelDetails: VideoRoomOrchestratorState['channelDetails']) => { + console.log( + 'supriya [VideoCallRoomOrchestrator] Setting main RTC engine', + ); + setMainRoomState(prev => ({ + ...prev, + channelDetails: {...channelDetails}, + })); + }, + [], + ); + + const setBreakoutRtcEngine = useCallback((engine: IRtcEngine) => { + console.log('[VideoCallRoomOrchestrator] Setting breakout RTC engine'); + setBreakoutRoomState(prev => ({ + ...prev, + rtcEngine: engine, + })); + }, []); + + // // Handle return to main room + // const handleReturnToMain = () => { + // console.log('[VideoCallRoomOrchestrator] Returning to main room'); + // history.push(`/${phrase}?call=true`); + // }; + + console.log('[VideoCallRoomOrchestrator] Rendering:', { + isBreakoutRoom: isBreakoutRoomActive, + callActive, + phrase, + mainChannel: {...mainRoomState}, + breakoutChannel: {...breakoutRoomState}, + }); + + return ( + <> + {isBreakoutRoomActive && breakoutRoomState?.channelDetails?.channel ? ( + + ) : ( + + )} + + ); +}; + +export default VideoCallRoomOrchestrator; diff --git a/template/src/pages/video-call/SidePanelHeader.tsx b/template/src/pages/video-call/SidePanelHeader.tsx index a173cdeac..d7f8a1626 100644 --- a/template/src/pages/video-call/SidePanelHeader.tsx +++ b/template/src/pages/video-call/SidePanelHeader.tsx @@ -38,6 +38,7 @@ import { vbPanelHeading, } from '../../language/default-labels/precallScreenLabels'; import { + breakoutRoomPanelHeaderText, chatPanelGroupTabText, chatPanelPrivateTabText, peoplePanelHeaderText, @@ -93,6 +94,22 @@ export const PeopleHeader = () => { ); }; +export const BreakoutRoomHeader = () => { + const headerText = useString(breakoutRoomPanelHeaderText)(); + const {setSidePanel} = useSidePanel(); + return ( + {headerText} + } + trailingIconName="close" + trailingIconOnPress={() => { + setSidePanel(SidePanelType.None); + }} + /> + ); +}; + export const ChatHeader = () => { const { unreadGroupMessageCount, diff --git a/template/src/pages/video-call/VideoCallScreen.tsx b/template/src/pages/video-call/VideoCallScreen.tsx index 4093cc44e..06321398b 100644 --- a/template/src/pages/video-call/VideoCallScreen.tsx +++ b/template/src/pages/video-call/VideoCallScreen.tsx @@ -55,6 +55,7 @@ import {useIsRecordingBot} from '../../subComponents/recording/useIsRecordingBot import {ToolbarPresetProps} from '../../atoms/ToolbarPreset'; import CustomSidePanelView from '../../components/CustomSidePanel'; import {useControlPermissionMatrix} from '../../components/controls/useControlPermissionMatrix'; +import BreakoutRoomPanel from '../../components/breakout-room/BreakoutRoomPanel'; const VideoCallScreen = () => { useFindActiveSpeaker(); @@ -73,6 +74,7 @@ const VideoCallScreen = () => { VideocallComponent, BottombarComponent, ParticipantsComponent, + BreakoutRoomComponent, TranscriptComponent, CaptionComponent, VirtualBackgroundComponent, @@ -99,6 +101,7 @@ const VideoCallScreen = () => { CaptionComponent: React.ComponentType; VirtualBackgroundComponent: React.ComponentType; SettingsComponent: React.ComponentType; + BreakoutRoomComponent: React.ComponentType; TopbarComponent: React.ComponentType; VideocallBeforeView: React.ComponentType; VideocallAfterView: React.ComponentType; @@ -118,6 +121,7 @@ const VideoCallScreen = () => { CaptionComponent: CaptionContainer, VirtualBackgroundComponent: VBPanel, SettingsComponent: SettingsView, + BreakoutRoomComponent: BreakoutRoomPanel, VideocallAfterView: React.Fragment, VideocallBeforeView: React.Fragment, VideocallWrapper: React.Fragment, @@ -236,6 +240,15 @@ const VideoCallScreen = () => { data?.components?.videoCall.participantsPanel; } + if ( + data?.components?.videoCall.breakoutRoomPanel && + typeof data?.components?.videoCall.breakoutRoomPanel !== 'object' && + isValidReactComponent(data?.components?.videoCall.breakoutRoomPanel) + ) { + components.BreakoutRoomComponent = + data?.components?.videoCall.breakoutRoomPanel; + } + if ( data?.components?.videoCall.transcriptPanel && typeof data?.components?.videoCall.transcriptPanel !== 'object' && @@ -428,6 +441,11 @@ const VideoCallScreen = () => { ) : ( <> )} + {sidePanel === SidePanelType.BreakoutRoom ? ( + + ) : ( + <> + )} {sidePanel === SidePanelType.Transcript ? ( $config.ENABLE_MEETING_TRANSCRIPT ? ( diff --git a/template/src/pages/video-call/VideoCallScreenWrapper.tsx b/template/src/pages/video-call/VideoCallScreenWrapper.tsx index c8322837e..c53e2d45b 100644 --- a/template/src/pages/video-call/VideoCallScreenWrapper.tsx +++ b/template/src/pages/video-call/VideoCallScreenWrapper.tsx @@ -1,7 +1,6 @@ import React, {useContext, useEffect} from 'react'; import {PropsContext} from '../../../agora-rn-uikit'; import VideoCallScreen from '../video-call/VideoCallScreen'; -import {isWebInternal} from '../../utils/common'; import {useLocation} from '../../components/Router'; import {getParamFromURL} from '../../utils/common'; import {useUserPreference} from '../../components/useUserPreference'; diff --git a/template/src/pages/video-call/VideoCallStateSetup.tsx b/template/src/pages/video-call/VideoCallStateSetup.tsx new file mode 100644 index 000000000..c17ec52ba --- /dev/null +++ b/template/src/pages/video-call/VideoCallStateSetup.tsx @@ -0,0 +1,47 @@ +import React, {useEffect} from 'react'; +import {useRoomInfo, useRtc} from 'customization-api'; +import {VideoCallProps} from '../VideoCall'; + +const VideoCallStateSetup: React.FC = ({ + setMainRtcEngine, + setMainChannelDetails, +}) => { + const {RtcEngineUnsafe} = useRtc(); + const {data: roomInfo} = useRoomInfo(); + + // Listen for engine changes and notify orchestrator + useEffect(() => { + if ($config.ENABLE_BREAKOUT_ROOM && RtcEngineUnsafe && setMainRtcEngine) { + console.log( + 'supriya [VideoCallStateSetup] Engine ready, storing in orchestrator', + ); + setMainRtcEngine(RtcEngineUnsafe); + } + }, [RtcEngineUnsafe, setMainRtcEngine]); + + // Listen for channel details and notify orchestrator + useEffect(() => { + if ( + $config.ENABLE_BREAKOUT_ROOM && + roomInfo?.channel && + setMainChannelDetails + ) { + console.log( + 'supriya [VideoCallStateSetup] Channel details ready, storing in orchestrator', + ); + setMainChannelDetails({ + channel: roomInfo.channel || '', + token: roomInfo.token || '', + uid: roomInfo.uid || 0, + screenShareToken: roomInfo.screenShareToken, + screenShareUid: roomInfo.screenShareUid, + rtmToken: roomInfo.rtmToken, + }); + } + }, [roomInfo, setMainChannelDetails]); + + // This component only handles effects, renders nothing + return null; +}; + +export default VideoCallStateSetup; diff --git a/template/src/rtm-events-api/Events.ts b/template/src/rtm-events-api/Events.ts index 90d0cdfa3..e6988ed0a 100644 --- a/template/src/rtm-events-api/Events.ts +++ b/template/src/rtm-events-api/Events.ts @@ -11,7 +11,7 @@ */ ('use strict'); -import RtmEngine from 'agora-react-native-rtm'; +import {type RTMClient} from 'agora-react-native-rtm'; import RTMEngine from '../rtm/RTMEngine'; import {EventUtils} from '../rtm-events'; import { @@ -23,6 +23,7 @@ import { } from './types'; import {adjustUID} from '../rtm/utils'; import {LogSource, logger} from '../logger/AppBuilderLogger'; +import {nativeChannelTypeMapping} from '../../bridge/rtm/web/Types'; class Events { private source: EventSource = EventSource.core; @@ -41,11 +42,17 @@ class Events { * @api private */ private _persist = async (evt: string, payload: string) => { - const rtmEngine: RtmEngine = RTMEngine.getInstance().engine; + const rtmEngine: RTMClient = RTMEngine.getInstance().engine; + const userId = RTMEngine.getInstance().localUid; try { const rtmAttribute = {key: evt, value: payload}; // Step 1: Call RTM API to update local attributes - await rtmEngine.addOrUpdateLocalUserAttributes([rtmAttribute]); + await rtmEngine.storage.setUserMetadata( + {items: [rtmAttribute]}, + { + userId, + }, + ); } catch (error) { logger.error( LogSource.Events, @@ -68,8 +75,8 @@ class Events { `CUSTOM_EVENT_API Event name cannot be of type ${typeof evt}`, ); } - if (evt.trim() == '') { - throw Error(`CUSTOM_EVENT_API Name or function cannot be empty`); + if (evt.trim() === '') { + throw Error('CUSTOM_EVENT_API Name or function cannot be empty'); } return true; }; @@ -103,10 +110,15 @@ class Events { rtmPayload: RTMAttributePayload, toUid?: ReceiverUid, ) => { - const to = typeof toUid == 'string' ? parseInt(toUid) : toUid; - const rtmEngine: RtmEngine = RTMEngine.getInstance().engine; + const to = typeof toUid === 'string' ? parseInt(toUid, 10) : toUid; const text = JSON.stringify(rtmPayload); + + if (!RTMEngine.getInstance().isEngineReady) { + throw new Error('RTM Engine is not ready. Call setLocalUID() first.'); + } + const rtmEngine: RTMClient = RTMEngine.getInstance().engine; + // Case 1: send to channel if ( typeof to === 'undefined' || @@ -120,7 +132,14 @@ class Events { ); try { const channelId = RTMEngine.getInstance().channelUid; - await rtmEngine.sendMessageByChannelId(channelId, text); + if (!channelId || channelId.trim() === '') { + throw new Error( + 'Channel ID is not set. Cannot send channel attributes.', + ); + } + await rtmEngine.publish(channelId, text, { + channelType: nativeChannelTypeMapping.MESSAGE, // 1 is message + }); } catch (error) { logger.error( LogSource.Events, @@ -140,10 +159,8 @@ class Events { ); const adjustedUID = adjustUID(to); try { - await rtmEngine.sendMessageToPeer({ - peerId: `${adjustedUID}`, - offline: false, - text, + await rtmEngine.publish(`${adjustedUID}`, text, { + channelType: nativeChannelTypeMapping.USER, // user }); } catch (error) { logger.error( @@ -164,14 +181,30 @@ class Events { to, ); try { - for (const uid of to) { - const adjustedUID = adjustUID(uid); - await rtmEngine.sendMessageToPeer({ - peerId: `${adjustedUID}`, - offline: false, - text, - }); - } + const response = await Promise.allSettled( + to.map(uid => + rtmEngine.publish(`${adjustUID(uid)}`, text, { + channelType: nativeChannelTypeMapping.USER, + }), + ), + ); + response.forEach((result, index) => { + const uid = to[index]; + if (result.status === 'rejected') { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + `Failed to publish to user ${uid}:`, + result.reason, + ); + } + }); + // for (const uid of to) { + // const adjustedUID = adjustUID(uid); + // await rtmEngine.publish(`${adjustedUID}`, text, { + // channelType: 3, // user + // }); + // } } catch (error) { logger.error( LogSource.Events, @@ -192,13 +225,31 @@ class Events { 'updating channel attributes', ); try { - const rtmEngine: RtmEngine = RTMEngine.getInstance().engine; + // Validate if rtmengine is ready + if (!RTMEngine.getInstance().isEngineReady) { + throw new Error('RTM Engine is not ready. Call setLocalUID() first.'); + } + const rtmEngine: RTMClient = RTMEngine.getInstance().engine; + const channelId = RTMEngine.getInstance().channelUid; + if (!channelId || channelId.trim() === '') { + throw new Error( + 'Channel ID is not set. Cannot send channel attributes.', + ); + } + const rtmAttribute = [{key: rtmPayload.evt, value: rtmPayload.value}]; - // Step 1: Call RTM API to update local attributes - await rtmEngine.addOrUpdateChannelAttributes(channelId, rtmAttribute, { - enableNotificationToChannelMembers: true, - }); + await rtmEngine.storage.setChannelMetadata( + channelId, + nativeChannelTypeMapping.MESSAGE, + { + items: rtmAttribute, + }, + { + addUserId: true, + addTimeStamp: true, + }, + ); } catch (error) { logger.error( LogSource.Events, @@ -223,7 +274,8 @@ class Events { on = (eventName: string, listener: EventCallback): Function => { try { if (!this._validateEvt(eventName) || !this._validateListener(listener)) { - return; + // Return no-op function instead of undefined to prevent errors + return () => {}; } EventUtils.addListener(eventName, listener, this.source); console.log('CUSTOM_EVENT_API event listener registered', eventName); @@ -238,6 +290,8 @@ class Events { 'Error: events.on', error, ); + // Return no-op function on error to prevent undefined issues + return () => {}; } }; @@ -253,7 +307,11 @@ class Events { off = (eventName?: string, listener?: EventCallback) => { try { if (listener) { - if (this._validateListener(listener) && this._validateEvt(eventName)) { + if ( + eventName && + this._validateListener(listener) && + this._validateEvt(eventName) + ) { // listen off an event by eventName and listener //@ts-ignore EventUtils.removeListener(eventName, listener, this.source); @@ -295,8 +353,18 @@ class Events { persistLevel: PersistanceLevel = PersistanceLevel.None, receiver: ReceiverUid = -1, ) => { - if (!this._validateEvt(eventName)) { - return; + try { + if (!this._validateEvt(eventName)) { + return; + } + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'Event validation failed', + error, + ); + return; // Don't throw - just log and return } const persistValue = JSON.stringify({ @@ -318,6 +386,7 @@ class Events { await this._persist(eventName, persistValue); } catch (error) { logger.error(LogSource.Events, 'CUSTOM_EVENTS', 'persist error', error); + // don't throw - just log the error, application should continue running } } try { @@ -336,9 +405,10 @@ class Events { logger.error( LogSource.Events, 'CUSTOM_EVENTS', - 'sending event failed', + `Failed to send event '${eventName}' - event lost`, error, ); + // don't throw - just log the error, application should continue running } }; } diff --git a/template/src/rtm-events/constants.ts b/template/src/rtm-events/constants.ts index f65b84b4e..04bb65d98 100644 --- a/template/src/rtm-events/constants.ts +++ b/template/src/rtm-events/constants.ts @@ -40,6 +40,10 @@ const BOARD_COLOR_CHANGED = 'BOARD_COLOR_CHANGED'; const WHITEBOARD_LAST_IMAGE_UPLOAD_POSITION = 'WHITEBOARD_L_I_U_P'; const RECORDING_DELETED = 'RECORDING_DELETED'; const SPOTLIGHT_USER_CHANGED = 'SPOTLIGHT_USER_CHANGED'; +// 9. BREAKOUT ROOM +const BREAKOUT_ROOM_JOIN_DETAILS = 'BREAKOUT_ROOM_BREAKOUT_ROOM_JOIN_DETAILS'; +const BREAKOUT_ROOM_STATE = 'BREAKOUT_ROOM_BREAKOUT_ROOM_STATE'; + const EventNames = { RECORDING_STATE_ATTRIBUTE, RECORDING_STARTED_BY_ATTRIBUTE, @@ -61,6 +65,8 @@ const EventNames = { WHITEBOARD_LAST_IMAGE_UPLOAD_POSITION, RECORDING_DELETED, SPOTLIGHT_USER_CHANGED, + BREAKOUT_ROOM_JOIN_DETAILS, + BREAKOUT_ROOM_STATE, }; /** ***** EVENT NAMES ENDS ***** */ diff --git a/template/src/rtm/RTMEngine.ts b/template/src/rtm/RTMEngine.ts index e1a79ad6e..7b1dd00d5 100644 --- a/template/src/rtm/RTMEngine.ts +++ b/template/src/rtm/RTMEngine.ts @@ -10,57 +10,71 @@ ********************************************* */ -import RtmEngine from 'agora-react-native-rtm'; +import { + createAgoraRtmClient, + RtmConfig, + type RTMClient, +} from 'agora-react-native-rtm'; import {isAndroid, isIOS} from '../utils/common'; class RTMEngine { - engine!: RtmEngine; + private _engine?: RTMClient; private localUID: string = ''; private channelId: string = ''; private static _instance: RTMEngine | null = null; + private constructor() { + if (RTMEngine._instance) { + return RTMEngine._instance; + } + RTMEngine._instance = this; + return RTMEngine._instance; + } + public static getInstance() { + // We are only creating the instance but not creating the rtm client yet if (!RTMEngine._instance) { - return new RTMEngine(); + RTMEngine._instance = new RTMEngine(); } return RTMEngine._instance; } - private async createClientInstance() { - await this.engine.createClient($config.APP_ID); - } + setLocalUID(localUID: string | number) { + if (localUID === null || localUID === undefined) { + throw new Error('setLocalUID: localUID cannot be null or undefined'); + } - private async destroyClientInstance() { - await this.engine.logout(); - if (isIOS() || isAndroid()) { - await this.engine.destroyClient(); + const newUID = String(localUID); + if (newUID.trim() === '') { + throw new Error( + 'setLocalUID: localUID cannot be empty after string conversion', + ); } - } - private constructor() { - if (RTMEngine._instance) { - return RTMEngine._instance; + // If UID is changing and we have an existing engine, throw error + if (this._engine && this.localUID !== newUID) { + throw new Error( + `RTMEngine: Cannot change UID from '${this.localUID}' to '${newUID}' while engine is active. ` + + `Please call destroy() first, then setLocalUID() with the new UID.`, + ); } - RTMEngine._instance = this; - this.engine = new RtmEngine(); - this.localUID = ''; - this.channelId = ''; - this.createClientInstance(); - return RTMEngine._instance; - } + this.localUID = newUID; - setLocalUID(localUID: string) { - this.localUID = localUID; + if (!this._engine) { + this.createClientInstance(); + } } setChannelId(channelID: string) { - this.channelId = channelID; - } - - setLoginInfo(localUID: string, channelID: string) { - this.localUID = localUID; + if ( + !channelID || + typeof channelID !== 'string' || + channelID.trim() === '' + ) { + throw new Error('setChannelId: channelID must be a non-empty string'); + } this.channelId = channelID; } @@ -72,16 +86,99 @@ class RTMEngine { return this.channelId; } + get isEngineReady() { + return !!this._engine && !!this.localUID; + } + + get engine(): RTMClient { + this.ensureEngineReady(); + return this._engine!; + } + + private ensureEngineReady() { + if (!this.isEngineReady) { + throw new Error( + 'RTM Engine not ready. Please call setLocalUID() with a valid UID first.', + ); + } + } + + private createClientInstance() { + try { + if (!this.localUID || this.localUID.trim() === '') { + throw new Error('Cannot create RTM client: localUID is not set'); + } + if (!$config.APP_ID) { + throw new Error('Cannot create RTM client: APP_ID is not configured'); + } + const rtmConfig = new RtmConfig({ + appId: $config.APP_ID, + userId: this.localUID, + }); + this._engine = createAgoraRtmClient(rtmConfig); + } catch (error) { + const contextError = new Error( + `Failed to create RTM client instance for userId: ${ + this.localUID + }, appId: ${$config.APP_ID}. Error: ${error.message || error}`, + ); + console.error('RTMEngine createClientInstance error:', contextError); + throw contextError; + } + } + + private async destroyClientInstance() { + try { + if (this._engine) { + // 1. Unsubscribe from channel if we have one + if (this.channelId) { + try { + await this._engine.unsubscribe(this.channelId); + } catch (error) { + console.warn( + `Failed to unsubscribe from channel '${this.channelId}':`, + error, + ); + // Continue with cleanup even if unsubscribe fails + } + } + // 2. Remove all listeners + try { + this._engine.removeAllListeners?.(); + } catch (error) { + console.warn('Failed to remove listeners:', error); + } + // 3. Logout + try { + await this._engine.logout(); + if (isAndroid() || isIOS()) { + this._engine.release(); + } + } catch (error) { + console.warn('Failed to logout:', error); + } + } + } catch (error) { + console.error('Error during client instance destruction:', error); + // Don't re-throw - we want cleanup to complete + } + } + async destroy() { try { - await this.destroyClientInstance(); - if (isIOS() || isAndroid()) { - RTMEngine._instance = null; + if (!this._engine) { + return; } - this.localUID = ''; + + await this.destroyClientInstance(); this.channelId = ''; + this.localUID = ''; + this._engine = undefined; + RTMEngine._instance = null; } catch (error) { - console.log('Error destroying instance error: ', error); + console.error('Error destroying RTM instance:', error); + // Don't re-throw - destruction should be a best-effort cleanup + // Re-throwing could prevent proper cleanup in calling code } } } diff --git a/template/src/subComponents/SidePanelEnum.tsx b/template/src/subComponents/SidePanelEnum.tsx index 0ac6cec07..f784930cc 100644 --- a/template/src/subComponents/SidePanelEnum.tsx +++ b/template/src/subComponents/SidePanelEnum.tsx @@ -16,4 +16,5 @@ export enum SidePanelType { Settings = 'Settings', Transcript = 'Transcript', VirtualBackground = 'VirtualBackground', + BreakoutRoom = 'BreakoutRoom', } diff --git a/template/src/subComponents/caption/useCaption.tsx b/template/src/subComponents/caption/useCaption.tsx index a924d4d2c..756f4d2ee 100644 --- a/template/src/subComponents/caption/useCaption.tsx +++ b/template/src/subComponents/caption/useCaption.tsx @@ -72,7 +72,7 @@ export const CaptionContext = React.createContext<{ }); interface CaptionProviderProps { - callActive: boolean; + callActive?: boolean; children: React.ReactNode; } diff --git a/template/src/utils/useEndCall.ts b/template/src/utils/useEndCall.ts index 1a5cc0ad4..13d8d08d2 100644 --- a/template/src/utils/useEndCall.ts +++ b/template/src/utils/useEndCall.ts @@ -69,7 +69,7 @@ const useEndCall = () => { if ($config.CHAT) { deleteChatUser(); } - RTMEngine.getInstance().engine.leaveChannel(rtcProps.channel); + RTMEngine.getInstance().destroy(); if (!ENABLE_AUTH) { // await authLogout(); await authLogin(); From 95fd844fc359be3c13d79ea0891c0a753bc106b3 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Fri, 29 Aug 2025 11:04:44 +0530 Subject: [PATCH 02/56] Breakout RTM Refactor (#741) ** add breakout ui --- template/src/AppRoutes.tsx | 7 +- template/src/assets/font-styles.css | 28 + template/src/assets/fonts/icomoon.ttf | Bin 40200 -> 43152 bytes template/src/assets/selection.json | 2 +- template/src/atoms/CustomIcon.tsx | 7 + template/src/atoms/TertiaryButton.tsx | 2 +- template/src/components/ChatContext.ts | 4 +- template/src/components/Controls.tsx | 35 +- template/src/components/RTMConfigure.tsx | 778 +++++++--------- .../breakout-room/BreakoutRoomPanel.tsx | 121 +-- .../context/BreakoutRoomContext.tsx | 858 ++++++++++++++++-- .../context/BreakoutRoomEngineContext.tsx | 118 --- .../context/BreakoutRoomStateContext.tsx | 39 - .../events/BreakoutRoomEventsConfigure.tsx | 110 +++ .../breakout-room/events/constants.ts | 15 + .../hooks/useBreakoutRoomExit.ts | 37 + .../components/breakout-room/state/reducer.ts | 331 ++++++- .../components/breakout-room/state/types.ts | 4 +- .../ui/BreakoutRoomActionMenu.tsx | 136 +++ .../ui/BreakoutRoomAnnouncementModal.tsx | 136 +++ .../ui/BreakoutRoomGroupSettings.tsx | 301 +++++- .../ui/BreakoutRoomMainRoomUsers.tsx | 216 +++++ .../ui/BreakoutRoomParticipants.tsx | 57 +- .../ui/BreakoutRoomRaiseHand.tsx | 76 ++ .../ui/BreakoutRoomRenameModal.tsx | 150 +++ .../breakout-room/ui/BreakoutRoomSettings.tsx | 63 +- .../ui/BreakoutRoomTransition.tsx | 42 + .../breakout-room/ui/BreakoutRoomView.tsx | 158 ++++ .../ui/ExitBreakoutRoomIconButton.tsx | 68 ++ .../ui/ParticipantManualAssignmentModal.tsx | 440 +++++++++ .../SelectParticipantAssignmentStrategy.tsx | 14 - .../ExitBreakoutRoomToolbarItem.tsx | 13 + .../participants/UserActionMenuOptions.tsx | 45 +- template/src/logger/AppBuilderLogger.tsx | 8 +- template/src/pages/VideoCall.tsx | 676 +++----------- .../src/pages/VideoCallRoomOrchestrator.tsx | 201 ++-- .../video-call/BreakoutVideoCallContent.tsx | 241 +++++ .../pages/video-call/MainVideoCallContent.tsx | 212 +++++ .../src/pages/video-call/VideoCallContent.tsx | 156 ++++ .../pages/video-call/VideoCallStateSetup.tsx | 4 +- .../video-call/VideoCallStateWrapper.tsx | 485 ++++++++++ template/src/rtm-events-api/Events.ts | 33 +- template/src/rtm-events/constants.ts | 5 - template/src/rtm/RTMConfigure-v2.tsx | 774 ++++++++++++++++ template/src/rtm/RTMCoreProvider.tsx | 329 +++++++ template/src/rtm/RTMEngine.ts | 133 +-- .../src/rtm/hooks/useSubscribeChannel.tsx | 39 + .../screenshare/ScreenshareButton.tsx | 6 + 48 files changed, 6016 insertions(+), 1697 deletions(-) delete mode 100644 template/src/components/breakout-room/context/BreakoutRoomEngineContext.tsx delete mode 100644 template/src/components/breakout-room/context/BreakoutRoomStateContext.tsx create mode 100644 template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx create mode 100644 template/src/components/breakout-room/events/constants.ts create mode 100644 template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomAnnouncementModal.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomView.tsx create mode 100644 template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx create mode 100644 template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx create mode 100644 template/src/components/controls/toolbar-items/ExitBreakoutRoomToolbarItem.tsx create mode 100644 template/src/pages/video-call/BreakoutVideoCallContent.tsx create mode 100644 template/src/pages/video-call/MainVideoCallContent.tsx create mode 100644 template/src/pages/video-call/VideoCallContent.tsx create mode 100644 template/src/pages/video-call/VideoCallStateWrapper.tsx create mode 100644 template/src/rtm/RTMConfigure-v2.tsx create mode 100644 template/src/rtm/RTMCoreProvider.tsx create mode 100644 template/src/rtm/hooks/useSubscribeChannel.tsx diff --git a/template/src/AppRoutes.tsx b/template/src/AppRoutes.tsx index 17f0cf2d9..0d353c914 100644 --- a/template/src/AppRoutes.tsx +++ b/template/src/AppRoutes.tsx @@ -11,7 +11,6 @@ */ import React from 'react'; import Join from './pages/Join'; -import VideoCall from './pages/VideoCall'; import Create from './pages/Create'; import {Route, Switch, Redirect} from './components/Router'; import AuthRoute from './auth/AuthRoute'; @@ -25,7 +24,7 @@ import {useIsRecordingBot} from './subComponents/recording/useIsRecordingBot'; import {isValidReactComponent} from './utils/common'; import ErrorBoundary from './components/ErrorBoundary'; import {ErrorBoundaryFallback} from './components/ErrorBoundaryFallback'; -import VideoCallRoomOrchestrator from './pages/VideoCallRoomOrchestrator'; +import VideoCallStateWrapper from './pages/video-call/VideoCallStateWrapper'; function VideoCallWrapper(props) { const {isRecordingBot} = useIsRecordingBot(); @@ -33,13 +32,13 @@ function VideoCallWrapper(props) { return isRecordingBot ? ( - + ) : ( - + ); diff --git a/template/src/assets/font-styles.css b/template/src/assets/font-styles.css index 60b4ab754..ae7bf03c2 100644 --- a/template/src/assets/font-styles.css +++ b/template/src/assets/font-styles.css @@ -24,6 +24,34 @@ -moz-osx-font-smoothing: grayscale; } +.icon-people-assigned:before { + content: '\e9a4'; + color: #e8eaed; +} +.icon-double-up-arrow:before { + content: '\e9a5'; + color: #fff; +} +.icon-move-up:before { + content: '\e9a6'; + color: #fff; +} +.icon-close-room:before { + content: '\e9a7'; + color: #fff; +} +.icon-announcement:before { + content: '\e9a8'; + color: #fff; +} +.icon-settings-outlined:before { + content: '\e9a9'; + color: #fff; +} +.icon-breakout-room:before { + content: '\e9aa'; + color: #d9d9d9; +} .icon-spotlight:before { content: '\e9a3'; color: #fff; diff --git a/template/src/assets/fonts/icomoon.ttf b/template/src/assets/fonts/icomoon.ttf index 2327d47ac56ffb713b456ef948662953dcc63e55..2762c18eaf6fe73eb682220308c52979638ad5ad 100644 GIT binary patch delta 2973 zcmaJ@TWnlM8J;nq^{%~M+w0hlxAi6K5aL{ln-DunAk7U0#l~@Bk~j$@ zDnV+s2}My=UYag#=tWS422da=U{zccq&(ySq!tM%Y9E6XRXo5$k$9rn@XhQJTb0OY zcF+IMKl9)Jf9Cn$YA^mts}V{FamWS2$YZ%k|$e@2LQ>*VSAEps1!%-$m8+9H_DF3p`kL+;S`3Hj0QaiHte+|t6UJFNcz z|2ycH&YV7b4vJ#@L@Llln9eTW-bAmzM5pQX^4c0$4HfYk$eM3 z>laU@srt7{oS_Z5VwYZYgG)gt}VR(a+O#sV?(6_klcuUQe>i0{FEdm{or$5 zUm5G(Grz-4Symu|jAdGo+~!1m$<*3*x;Zi2p)~e7!>A{Yd!ne|Q5whXM3er~?!jN( zWD1^w#ij1Pa{Uj7b7r&(scGb-)1jE$?ar;e2ZKnq!&sKBWUZYleAiXX*OF^*e0qXnoOh+zzuXnewuL{ zq_H}(k*YJJMMcX~735MedK9rbI$olOH_Q*LSyS#W&rVE89~iE*`#Ai}ziPO=CLFn_ zj}LslkS}y~b?sa`1G<5syL{%p>2rf)`X-G{-{1zOZ#Mq<|JE~i?_TKd@1Ot7#Nl#z z&{%IcoI?V*9p}CPhvO1~VBzTFMt7l>-!?Eb)7jYxB^&`|bc3E&GVWMjQ?zH@QIZ>% zp^0bSu^y_?1UFwsRiVC6KySd;!(E*t1)1{^2svy)h&f7w%OE?KyOcJTTiJM>Uw75HQ1?Rz~Zq# zGRM)F>4EYbbT|0qiN92n zY+IM=ggcTc)g5?Wg~BDssTdBD-WjGy$xYH?)kUzXDr%UvGH(MojL5W%kb1nkLQ5@g zwldHdU)_D!-u>J`T46eTKava(Fh^Y1Z6pMrG6ZG1gk7TvpPE8aa9h_IrQDdp9??SX z|9T+EI%;HtAJnYavMtQms!b?wgSny81vfTjD>7r3{!)q+D+);YhJMxH6hgGb7sz`C`3R+7PxZ{7NOn*;;Y0W`o#KJ4eCbZr02JoaL0TP^4FWS=Ei0he z=jA1!Vg>gjLUQ6xP(ZVHq(!=_;?PN}^=MzRR-mKwhH#Nn)HNd$ZFV2`;2Ji`mP2uEZKtl ze6PUaP#)%nTNBTub_YS;^D^$y{(}s`m4mS_^&fNE13w>lU+}`b=S%VdO>dHQ?Lhxg zC*ynh&|i}sY*S+}n;nWI6b)r(5KdPdVy0o(yoXVynfwG4NB@}6EyJ*M10;)2)Q|^I z_&B$quGYhC!!X$)7}RQohjdeyEI@q7LSJtn^CSz9j0xG6S{#^SAlR1$I;6hwl+|-~GTc)coE$L3p6bW}=YO5qg7XHmZJn`h9n-Frtd$49On;BB} zwGCyIsYo6oR4o%^PmXLQM^MXG$?piBIIXKe)*x=YRwIj=S3}&bN?T~|Wi+DP`n5D@*pc?Ja1;4~=EIOFje{#N z{qdR=r18@gSo^wCwXwKj0=iwNZEy=8 zOp6;fKF`7+fEGSrYAG^aAnK(|NS+k`arwmI;IscGj$9?=-n*CL|9$I;D^>dT^30Vt zwff=}x8C(Voik(``(XK_R}av_^vUV>XTG~LvGd-piQOj^%eU#DNkTEw>L0%mFVDYz Gjs6#{EQOW; delta 281 zcmbPmk*Q-BQ#}JC0|Ns$LjwadgMojrz7f9=+eM(r9w1Ig&P^;354)_zz`!U24215n;911P}B!+Mi}VLDI^ zgGxqjNyV((|9^md9|i`d1v&Z2j)@=6pABJPnE3{1NKkHKMFGQJ#w!dAvmJmM6!H>t zQ)hZ{{s!_Bfbwkx`NbtbhXTQz7a(~CW)_x-2kaRaPh#|DT)f$bap_cLkbi(+v3zo3 xJipCX25uHG|8eTx;~)lzo?NoXoN>kGHH#K7ZO&L4#K-{@2WeZe`P{NYi~y { const { @@ -679,6 +681,10 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { } // 8. Screenshare + const {permissions} = useBreakoutRoom(); + const canAccessBreakoutRoom = useControlPermissionMatrix('breakoutRoom'); + const canScreenshareInBreakoutRoom = permissions.canScreenshare; + const canAccessScreenshare = useControlPermissionMatrix('screenshareControl'); if (canAccessScreenshare) { if ( @@ -695,10 +701,11 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { componentName: 'screenshare', order: 8, disabled: - rtcProps.role == ClientRoleType.ClientRoleAudience && - $config.EVENT_MODE && - $config.RAISE_HAND && - !isHost, + (rtcProps.role == ClientRoleType.ClientRoleAudience && + $config.EVENT_MODE && + $config.RAISE_HAND && + !isHost) || + !canScreenshareInBreakoutRoom, icon: isScreenshareActive ? 'stop-screen-share' : 'screen-share', iconColor: isScreenshareActive ? $config.SEMANTIC_ERROR @@ -837,12 +844,11 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { } // 14. Breakout Room - const canAccessBreakoutRoom = useControlPermissionMatrix('breakoutRoom'); if (canAccessBreakoutRoom) { actionMenuitems.push({ componentName: 'breakoutRoom', order: 14, - icon: 'participants', + icon: 'breakout-room', iconColor: $config.SECONDARY_ACTION_COLOR, textColor: $config.FONT_COLOR, title: breakoutRoomLabel, @@ -1211,6 +1217,7 @@ const Controls = (props: ControlsProps) => { const {sttLanguage, isSTTActive} = useRoomInfo(); const {addStreamMessageListener} = useSpeechToText(); + const {permissions} = useBreakoutRoom(); React.useEffect(() => { defaultContentRef.current = defaultContent; @@ -1305,6 +1312,11 @@ const Controls = (props: ControlsProps) => { const canAccessInvite = useControlPermissionMatrix('inviteControl'); const canAccessScreenshare = useControlPermissionMatrix('screenshareControl'); + const canAccessExitBreakoutRoomBtn = permissions.canExitRoom; + console.log( + 'supriya-exit canAccessExitBreakoutRoomBtn: ', + canAccessExitBreakoutRoomBtn, + ); const defaultItems: ToolbarPresetProps['items'] = React.useMemo(() => { return { @@ -1365,13 +1377,20 @@ const Controls = (props: ControlsProps) => { component: MoreButtonToolbarItem, order: 6, }, + 'exit-breakout-room': { + align: 'center', + component: canAccessExitBreakoutRoomBtn + ? ExitBreakoutRoomToolbarItem + : null, + order: 7, + }, 'end-call': { align: 'center', component: LocalEndcallToolbarItem, - order: 7, + order: 8, }, }; - }, [canAccessInvite, canAccessScreenshare]); + }, [canAccessInvite, canAccessScreenshare, canAccessExitBreakoutRoomBtn]); const mergedItems = CustomToolbarMerge( includeDefaultItems ? defaultItems : {}, diff --git a/template/src/components/RTMConfigure.tsx b/template/src/components/RTMConfigure.tsx index 42e1e4c49..858c7f4d8 100644 --- a/template/src/components/RTMConfigure.tsx +++ b/template/src/components/RTMConfigure.tsx @@ -65,6 +65,7 @@ import { nativePresenceEventTypeMapping, nativeStorageEventTypeMapping, } from '../../bridge/rtm/web/Types'; +import {useRTMCore} from '../rtm/RTMCoreProvider'; export enum UserType { ScreenShare = 'screenshare', @@ -72,12 +73,16 @@ export enum UserType { const eventTimeouts = new Map>(); -const RtmConfigure = (props: any) => { - let engine = useRef(null!); +interface Props { + callActive: boolean; + children: React.ReactNode; + channelName: string; +} + +const RtmConfigure = (props: Props) => { const rtmInitTimstamp = new Date().getTime(); const localUid = useLocalUid(); - const {callActive} = props; - const {rtcProps} = useContext(PropsContext); + const {callActive, channelName} = props; const {dispatch} = useContext(DispatchContext); const {defaultContent, activeUids} = useContent(); const { @@ -89,7 +94,8 @@ const RtmConfigure = (props: any) => { const [onlineUsersCount, setTotalOnlineUsers] = useState(0); const timerValueRef: any = useRef(5); // Track RTM connection state (equivalent to v1.5x connectionState check) - const [rtmConnectionState, setRtmConnectionState] = useState(0); // 0=IDLE, 2=CONNECTED + const {client, isLoggedIn, registerCallbacks, unregisterCallbacks} = + useRTMCore(); /** * inside event callback state won't have latest value. @@ -143,442 +149,91 @@ const RtmConfigure = (props: any) => { ); }, [defaultContent]); - React.useEffect(() => { - // If its not a convo ai project and - // the platform is web execute the window listeners - if (!$config.ENABLE_CONVERSATIONAL_AI && isWebInternal()) { - const handBrowserClose = ev => { - ev.preventDefault(); - return (ev.returnValue = 'Are you sure you want to exit?'); - }; - const logoutRtm = () => { - try { - if (engine.current && RTMEngine.getInstance().channelUid) { - // First unsubscribe from channel (like v1.5x leaveChannel) - engine.current.unsubscribe(RTMEngine.getInstance().channelUid); - // Then logout - engine.current.logout(); - } - } catch (error) { - console.error('Error during browser close RTM cleanup:', error); - } - }; - - // Set up window listeners - window.addEventListener( - 'beforeunload', - isWeb() && !isSDK() ? handBrowserClose : () => {}, - ); - - window.addEventListener('pagehide', logoutRtm); - return () => { - // Remove listeners on unmount - window.removeEventListener( - 'beforeunload', - isWeb() && !isSDK() ? handBrowserClose : () => {}, - ); - window.removeEventListener('pagehide', logoutRtm); - }; - } - }, []); - - const init = async (rtcUid: UidType) => { - //on sdk due to multiple re-render we are getting rtm error code 8 - //you are joining the same channel too frequently, exceeding the allowed rate of joining the same channel multiple times within a short period - //so checking rtm connection state before proceed - - // Check if already connected (equivalent to v1.5x connectionState === 'CONNECTED') - if ( - rtmConnectionState === nativeLinkStateMapping.CONNECTED && - RTMEngine.getInstance().isEngineReady - ) { - logger.log( - LogSource.AgoraSDK, - 'Log', - '🚫 RTM already connected, skipping initialization', - ); - return; - } - - try { - if (!RTMEngine.getInstance().isEngineReady) { - RTMEngine.getInstance().setLocalUID(rtcUid); - logger.log(LogSource.AgoraSDK, 'API', 'RTM local Uid set', rtcUid); - } - engine.current = RTMEngine.getInstance().engine; - // Logout any opened sessions if any - engine.current.logout(); - logger.log(LogSource.AgoraSDK, 'Log', 'RTM client creation done'); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'Log', - 'RTM engine initialization failed:', - {error}, - ); - throw error; - } - - engine.current.addEventListener( - 'linkState', - async (data: LinkStateEvent) => { - // Update connection state for duplicate initialization prevention - setRtmConnectionState(data.currentState); - logger.log( - LogSource.AgoraSDK, - 'Event', - `RTM linkState changed: ${data.previousState} -> ${data.currentState}`, - data, - ); - if (data.currentState === nativeLinkStateMapping.CONNECTED) { - // CONNECTED state - logger.log(LogSource.AgoraSDK, 'Event', 'RTM connected', { - previousState: data.previousState, - currentState: data.currentState, - }); - } - if (data.currentState === nativeLinkStateMapping.FAILED) { - // FAILED state - logger.error(LogSource.AgoraSDK, 'Event', 'RTM connection failed', { - error: { - reasonCode: data.reasonCode, - currentState: data.currentState, - }, - }); - } - }, - ); - - engine.current.addEventListener('storage', (storage: StorageEvent) => { - // when remote user sets/updates metadata - 3 - if ( - storage.eventType === nativeStorageEventTypeMapping.SET || - storage.eventType === nativeStorageEventTypeMapping.UPDATE - ) { - const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; - const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; - logger.log( - LogSource.AgoraSDK, - 'Event', - `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, - storage, - ); - try { - if (storage.data?.items && Array.isArray(storage.data.items)) { - storage.data.items.forEach(item => { - try { - if (!item || !item.key) { - logger.warn( - LogSource.Events, - 'CUSTOM_EVENTS', - 'Invalid storage item:', - item, - ); - return; - } - - const {key, value, authorUserId, updateTs} = item; - const timestamp = getMessageTime(updateTs); - const sender = Platform.OS - ? get32BitUid(authorUserId) - : parseInt(authorUserId, 10); - eventDispatcher( - { - evt: key, - value, - }, - `${sender}`, - timestamp, - ); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - `Failed to process storage item: ${JSON.stringify(item)}`, - {error}, - ); - } - }); - } - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } - }); - - engine.current.addEventListener( - 'presence', - async (presence: PresenceEvent) => { - if (`${localUid}` === presence.publisher) { - return; - } - // remoteJoinChannel - if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { - logger.log( - LogSource.AgoraSDK, - 'Event', - 'RTM presenceEvent of type [3 - remoteJoin] (channelMemberJoined)', - ); - const backoffAttributes = await fetchUserAttributesWithBackoffRetry( - presence.publisher, - ); - await processUserUidAttributes(backoffAttributes, presence.publisher); - } - // remoteLeaveChannel - if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { - logger.log( - LogSource.AgoraSDK, - 'Event', - 'RTM presenceEvent of type [4 - remoteLeave] (channelMemberLeft)', - presence, - ); - // Chat of left user becomes undefined. So don't cleanup - const uid = presence?.publisher - ? parseInt(presence.publisher, 10) - : undefined; - - if (!uid) { - return; - } - SDKEvents.emit('_rtm-left', uid); - // updating the rtc data - updateRenderListState(uid, { - offline: true, - }); - } - }, - ); - - engine.current.addEventListener('message', (message: MessageEvent) => { - if (`${localUid}` === message.publisher) { - return; - } - // message - 1 (channel) - if (message.channelType === nativeChannelTypeMapping.MESSAGE) { - logger.debug( - LogSource.Events, - 'CUSTOM_EVENTS', - 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', - message, - ); - const { - publisher: uid, - channelName: channelId, - message: text, - timestamp: ts, - } = message; - //whiteboard upload - if (parseInt(uid, 10) === 1010101) { - const [err, res] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - if (res?.data?.data?.images) { - LocalEventEmitter.emit( - LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, - res?.data?.data?.images, - ); - } - } else { - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - - const timestamp = getMessageTime(ts); - const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); - - if (channelId === rtcProps.channel) { - try { - eventDispatcher(msg, `${sender}`, timestamp); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } - } - } - - // message - 3 (user) - if (message.channelType === nativeChannelTypeMapping.USER) { - logger.debug( - LogSource.Events, - 'CUSTOM_EVENTS', - 'messageEvent of type [3- USER] (messageReceived)', - message, - ); - const {publisher: peerId, timestamp: ts, message: text} = message; - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - - const timestamp = getMessageTime(ts); - - const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); - - try { - eventDispatcher(msg, `${sender}`, timestamp); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } - }); - - await doLoginAndSetupRTM(); - }; - - const doLoginAndSetupRTM = async () => { - try { - logger.log(LogSource.AgoraSDK, 'API', 'RTM login starts'); - console.log('supriya[RTM] localUid', localUid, rtcProps, rtcProps.rtm); - await engine.current.login({ - // @ts-ignore - token: rtcProps.rtm, - }); - logger.log(LogSource.AgoraSDK, 'API', 'RTM login done'); - timerValueRef.current = 5; - // waiting for login to be fully connected - await new Promise(resolve => setTimeout(resolve, 500)); - await setAttribute(); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'Log', - 'RTM login failed..Trying again', - {error}, - ); - setTimeout(async () => { - // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); - doLoginAndSetupRTM(); - }, timerValueRef.current * 1000); - } - }; - - const setAttribute = async () => { - const rtmAttributes = [ - {key: 'screenUid', value: String(rtcProps.screenShareUid)}, - {key: 'isHost', value: String(isHostRef.current.isHost)}, - ]; - try { - const data: Metadata = { - items: rtmAttributes, - }; - const options: SetOrUpdateUserMetadataOptions = { - userId: `${localUid}`, - }; - await engine.current.storage.setUserMetadata(data, options); - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM setting local user attributes', - { - attr: rtmAttributes, - }, - ); - timerValueRef.current = 5; - await subscribeChannel(); - logger.log( - LogSource.AgoraSDK, - 'Log', - 'RTM subscribe, fetch members, reading channel atrributes all done', - { - data: rtmAttributes, - }, - ); - setHasUserJoinedRTM(true); - await runQueuedEvents(); - setIsInitialQueueCompleted(true); - logger.log( - LogSource.AgoraSDK, - 'Log', - 'RTM queued events finished running', - { - attr: rtmAttributes, - }, - ); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'Log', - 'RTM setAttribute failed..Trying again', - {error}, - ); - setTimeout(async () => { - // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); - setAttribute(); - }, timerValueRef.current * 1000); - } + // React.useEffect(() => { + // // If its not a convo ai project and + // // the platform is web execute the window listeners + // if (!$config.ENABLE_CONVERSATIONAL_AI && isWebInternal()) { + // const handBrowserClose = ev => { + // ev.preventDefault(); + // return (ev.returnValue = 'Are you sure you want to exit?'); + // }; + + // const logoutRtm = () => { + // try { + // if (client && RTMEngine.getInstance().channelUid) { + // // First unsubscribe from channel (like v1.5x leaveChannel) + // client.unsubscribe(RTMEngine.getInstance().channelUid); + // // Then logout + // client.logout(); + // } + // } catch (error) { + // console.error('Error during browser close RTM cleanup:', error); + // } + // }; + + // // Set up window listeners + // window.addEventListener( + // 'beforeunload', + // isWeb() && !isSDK() ? handBrowserClose : () => {}, + // ); + + // window.addEventListener('pagehide', logoutRtm); + // return () => { + // // Remove listeners on unmount + // window.removeEventListener( + // 'beforeunload', + // isWeb() && !isSDK() ? handBrowserClose : () => {}, + // ); + // window.removeEventListener('pagehide', logoutRtm); + // }; + // } + // }, []); + + const init = async () => { + await subscribeChannel(); + setHasUserJoinedRTM(true); + await runQueuedEvents(); + setIsInitialQueueCompleted(true); + logger.log(LogSource.AgoraSDK, 'Log', 'RTM queued events finished running'); }; const subscribeChannel = async () => { try { - if (RTMEngine.getInstance().channelUid === rtcProps.channel) { + if (RTMEngine.getInstance().allChannels.includes(channelName)) { logger.debug( LogSource.AgoraSDK, 'Log', '🚫 RTM already subscribed channel skipping', - rtcProps.channel, + channelName, ); } else { - await engine.current.subscribe(rtcProps.channel, { + await client.subscribe(channelName, { withMessage: true, withPresence: true, withMetadata: true, withLock: false, }); logger.log(LogSource.AgoraSDK, 'API', 'RTM subscribeChannel', { - data: rtcProps.channel, + data: channelName, }); // Set channel ID AFTER successful subscribe (like v1.5x) - RTMEngine.getInstance().setChannelId(rtcProps.channel); + console.log('setting primary channel', channelName); + RTMEngine.getInstance().addChannel(channelName, true); logger.log( LogSource.AgoraSDK, 'API', 'RTM setChannelId as subscribe is successful', - rtcProps.channel, + channelName, ); - logger.debug( LogSource.SDK, 'Event', 'Emitting rtm joined', - rtcProps.channel, + channelName, ); // @ts-ignore - SDKEvents.emit('_rtm-joined', rtcProps.channel); + SDKEvents.emit('_rtm-joined', channelName); timerValueRef.current = 5; await getMembers(); await readAllChannelAttributes(); @@ -610,8 +265,8 @@ const RtmConfigure = (props: any) => { 'API', 'RTM presence.getOnlineUsers(getMembers) start', ); - await engine.current.presence - .getOnlineUsers(rtcProps.channel, 1) + await client.presence + .getOnlineUsers(channelName, 1) .then(async (data: GetOnlineUsersResponse) => { logger.log( LogSource.AgoraSDK, @@ -686,8 +341,8 @@ const RtmConfigure = (props: any) => { const readAllChannelAttributes = async () => { try { - await engine.current.storage - .getChannelMetadata(rtcProps.channel, 1) + await client.storage + .getChannelMetadata(channelName, 1) .then(async (data: GetChannelMetadataResponse) => { for (const item of data.items) { try { @@ -745,7 +400,7 @@ const RtmConfigure = (props: any) => { ); const attr: GetUserMetadataResponse = - await engine.current.storage.getUserMetadata({ + await client.storage.getUserMetadata({ userId: userId, }); @@ -808,6 +463,7 @@ const RtmConfigure = (props: any) => { isHost: isHostItem?.value || false, lastMessageTimeStamp: 0, }; + console.log('new user joined', uid, userData); updateRenderListState(uid, userData); //end- updating user data in rtc @@ -961,7 +617,7 @@ const RtmConfigure = (props: any) => { const options: SetOrUpdateUserMetadataOptions = { userId: `${localUid}`, }; - await engine.current.storage.setUserMetadata( + await client.storage.setUserMetadata( { items: [rtmAttribute], }, @@ -969,7 +625,7 @@ const RtmConfigure = (props: any) => { ); } // Step 2: Emit the event - console.log(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: '); + console.log(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: ', evt); EventUtils.emitEvent(evt, source, {payload, persistLevel, sender, ts}); // Because async gets evaluated in a different order when in an sdk if (evt === 'name') { @@ -1005,62 +661,262 @@ const RtmConfigure = (props: any) => { } }; - const end = async () => { - if (!callActive) { + const unsubscribeAndCleanup = async (channelName: string) => { + if (!callActive || !isLoggedIn) { return; } - // Destroy and clean up RTM state - await RTMEngine.getInstance().destroy(); - // Set the engine as null - engine.current = null; - logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); - if (isIOS() || isAndroid()) { - EventUtils.clear(); + try { + client.unsubscribe(channelName); + RTMEngine.getInstance().removeChannel(channelName); + logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); + if (isIOS() || isAndroid()) { + EventUtils.clear(); + } + setHasUserJoinedRTM(false); + setIsInitialQueueCompleted(false); + logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); + } catch (unsubscribeError) { + console.log('supriya error while unsubscribing: ', unsubscribeError); } - setHasUserJoinedRTM(false); - setIsInitialQueueCompleted(false); - logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); }; - useAsyncEffect(async () => { - //waiting room attendee -> rtm login will happen on page load - try { - if ($config.ENABLE_WAITING_ROOM) { - //attendee - //for waiting room attendee rtm login will happen on mount - if (!isHost && !callActive) { - await init(localUid); + // Register listeners when client is created + useEffect(() => { + if (!client) { + return; + } + + const handleStorageEvent = (storage: StorageEvent) => { + // when remote user sets/updates metadata - 3 + if ( + storage.eventType === nativeStorageEventTypeMapping.SET || + storage.eventType === nativeStorageEventTypeMapping.UPDATE + ) { + const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; + const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; + logger.log( + LogSource.AgoraSDK, + 'Event', + `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, + storage, + ); + try { + if (storage.data?.items && Array.isArray(storage.data.items)) { + storage.data.items.forEach(item => { + try { + if (!item || !item.key) { + logger.warn( + LogSource.Events, + 'CUSTOM_EVENTS', + 'Invalid storage item:', + item, + ); + return; + } + + const {key, value, authorUserId, updateTs} = item; + const timestamp = getMessageTime(updateTs); + const sender = Platform.OS + ? get32BitUid(authorUserId) + : parseInt(authorUserId, 10); + eventDispatcher( + { + evt: key, + value, + }, + `${sender}`, + timestamp, + ); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + `Failed to process storage item: ${JSON.stringify(item)}`, + {error}, + ); + } + }); + } + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); } - //host - if ( - isHost && - ($config.AUTO_CONNECT_RTM || - (!$config.AUTO_CONNECT_RTM && callActive)) - ) { - await init(localUid); + } + }; + + const handlePresenceEvent = async (presence: PresenceEvent) => { + if (`${localUid}` === presence.publisher) { + return; + } + if (presence.channelName !== channelName) { + console.log( + 'supriya event recevied in channel', + presence.channelName, + channelName, + ); + return; + } + // remoteJoinChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { + logger.log( + LogSource.AgoraSDK, + 'Event', + 'RTM presenceEvent of type [3 - remoteJoin] (channelMemberJoined)', + ); + const backoffAttributes = await fetchUserAttributesWithBackoffRetry( + presence.publisher, + ); + await processUserUidAttributes(backoffAttributes, presence.publisher); + } + // remoteLeaveChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { + logger.log( + LogSource.AgoraSDK, + 'Event', + 'RTM presenceEvent of type [4 - remoteLeave] (channelMemberLeft)', + presence, + ); + // Chat of left user becomes undefined. So don't cleanup + const uid = presence?.publisher + ? parseInt(presence.publisher, 10) + : undefined; + + if (!uid) { + return; } - } else { - //non waiting room case - //host and attendee - if ( - $config.AUTO_CONNECT_RTM || - (!$config.AUTO_CONNECT_RTM && callActive) - ) { - await init(localUid); + SDKEvents.emit('_rtm-left', uid); + // updating the rtc data + updateRenderListState(uid, { + offline: true, + }); + } + }; + + const handleMessageEvent = (message: MessageEvent) => { + console.log('supriya current message channel: ', channelName); + console.log('supriya message event is', message); + // message - 1 (channel) + if (message.channelType === nativeChannelTypeMapping.MESSAGE) { + // here the channel name will be the channel name + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', + message, + ); + const { + publisher: uid, + channelName, + message: text, + timestamp: ts, + } = message; + //whiteboard upload + if (parseInt(uid, 10) === 1010101) { + const [err, res] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + if (res?.data?.data?.images) { + LocalEventEmitter.emit( + LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, + res?.data?.data?.images, + ); + } + } else { + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } } } + + // message - 3 (user) + if (message.channelType === nativeChannelTypeMapping.USER) { + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [3- USER] (messageReceived)', + message, + ); + // here the (message.channelname) channel name will be the to UID + const {publisher: peerId, timestamp: ts, message: text} = message; + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + + const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); + + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + }; + + registerCallbacks(channelName, { + storage: handleStorageEvent, + presence: handlePresenceEvent, + message: handleMessageEvent, + }); + + return () => { + unregisterCallbacks(channelName); + }; + }, [client, channelName]); + + useAsyncEffect(async () => { + try { + if (isLoggedIn && callActive) { + await init(); + } } catch (error) { logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); } return async () => { - logger.log( - LogSource.AgoraSDK, - 'Log', - 'RTM unmounting calling end(destroy) ', - ); - await end(); + await unsubscribeAndCleanup(channelName); }; - }, [rtcProps.channel, rtcProps.appId, callActive, localUid]); + }, [isLoggedIn, callActive, channelName]); return ( { isInitialQueueCompleted, rtmInitTimstamp, hasUserJoinedRTM, - engine: engine.current, + engine: client, localUid: localUid, onlineUsersCount, }}> diff --git a/template/src/components/breakout-room/BreakoutRoomPanel.tsx b/template/src/components/breakout-room/BreakoutRoomPanel.tsx index 722a65198..0093711ba 100644 --- a/template/src/components/breakout-room/BreakoutRoomPanel.tsx +++ b/template/src/components/breakout-room/BreakoutRoomPanel.tsx @@ -9,52 +9,24 @@ information visit https://appbuilder.agora.io. ********************************************* */ -import React, {useEffect} from 'react'; -import {View, StyleSheet, ScrollView} from 'react-native'; +import React from 'react'; +import {View} from 'react-native'; import {isMobileUA, isWebInternal, useIsSmall} from '../../utils/common'; import CommonStyles from '../CommonStyles'; import {getGridLayoutName} from '../../pages/video-call/DefaultLayouts'; -import {BreakoutRoomHeader} from '../../pages/video-call/SidePanelHeader'; import useCaptionWidth from '../../subComponents/caption/useCaptionWidth'; -import {useRoomInfo} from '../room-info/useRoomInfo'; -import {useBreakoutRoom} from './context/BreakoutRoomContext'; -import BreakoutRoomSettings from './ui/BreakoutRoomSettings'; -import BreakoutRoomGroupSettings from './ui/BreakoutRoomGroupSettings'; -import ThemeConfig from '../../theme'; -import TertiaryButton from '../../atoms/TertiaryButton'; + import {useLayout} from '../../utils/useLayout'; +import {useSidePanel} from '../../utils/useSidePanel'; +import {SidePanelType} from '../../subComponents/SidePanelEnum'; + +import BreakoutRoomView from './ui/BreakoutRoomView'; -const BreakoutRoomPanel = props => { - const {showHeader = true} = props; +const BreakoutRoomPanel = () => { + const {setSidePanel} = useSidePanel(); const isSmall = useIsSmall(); const {currentLayout} = useLayout(); const {transcriptHeight} = useCaptionWidth(); - const { - data: {isHost}, - } = useRoomInfo(); - - const { - breakoutSessionId, - checkIfBreakoutRoomSessionExistsAPI, - createBreakoutRoomGroup, - breakoutGroups, - startBreakoutRoomAPI, - closeBreakoutRoomAPI, - } = useBreakoutRoom(); - - useEffect(() => { - const init = async () => { - try { - const activeSession = await checkIfBreakoutRoomSessionExistsAPI(); - if (!activeSession) { - startBreakoutRoomAPI(); - } - } catch (error) { - console.error('Failed to check breakout session:', error); - } - }; - init(); - }, []); return ( { //@ts-ignore transcriptHeight && !isMobileUA() && {height: transcriptHeight}, ]}> - {showHeader && } - - - - - createBreakoutRoomGroup()} - /> - - - {isHost && breakoutSessionId ? ( - - - { - closeBreakoutRoomAPI(); - }} - text={'Close All Rooms'} - /> - - - ) : ( - <> - )} + { + setSidePanel(SidePanelType.None); + }} + /> ); }; -const style = StyleSheet.create({ - footer: { - width: '100%', - padding: 12, - height: 'auto', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'center', - backgroundColor: $config.CARD_LAYER_2_COLOR, - }, - pannelOuterBody: { - display: 'flex', - flex: 1, - }, - panelInnerBody: { - display: 'flex', - flex: 1, - padding: 12, - gap: 12, - }, - fullWidth: { - display: 'flex', - flex: 1, - }, - createBtnContainer: { - backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR, - borderColor: $config.INPUT_FIELD_BORDER_COLOR, - borderRadius: 8, - }, - createBtnText: { - color: $config.PRIMARY_ACTION_BRAND_COLOR, - lineHeight: 20, - fontWeight: '500', - fontSize: ThemeConfig.FontSize.normal, - }, -}); - export default BreakoutRoomPanel; diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 3aa8530b1..2662b1e0a 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -1,4 +1,12 @@ -import React, {useContext, useReducer, useEffect} from 'react'; +import React, { + useContext, + useReducer, + useEffect, + useState, + useCallback, + useMemo, + useRef, +} from 'react'; import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; import {createHook} from 'customization-implementation'; import {randomNameGenerator} from '../../../utils'; @@ -13,9 +21,17 @@ import { breakoutRoomReducer, initialBreakoutRoomState, RoomAssignmentStrategy, + ManualParticipantAssignment, } from '../state/reducer'; import {useLocalUid} from '../../../../agora-rn-uikit'; import {useContent} from '../../../../customization-api'; +import events, {PersistanceLevel} from '../../../rtm-events-api'; +import {BreakoutRoomAction, initialBreakoutGroups} from '../state/reducer'; +import {BreakoutRoomEventNames} from '../events/constants'; +import {BreakoutRoomSyncStateEventPayload} from '../state/types'; +import {IconsInterface} from '../../../atoms/CustomIcon'; +import Toast from '../../../../react-native-toast-message'; +import useBreakoutRoomExit from '../hooks/useBreakoutRoomExit'; const getSanitizedPayload = (payload: BreakoutGroup[]) => { return payload.map(({id, ...rest}) => { @@ -26,50 +42,150 @@ const getSanitizedPayload = (payload: BreakoutGroup[]) => { }); }; +export interface MemberDropdownOption { + type: 'move-to-main' | 'move-to-room' | 'make-presenter'; + icon: keyof IconsInterface; + title: string; + roomId?: string; + roomName?: string; + onOptionPress: () => void; +} + +interface BreakoutRoomPermissions { + // Room navigation + canJoinRoom: boolean; + canExitRoom: boolean; + canSwitchBetweenRooms: boolean; + // Media controls + canScreenshare: boolean; + canRaiseHands: boolean; + // Room management (host only) + canAssignParticipants: boolean; + canCreateRooms: boolean; + canMoveUsers: boolean; + canCloseRooms: boolean; + canMakePresenter: boolean; +} interface BreakoutRoomContextValue { + mainChannelId: string; breakoutSessionId: BreakoutRoomState['breakoutSessionId']; breakoutGroups: BreakoutRoomState['breakoutGroups']; assignmentStrategy: RoomAssignmentStrategy; setStrategy: (strategy: RoomAssignmentStrategy) => void; + canUserSwitchRoom: boolean; + toggleSwitchRooms: (value: boolean) => void; unsassignedParticipants: {uid: UidType; user: ContentInterface}[]; + manualAssignments: ManualParticipantAssignment[]; + setManualAssignments: (assignments: ManualParticipantAssignment[]) => void; + clearManualAssignments: () => void; createBreakoutRoomGroup: (name?: string) => void; - addUserIntoGroup: ( - uid: UidType, - selectGroupId: string, - isHost: boolean, - ) => void; - startBreakoutRoomAPI: () => void; + isUserInRoom: (room?: BreakoutGroup) => boolean; + joinRoom: (roomId: string) => void; + exitRoom: (roomId?: string) => Promise; + closeRoom: (roomId: string) => void; + closeAllRooms: () => void; + updateRoomName: (newRoomName: string, roomId: string) => void; + getAllRooms: () => BreakoutGroup[]; + getRoomMemberDropdownOptions: (memberUid: UidType) => MemberDropdownOption[]; + upsertBreakoutRoomAPI: (type: 'START' | 'UPDATE') => Promise; closeBreakoutRoomAPI: () => void; checkIfBreakoutRoomSessionExistsAPI: () => Promise; - assignParticipants: () => void; + handleAssignParticipants: (strategy: RoomAssignmentStrategy) => void; + sendAnnouncement: (announcement: string) => void; + // Presenters + onMakeMePresenter: (action: 'start' | 'stop') => void; + presenters: {uid: UidType; timestamp: number}[]; + clearAllPresenters: () => void; + // State sync + handleBreakoutRoomSyncState: ( + data: BreakoutRoomSyncStateEventPayload['data']['data'], + ) => void; + permissions: BreakoutRoomPermissions; } const BreakoutRoomContext = React.createContext({ + mainChannelId: '', breakoutSessionId: undefined, unsassignedParticipants: [], breakoutGroups: [], assignmentStrategy: RoomAssignmentStrategy.NO_ASSIGN, setStrategy: () => {}, - assignParticipants: () => {}, + manualAssignments: [], + setManualAssignments: () => {}, + clearManualAssignments: () => {}, + canUserSwitchRoom: false, + toggleSwitchRooms: () => {}, + handleAssignParticipants: () => {}, createBreakoutRoomGroup: () => {}, - addUserIntoGroup: () => {}, - startBreakoutRoomAPI: () => {}, + isUserInRoom: () => false, + joinRoom: () => {}, + exitRoom: async () => {}, + closeRoom: () => {}, + closeAllRooms: () => {}, + updateRoomName: () => {}, + getAllRooms: () => [], + getRoomMemberDropdownOptions: () => [], + sendAnnouncement: () => {}, + upsertBreakoutRoomAPI: async () => {}, closeBreakoutRoomAPI: () => {}, checkIfBreakoutRoomSessionExistsAPI: async () => false, + onMakeMePresenter: () => {}, + presenters: [], + clearAllPresenters: () => {}, + handleBreakoutRoomSyncState: () => {}, + permissions: null, }); -const BreakoutRoomProvider = ({children}: {children: React.ReactNode}) => { +const BreakoutRoomProvider = ({ + children, + mainChannel, +}: { + children: React.ReactNode; + mainChannel: string; +}) => { const {store} = useContext(StorageContext); const {defaultContent, activeUids} = useContent(); const localUid = useLocalUid(); - const [state, dispatch] = useReducer( + const [state, baseDispatch] = useReducer( breakoutRoomReducer, initialBreakoutRoomState, ); const { - data: {roomId}, + data: {isHost, roomId}, } = useRoomInfo(); + const breakoutRoomExit = useBreakoutRoomExit(); + // Sync state + const lastSyncTimeRef = useRef(Date.now()); + console.log('supriya-exit', state.breakoutGroups); + // Join Room + const [selfJoinRoomId, setSelfJoinRoomId] = useState(null); + + // Enhanced dispatch that tracks user actions + const [lastAction, setLastAction] = useState(null); + const dispatch = useCallback((action: BreakoutRoomAction) => { + baseDispatch(action); + setLastAction(action); + }, []); + + // Presenter + const [canIPresent, setICanPresent] = useState(false); + const [presenters, setPresenters] = useState< + {uid: UidType; timestamp: number}[] + >([]); + + // Fetch the data when the context loads. Dont wait till they open panel + useEffect(() => { + const loadData = async () => { + try { + await checkIfBreakoutRoomSessionExistsAPI(); + } catch (error) { + console.error('Failed to load breakout session:', error); + } + }; + loadData(); + }, []); + // Update unassigned participants whenever defaultContent or activeUids change useEffect(() => { // Get currently assigned participants from all rooms @@ -95,6 +211,10 @@ const BreakoutRoomProvider = ({children}: {children: React.ReactNode}) => { if (user.parentUid) { return false; } + // Exclude yourself from assigning + if (uid === localUid) { + return false; + } return true; }) .map(uid => ({ @@ -119,13 +239,15 @@ const BreakoutRoomProvider = ({children}: {children: React.ReactNode}) => { unassignedParticipants: sortedParticipants, }, }); - }, [defaultContent, activeUids, localUid]); + }, [defaultContent, activeUids, localUid, dispatch]); const checkIfBreakoutRoomSessionExistsAPI = async (): Promise => { try { const requestId = getUniqueID(); const response = await fetch( - `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room?passphrase=${roomId.host}`, + `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room?passphrase=${ + isHost ? roomId.host : roomId.attendee + }`, { method: 'GET', headers: { @@ -151,16 +273,13 @@ const BreakoutRoomProvider = ({children}: {children: React.ReactNode}) => { if (data?.session_id) { dispatch({ - type: BreakoutGroupActionTypes.SET_SESSION_ID, - payload: {sessionId: data.session_id}, + type: BreakoutGroupActionTypes.SYNC_STATE, + payload: { + sessionId: data.session_id, + rooms: data?.breakout_room || [], + switchRoom: data.switch_room || false, + }, }); - - if (data?.breakout_room) { - dispatch({ - type: BreakoutGroupActionTypes.SET_GROUPS, - payload: data.breakout_room, - }); - } return true; } @@ -171,26 +290,64 @@ const BreakoutRoomProvider = ({children}: {children: React.ReactNode}) => { } }; - const startBreakoutRoomAPI = () => { - const startReqTs = Date.now(); - const requestId = getUniqueID(); - - fetch(`${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - authorization: store.token ? `Bearer ${store.token}` : '', - 'X-Request-Id': requestId, - 'X-Session-Id': logger.getSessionId(), - }, - body: JSON.stringify({ - passphrase: roomId.host, - switch_room: false, - session_id: state.breakoutSessionId || randomNameGenerator(6), - breakout_room: getSanitizedPayload(state.breakoutGroups), - }), - }) - .then(async response => { + // Polling for sync event + const pollBreakoutGetAPI = useCallback(async () => { + const now = Date.now(); + const timeSinceLastAPICall = now - lastSyncTimeRef.current; + + // If no UPDATE API call in last 30 seconds and we have an active session + if (timeSinceLastAPICall > 30000 && isHost && state.breakoutSessionId) { + console.log( + 'Fallback: Calling breakout session to sync events due to no recent updates', + ); + await checkIfBreakoutRoomSessionExistsAPI(); + lastSyncTimeRef.current = Date.now(); + } + }, [isHost, state.breakoutSessionId]); + + // Automatic interval management with cleanup + useEffect(() => { + if (isHost && state.breakoutSessionId) { + // Check every 15 seconds + const interval = setInterval(pollBreakoutGetAPI, 15000); + // React will automatically call this cleanup function + return () => clearInterval(interval); + } + }, [isHost, state.breakoutSessionId, pollBreakoutGetAPI]); + + const upsertBreakoutRoomAPI = useCallback( + async (type: 'START' | 'UPDATE' = 'START') => { + const startReqTs = Date.now(); + const requestId = getUniqueID(); + try { + const payload = { + passphrase: roomId.host, + switch_room: state.canUserSwitchRoom, + session_id: state.breakoutSessionId || randomNameGenerator(6), + breakout_room: + type === 'START' + ? getSanitizedPayload(initialBreakoutGroups) + : getSanitizedPayload(state.breakoutGroups), + }; + + // Only add join_room_id if there's a pending join + if (selfJoinRoomId) { + payload.join_room_id = selfJoinRoomId; + } + + const response = await fetch( + `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: store.token ? `Bearer ${store.token}` : '', + 'X-Request-Id': requestId, + 'X-Session-Id': logger.getSessionId(), + }, + body: JSON.stringify(payload), + }, + ); const endRequestTs = Date.now(); const latency = endRequestTs - startReqTs; if (!response.ok) { @@ -198,27 +355,40 @@ const BreakoutRoomProvider = ({children}: {children: React.ReactNode}) => { throw new Error(`Breakout room creation failed: ${msg}`); } else { const data = await response.json(); - console.log('supriya res', response); - if (data?.session_id) { + if (selfJoinRoomId) { + setSelfJoinRoomId(null); + } + if (type === 'START' && data?.session_id) { dispatch({ type: BreakoutGroupActionTypes.SET_SESSION_ID, payload: {sessionId: data.session_id}, }); - - if (data?.breakout_room) { - dispatch({ - type: BreakoutGroupActionTypes.SET_GROUPS, - payload: data.breakout_room, - }); - } + } + if (data?.breakout_room) { + dispatch({ + type: BreakoutGroupActionTypes.UPDATE_GROUPS_IDS, + payload: data.breakout_room, + }); + } + if (type === 'UPDATE') { + lastSyncTimeRef.current = Date.now(); } } - }) - .catch(err => { + } catch (err) { console.log('debugging err', err); - }); - }; + } + }, + [ + roomId.host, + state.breakoutSessionId, + state.breakoutGroups, + state.canUserSwitchRoom, + store.token, + dispatch, + selfJoinRoomId, + ], + ); const closeBreakoutRoomAPI = () => { console.log('supriya close breakout room API not yet implemented'); @@ -230,43 +400,591 @@ const BreakoutRoomProvider = ({children}: {children: React.ReactNode}) => { payload: {strategy}, }); }; + + const setManualAssignments = useCallback( + (assignments: ManualParticipantAssignment[]) => { + dispatch({ + type: BreakoutGroupActionTypes.SET_MANUAL_ASSIGNMENTS, + payload: {assignments}, + }); + }, + [dispatch], + ); + + const clearManualAssignments = useCallback(() => { + dispatch({ + type: BreakoutGroupActionTypes.CLEAR_MANUAL_ASSIGNMENTS, + }); + }, [dispatch]); + + const toggleSwitchRooms = (value: boolean) => { + dispatch({ + type: BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, + payload: { + canUserSwitchRoom: value, + }, + }); + }; + const createBreakoutRoomGroup = () => { dispatch({ type: BreakoutGroupActionTypes.CREATE_GROUP, }); }; - const addUserIntoGroup = ( - uid: UidType, - toGroupId: string, - isHost: boolean, - ) => { + const handleAssignParticipants = (strategy: RoomAssignmentStrategy) => { + if (strategy === RoomAssignmentStrategy.AUTO_ASSIGN) { + dispatch({ + type: BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, + }); + } + if (strategy === RoomAssignmentStrategy.MANUAL_ASSIGN) { + dispatch({ + type: BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, + }); + } + }; + + const moveUserToMainRoom = (user: ContentInterface) => { + try { + // Find user's current breakout group + const currentGroup = state.breakoutGroups.find( + group => + group.participants.hosts.includes(user.uid) || + group.participants.attendees.includes(user.uid), + ); + // Dispatch action to remove user from breakout group + if (currentGroup) { + dispatch({ + type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, + payload: { + user: user, + fromGroupId: currentGroup.id, + }, + }); + } + console.log(`supriya User ${user.name} (${user.uid}) moved to main room`); + } catch (error) { + console.error('supriya Error moving user to main room:', error); + } + }; + + const moveUserIntoGroup = (user: ContentInterface, toGroupId: string) => { + console.log('supriya move user to another room', user, toGroupId); + try { + // Find user's current breakout group + const currentGroup = state.breakoutGroups.find( + group => + group.participants.hosts.includes(user.uid) || + group.participants.attendees.includes(user.uid), + ); + // Find target group + const targetGroup = state.breakoutGroups.find( + group => group.id === toGroupId, + ); + if (!targetGroup) { + console.error('Target group not found:', toGroupId); + return; + } + // Dispatch action to move user between groups + dispatch({ + type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, + payload: { + user: user, + fromGroupId: currentGroup?.id, + toGroupId, + }, + }); + + console.log( + `supriya User ${user.name} (${user.uid}) moved to ${targetGroup.name}`, + ); + } catch (error) { + console.error('supriya Error moving user to breakout room:', error); + } + }; + + // To check if current user is in a specific room + const isUserInRoom = useCallback( + (room?: BreakoutGroup): boolean => { + if (room) { + // Check specific room + return ( + room.participants.hosts.includes(localUid) || + room.participants.attendees.includes(localUid) + ); + } else { + // Check ALL rooms - is user in any room? + return state.breakoutGroups.some( + group => + group.participants.hosts.includes(localUid) || + group.participants.attendees.includes(localUid), + ); + } + }, + [localUid, state.breakoutGroups], + ); + + // Function to get current room + const getCurrentRoom = useCallback((): BreakoutGroup => { + const userRoom = state.breakoutGroups.find( + group => + group.participants.hosts.includes(localUid) || + group.participants.attendees.includes(localUid), + ); + return userRoom ? userRoom : null; + }, [localUid, state.breakoutGroups]); + + const joinRoom = (toRoomId: string) => { + const user = defaultContent[localUid]; + moveUserIntoGroup(user, toRoomId); + setSelfJoinRoomId(toRoomId); + }; + + const exitRoom = async (fromRoomId?: string) => { + try { + const localUser = defaultContent[localUid]; + const currentRoomId = fromRoomId ? fromRoomId : getCurrentRoom()?.id; + if (currentRoomId) { + // Use breakout-specific exit (doesn't destroy main RTM) + await breakoutRoomExit(); + + dispatch({ + type: BreakoutGroupActionTypes.EXIT_GROUP, + payload: { + user: localUser, + fromGroupId: currentRoomId, + }, + }); + } + } catch (error) { + const localUser = defaultContent[localUid]; + const currentRoom = getCurrentRoom(); + if (currentRoom) { + dispatch({ + type: BreakoutGroupActionTypes.EXIT_GROUP, + payload: { + user: localUser, + fromGroupId: currentRoom.id, + }, + }); + } + } + }; + + const closeRoom = (roomIdToClose: string) => { + dispatch({ + type: BreakoutGroupActionTypes.CLOSE_GROUP, + payload: { + groupId: roomIdToClose, + }, + }); + }; + + const closeAllRooms = () => { dispatch({ - type: BreakoutGroupActionTypes.MOVE_PARTICIPANT, - payload: {uid, fromGroupId: null, toGroupId, isHost}, + type: BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, }); }; - const assignParticipants = () => { + const sendAnnouncement = (announcement: string) => { + events.send( + BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT, + JSON.stringify({ + uid: localUid, + timestamp: Date.now(), + announcement, + }), + ); + }; + + const updateRoomName = (newRoomName: string, roomIdToEdit: string) => { dispatch({ - type: BreakoutGroupActionTypes.ASSIGN_PARTICPANTS, + type: BreakoutGroupActionTypes.RENAME_GROUP, + payload: { + newName: newRoomName, + groupId: roomIdToEdit, + }, + }); + }; + + const getAllRooms = () => { + return state.breakoutGroups.length > 0 ? state.breakoutGroups : []; + }; + + const getRoomMemberDropdownOptions = (memberUid: UidType) => { + const options: MemberDropdownOption[] = []; + // Find which room the user is currently in + const getCurrentUserRoom = (uid: UidType) => { + return state.breakoutGroups.find( + group => + group.participants.hosts.includes(uid) || + group.participants.attendees.includes(uid), + ); + }; + const currentRoom = getCurrentUserRoom(memberUid); + const memberUser = defaultContent[memberUid]; + // Move to Main Room option + options.push({ + icon: 'double-up-arrow', + type: 'move-to-main', + title: 'Move to Main Room', + onOptionPress: () => moveUserToMainRoom(memberUser), }); + + // Move to other breakout rooms (exclude current room) + state.breakoutGroups + .filter(group => group.id !== currentRoom?.id) + .forEach(group => { + options.push({ + type: 'move-to-room', + icon: 'move-up', + title: `Shift to ${group.name}`, + roomId: group.id, + roomName: group.name, + onOptionPress: () => moveUserIntoGroup(memberUser, group.id), + }); + }); + + // Make presenter option (only for hosts) + if (isHost) { + const userIsPresenting = isUserPresenting(memberUid); + const title = userIsPresenting ? 'Stop presenter' : 'Make a Presenter'; + const action = userIsPresenting ? 'stop' : 'start'; + options.push({ + type: 'make-presenter', + icon: 'promote-filled', + title: title, + onOptionPress: () => makePresenter(memberUser, action), + }); + } + return options; + }; + + const isUserPresenting = useCallback( + (uid?: UidType) => { + if (uid) { + // Check specific user + return presenters.some(presenter => presenter.uid === uid); + } else { + // Check current user (same as canIPresent) + return false; + } + }, + [presenters], + ); + + // User wants to start presenting + const makePresenter = (user: ContentInterface, action: 'start' | 'stop') => { + try { + // Host can make someone a presenter + events.send( + BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, + JSON.stringify({ + uid: user.uid, + timestamp: Date.now(), + action: action, + }), + PersistanceLevel.None, + user.uid, + ); + if (action === 'start') { + addPresenter(user.uid); + } else if (action === 'stop') { + removePresenter(user.uid); + } + } catch (error) { + console.log('Error making user presenter:', error); + } }; + // Presenter management functions (called by event handlers) + const addPresenter = useCallback((uid: UidType) => { + setPresenters(prev => { + // Check if already presenting to avoid duplicates + const exists = prev.find(presenter => presenter.uid === uid); + if (exists) { + return prev; + } + return [...prev, {uid, timestamp: Date.now()}]; + }); + }, []); + + const removePresenter = useCallback((uid: UidType) => { + if (uid) { + setPresenters(prev => prev.filter(presenter => presenter.uid !== uid)); + } + }, []); + + const onMakeMePresenter = (action: 'start' | 'stop') => { + if (action === 'start') { + setICanPresent(true); + } else if (action === 'stop') { + setICanPresent(false); + } + }; + + const clearAllPresenters = useCallback(() => { + setPresenters([]); + }, []); + + // Calculate permissions dynamically + const permissions = useMemo((): BreakoutRoomPermissions => { + const currentlyInRoom = isUserInRoom(); + console.log('supriya-exit currentlyInRoom: ', currentlyInRoom); + const hasAvailableRooms = state.breakoutGroups.length > 0; + const canUserSwitchRoom = state.canUserSwitchRoom; + + if (true) { + return { + // Room navigation + canJoinRoom: + !currentlyInRoom && + hasAvailableRooms && + (isHost || canUserSwitchRoom), + canExitRoom: currentlyInRoom, + canSwitchBetweenRooms: + currentlyInRoom && hasAvailableRooms && (isHost || canUserSwitchRoom), + // Media controls + canScreenshare: currentlyInRoom ? canIPresent : isHost, + canRaiseHands: $config.RAISE_HAND && !isHost, + // Room management (host only) + canAssignParticipants: isHost, + canCreateRooms: isHost, + canMoveUsers: isHost, + canCloseRooms: isHost && hasAvailableRooms && !!state.breakoutSessionId, + canMakePresenter: isHost, + }; + } + return { + canJoinRoom: false, + canExitRoom: false, + canSwitchBetweenRooms: false, // Media controls + canScreenshare: true, + canRaiseHands: false, + // Room management (host only) + canAssignParticipants: false, + canCreateRooms: false, + canMoveUsers: false, + canCloseRooms: false, + canMakePresenter: false, + }; + }, [ + isUserInRoom, + isHost, + state.canUserSwitchRoom, + state.breakoutGroups, + state.breakoutSessionId, + canIPresent, + ]); + + const handleBreakoutRoomSyncState = useCallback( + (data: BreakoutRoomSyncStateEventPayload['data']['data']) => { + const {switch_room, breakout_room} = data; + + // Store previous state to compare changes + const prevGroups = state.breakoutGroups; + const prevSwitchRoom = state.canUserSwitchRoom; + const userCurrentRoom = getCurrentRoom(); + const userCurrentRoomId = userCurrentRoom.id; + + dispatch({ + type: BreakoutGroupActionTypes.SYNC_STATE, + payload: { + sessionId: data.session_id, + switchRoom: data.switch_room, + rooms: data.breakout_room, + }, + }); + + // Show notifications based on changes + // 1. Switch room enabled notification + if (switch_room && !prevSwitchRoom) { + Toast.show({ + leadingIconName: 'info', + type: 'info', + text1: 'Breakout rooms are now open. Please choose a room to join.', + visibilityTime: 4000, + }); + return; // Don't show other notifications when rooms first open + } + + // 2. User joined a room (compare previous and current state) + if (userCurrentRoomId) { + const wasInRoom = prevGroups.some( + group => + group.participants.hosts.includes(localUid) || + group.participants.attendees.includes(localUid), + ); + + if (!wasInRoom) { + const currentRoom = breakout_room.find( + room => room.id === userCurrentRoomId, + ); + Toast.show({ + type: 'success', + text1: `You've joined ${currentRoom?.name || 'a breakout room'}.`, + visibilityTime: 3000, + }); + return; + } + } + + // 3. User was moved to a different room by host + if (userCurrentRoom) { + const prevUserRoom = prevGroups.find( + group => + group.participants.hosts.includes(localUid) || + group.participants.attendees.includes(localUid), + ); + + if (prevUserRoom && prevUserRoom.id !== userCurrentRoomId) { + Toast.show({ + type: 'info', + text1: `You've been moved to ${userCurrentRoom.name} by the host.`, + visibilityTime: 4000, + }); + return; + } + } + + // 4. User was moved to main room + if (!userCurrentRoom) { + const wasInRoom = prevGroups.some( + group => + group.participants.hosts.includes(localUid) || + group.participants.attendees.includes(localUid), + ); + + if (wasInRoom) { + Toast.show({ + leadingIconName: 'arrow-up', + type: 'info', + text1: "You've returned to the main room.", + visibilityTime: 3000, + }); + return; + } + } + + // 5. All breakout rooms closed + if (breakout_room.length === 0 && prevGroups.length > 0) { + Toast.show({ + leadingIconName: 'close', + type: 'warning', + text1: 'Breakout rooms are now closed. Returning to the main room...', + visibilityTime: 4000, + }); + return; + } + + // 6. Specific room was closed (user was in it) + if (userCurrentRoomId) { + const roomStillExists = breakout_room.some( + room => room.id === userCurrentRoomId, + ); + if (!roomStillExists) { + const closedRoom = prevGroups.find( + room => room.id === userCurrentRoomId, + ); + Toast.show({ + leadingIconName: 'alert', + type: 'error', + text1: `${ + closedRoom?.name || 'Your room' + } is currently closed. Returning to main room. Please + contact the host.`, + visibilityTime: 5000, + }); + return; + } + } + + // 7. Room name changed + prevGroups.forEach(prevRoom => { + const currentRoom = breakout_room.find(room => room.id === prevRoom.id); + if (currentRoom && currentRoom.name !== prevRoom.name) { + Toast.show({ + type: 'info', + text1: `${prevRoom.name} has been renamed to '${currentRoom.name}'.`, + visibilityTime: 3000, + }); + } + }); + }, + [ + dispatch, + getCurrentRoom, + localUid, + state.breakoutGroups, + state.canUserSwitchRoom, + ], + ); + + // Action-based API triggering + useEffect(() => { + if (!lastAction || !lastAction.type) { + return; + } + console.log('supriya-exit 1 lastAction: ', lastAction.type); + + // Actions that should trigger API calls + const API_TRIGGERING_ACTIONS = [ + BreakoutGroupActionTypes.CREATE_GROUP, + BreakoutGroupActionTypes.RENAME_GROUP, + BreakoutGroupActionTypes.CLOSE_GROUP, + BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, + BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, + BreakoutGroupActionTypes.EXIT_GROUP, + ]; + + const shouldCallAPI = + API_TRIGGERING_ACTIONS.includes(lastAction.type as any) && isHost; + + if (shouldCallAPI) { + upsertBreakoutRoomAPI('UPDATE').finally(() => {}); + } else { + console.log(`Action ${lastAction.type} - skipping API call`); + } + }, [lastAction, upsertBreakoutRoomAPI, isHost]); + return ( {children} diff --git a/template/src/components/breakout-room/context/BreakoutRoomEngineContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomEngineContext.tsx deleted file mode 100644 index 7a723be0c..000000000 --- a/template/src/components/breakout-room/context/BreakoutRoomEngineContext.tsx +++ /dev/null @@ -1,118 +0,0 @@ -import React, {createContext, useCallback, useRef} from 'react'; -import RtcEngine, {createAgoraRtcEngine} from '../../../../bridge/rtc/webNg'; -import {ChannelProfileType} from '../../../../agora-rn-uikit'; -import {ConnectionState} from 'agora-rtc-sdk-ng'; -import {RtcConnection} from 'react-native-agora'; -import {createHook} from 'customization-implementation'; -import {BreakoutGroupActionTypes} from '../state/reducer'; -import { - useBreakoutRoomDispatch, - useBreakoutRoomState, -} from './BreakoutRoomStateContext'; - -// Context -const BreakoutRoomEngineContext = createContext<{ - joinRtcChannel: ( - roomId: string, - config: { - token: string; - channelName: string; - optionalUid: number; - }, - ) => Promise; - leaveRtcChannel: () => Promise; -} | null>(null); - -// Provider -const BreakoutRoomEngineProvider: React.FC<{ - children: React.ReactNode; -}> = ({children}) => { - const state = useBreakoutRoomState(); - const dispatch = useBreakoutRoomDispatch(); - - const breakoutEngineRf = useRef(null); - - const onBreakoutRoomChannelStateChanged = useCallback( - (_connection: RtcConnection, currState: ConnectionState) => { - dispatch({ - type: BreakoutGroupActionTypes.ENGINE_SET_CHANNEL_STATUS, - payload: {status: currState}, - }); - }, - [], - ); - - const joinRtcChannel = useCallback( - async ( - roomId: string, - { - token, - channelName, - optionalUid = null, - }: { - token: string; - channelName: string; - optionalUid?: number | null; - }, - ) => { - let appId = $config.APP_ID; - let channelProfile = ChannelProfileType.ChannelProfileLiveBroadcasting; - if (!breakoutEngineRf.current) { - let engine = createAgoraRtcEngine(); - engine.addListener( - 'onConnectionStateChanged', - onBreakoutRoomChannelStateChanged, - ); - breakoutEngineRf.current = engine; // ✅ set ref - - dispatch({ - type: BreakoutGroupActionTypes.ENGINE_INIT, - payload: {engine}, - }); - // Add listeners here - } - console.log('supriya 3'); - try { - // Initialize RtcEngine - await breakoutEngineRf.current.initialize({appId}); - await breakoutEngineRf.current.setChannelProfile(channelProfile); - // Join RtcChannel - await breakoutEngineRf.current.joinChannel( - token, - channelName, - optionalUid, - {}, - ); - } catch (e) { - console.error(`[${roomId}] Failed to join channel`, e); - throw e; - } - }, - [dispatch, onBreakoutRoomChannelStateChanged], - ); - - const leaveRtcChannel = useCallback(async () => { - if (state.breakoutGroupRtc.engine) { - await state.breakoutGroupRtc.engine.leaveChannel(); - await state.breakoutGroupRtc.engine.release(); - dispatch({ - type: BreakoutGroupActionTypes.ENGINE_LEAVE_AND_DESTROY, - }); - } - }, [dispatch, state.breakoutGroupRtc.engine]); - - const value = { - joinRtcChannel, - leaveRtcChannel, - }; - - return ( - - {children} - - ); -}; - -const useBreakoutRoomEngine = createHook(BreakoutRoomEngineContext); - -export {useBreakoutRoomEngine, BreakoutRoomEngineProvider}; diff --git a/template/src/components/breakout-room/context/BreakoutRoomStateContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomStateContext.tsx deleted file mode 100644 index 5f07d5273..000000000 --- a/template/src/components/breakout-room/context/BreakoutRoomStateContext.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, {createContext, useReducer} from 'react'; -import { - breakoutRoomReducer, - initialBreakoutRoomState, - BreakoutRoomAction, - BreakoutRoomState, -} from '../state/reducer'; -import {createHook} from 'customization-implementation'; - -export const BreakoutRoomStateContext = createContext< - BreakoutRoomState | undefined ->(undefined); -export const BreakoutRoomDispatchContext = createContext< - React.Dispatch | undefined ->(undefined); - -const BreakoutRoomStateProvider = ({children}: {children: React.ReactNode}) => { - const [state, dispatch] = useReducer( - breakoutRoomReducer, - initialBreakoutRoomState, - ); - - return ( - - - {children} - - - ); -}; - -const useBreakoutRoomState = createHook(BreakoutRoomStateContext); -const useBreakoutRoomDispatch = createHook(BreakoutRoomDispatchContext); - -export { - useBreakoutRoomState, - useBreakoutRoomDispatch, - BreakoutRoomStateProvider, -}; diff --git a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx new file mode 100644 index 000000000..06dd9839e --- /dev/null +++ b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx @@ -0,0 +1,110 @@ +import React, {useEffect} from 'react'; +import events from '../../../rtm-events-api'; +import {BreakoutRoomEventNames} from './constants'; +import Toast from '../../../../react-native-toast-message'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import {BreakoutRoomSyncStateEventPayload} from '../state/types'; + +interface Props { + children: React.ReactNode; + mainChannelName: string; +} + +const BreakoutRoomEventsConfigure: React.FC = ({ + children, + mainChannelName, +}) => { + const {onMakeMePresenter, handleBreakoutRoomSyncState} = useBreakoutRoom(); + + useEffect(() => { + const handleHandRaiseEvent = (evtData: any) => { + console.log('supriya BREAKOUT_ROOM_ATTENDEE_RAISE_HAND data: ', evtData); + try { + const {uid, payload} = evtData; + const data = JSON.parse(payload); + // uid timestamp action + if (data.action === 'raise') { + } else if (data.action === 'lower') { + } + } catch (error) {} + }; + + const handlePresenterStatusEvent = (evtData: any) => { + console.log('supriya BREAKOUT_ROOM_MAKE_PRESENTER data: ', evtData); + try { + const {payload} = evtData; + const data = JSON.parse(payload); + if (data.action === 'start') { + onMakeMePresenter('start'); + } else if (data.action === 'stop') { + onMakeMePresenter('stop'); + } + } catch (error) {} + }; + + const handleAnnouncementEvent = (evtData: any) => { + console.log('supriya BREAKOUT_ROOM_ANNOUNCEMENT data: ', evtData); + try { + const {_, payload} = evtData; + const data = JSON.parse(payload); + if (data.announcement) { + Toast.show({ + leadingIconName: 'speaker', + type: 'info', + text1: `Message from host: :${data.announcement}`, + visibilityTime: 3000, + primaryBtn: null, + secondaryBtn: null, + leadingIcon: null, + }); + } + } catch (error) {} + }; + + const handleBreakoutRoomStateSync = (evtData: any) => { + const {payload} = evtData; + console.log('supriya BREAKOUT_ROOM_SYNC_STATE data: ', evtData); + const data: BreakoutRoomSyncStateEventPayload = JSON.parse(payload); + if (data.data.act === 'SYNC_STATE') { + handleBreakoutRoomSyncState(data.data.data); + } + }; + + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT, + handleAnnouncementEvent, + ); + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, + handlePresenterStatusEvent, + ); + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, + handleHandRaiseEvent, + ); + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, + handleBreakoutRoomStateSync, + ); + + return () => { + events.off(BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT); + events.off( + BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, + handlePresenterStatusEvent, + ); + events.off( + BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, + handleHandRaiseEvent, + ); + events.off( + BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, + handleBreakoutRoomStateSync, + ); + }; + }, [onMakeMePresenter, handleBreakoutRoomSyncState]); + + return <>{children}; +}; + +export default BreakoutRoomEventsConfigure; diff --git a/template/src/components/breakout-room/events/constants.ts b/template/src/components/breakout-room/events/constants.ts new file mode 100644 index 000000000..4005e370d --- /dev/null +++ b/template/src/components/breakout-room/events/constants.ts @@ -0,0 +1,15 @@ +// 9. BREAKOUT ROOM +const BREAKOUT_ROOM_JOIN_DETAILS = 'BREAKOUT_ROOM_BREAKOUT_ROOM_JOIN_DETAILS'; +const BREAKOUT_ROOM_SYNC_STATE = 'BREAKOUT_ROOM_BREAKOUT_ROOM_STATE'; +const BREAKOUT_ROOM_ANNOUNCEMENT = 'BREAKOUT_ROOM_ANNOUNCEMENT'; +const BREAKOUT_ROOM_MAKE_PRESENTER = 'BREAKOUT_ROOM_MAKE_PRESENTER'; +const BREAKOUT_ROOM_ATTENDEE_RAISE_HAND = 'BREAKOUT_ROOM_ATTENDEE_RAISE_HAND'; + +const BreakoutRoomEventNames = { + BREAKOUT_ROOM_JOIN_DETAILS, + BREAKOUT_ROOM_SYNC_STATE, + BREAKOUT_ROOM_ANNOUNCEMENT, + BREAKOUT_ROOM_MAKE_PRESENTER, + BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, +}; +export {BreakoutRoomEventNames}; diff --git a/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts b/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts new file mode 100644 index 000000000..3d8b288d7 --- /dev/null +++ b/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts @@ -0,0 +1,37 @@ +import {useContext} from 'react'; +import {useHistory, useParams} from '../../../components/Router'; +import {useCaption, useContent, useSTTAPI} from 'customization-api'; + +const useBreakoutRoomExit = () => { + const history = useHistory(); + const {phrase} = useParams<{phrase: string}>(); + const {defaultContent} = useContent(); + const {stop: stopSTTAPI} = useSTTAPI(); + const {isSTTActive} = useCaption(); + + return async () => { + try { + // stopping STT on call end,if only last user is remaining in call + const usersInCall = Object.entries(defaultContent).filter( + item => + item[1].type === 'rtc' && + item[1].isHost === 'true' && + !item[1].offline, + ); + if (usersInCall.length === 1 && isSTTActive) { + console.log('Stopping stt api as only one host is in the call'); + stopSTTAPI().catch(error => { + console.log('Error stopping stt', error); + }); + } + // 2. Navigate back to main room (if not skipped) + history.push(`/${phrase}`); + } catch (error) { + history.push(`/${phrase}`); + + throw error; // Re-throw so caller can handle + } + }; +}; + +export default useBreakoutRoomExit; diff --git a/template/src/components/breakout-room/state/reducer.ts b/template/src/components/breakout-room/state/reducer.ts index 8cd71baa5..d589fe183 100644 --- a/template/src/components/breakout-room/state/reducer.ts +++ b/template/src/components/breakout-room/state/reducer.ts @@ -1,5 +1,4 @@ import {ContentInterface, UidType} from '../../../../agora-rn-uikit/src'; -import {BreakoutChannelJoinEventPayload} from '../state/types'; import {randomNameGenerator} from '../../../utils'; export enum RoomAssignmentStrategy { @@ -7,6 +6,12 @@ export enum RoomAssignmentStrategy { MANUAL_ASSIGN = 'manual-assign', NO_ASSIGN = 'no-assign', } +export interface ManualParticipantAssignment { + uid: UidType; + roomId: string | null; // null means stay in main room + isHost: boolean; + isSelected: boolean; +} export interface BreakoutGroup { id: string; @@ -20,68 +25,127 @@ export interface BreakoutRoomState { breakoutSessionId: string; breakoutGroups: BreakoutGroup[]; unassignedParticipants: {uid: UidType; user: ContentInterface}[]; + manualAssignments: ManualParticipantAssignment[]; assignmentStrategy: RoomAssignmentStrategy; - activeBreakoutGroup: { - id: number | string; - name: string; - channelInfo: BreakoutChannelJoinEventPayload['data']['data']; - }; + canUserSwitchRoom: boolean; } +export const initialBreakoutGroups = [ + { + name: 'Room 1', + id: `temp_${randomNameGenerator(6)}`, + participants: {hosts: [], attendees: []}, + }, + { + name: 'Room 2', + id: `temp_${randomNameGenerator(6)}`, + participants: {hosts: [], attendees: []}, + }, +]; + export const initialBreakoutRoomState: BreakoutRoomState = { breakoutSessionId: '', assignmentStrategy: RoomAssignmentStrategy.AUTO_ASSIGN, + canUserSwitchRoom: false, unassignedParticipants: [], - breakoutGroups: [ - { - name: 'Room 1', - id: `temp_${randomNameGenerator(6)}`, - participants: {hosts: [], attendees: []}, - }, - { - name: 'Room 2', - id: `temp_${randomNameGenerator(6)}`, - participants: {hosts: [], attendees: []}, - }, - ], - activeBreakoutGroup: { - id: undefined, - name: '', - channelInfo: undefined, - }, + manualAssignments: [], + breakoutGroups: [], }; export const BreakoutGroupActionTypes = { + // Initial state + SYNC_STATE: 'BREAKOUT_ROOM/SYNC_STATE', // session SET_SESSION_ID: 'BREAKOUT_ROOM/SET_SESSION_ID', // strategy SET_ASSIGNMENT_STRATEGY: 'BREAKOUT_ROOM/SET_ASSIGNMENT_STRATEGY', + // Manual assignment strategy + SET_MANUAL_ASSIGNMENTS: 'BREAKOUT_ROOM/SET_MANUAL_ASSIGNMENTS', + CLEAR_MANUAL_ASSIGNMENTS: 'BREAKOUT_ROOM/CLEAR_MANUAL_ASSIGNMENTS', + // switch room + SET_ALLOW_PEOPLE_TO_SWITCH_ROOM: + 'BREAKOUT_ROOM/SET_ALLOW_PEOPLE_TO_SWITCH_ROOM', // Group management SET_GROUPS: 'BREAKOUT_ROOM/SET_GROUPS', + UPDATE_GROUPS_IDS: 'BREAKOUT_ROOM/UPDATE_GROUPS_IDS', CREATE_GROUP: 'BREAKOUT_ROOM/CREATE_GROUP', + RENAME_GROUP: 'BREAKOUT_ROOM/RENAME_GROUP', + EXIT_GROUP: 'BREAKOUT_ROOM/EXIT_GROUP', + CLOSE_GROUP: 'BREAKOUT_ROOM/CLOSE_GROUP', + CLOSE_ALL_GROUPS: 'BREAKOUT_ROOM/CLOSE_ALL_GROUPS', // Participants Assignment UPDATE_UNASSIGNED_PARTICIPANTS: 'BREAKOUT_ROOM/UPDATE_UNASSIGNED_PARTICIPANTS', - ASSIGN_PARTICPANTS: 'BREAKOUT_ROOM/ASSIGN_PARTICPANTS', - MOVE_PARTICIPANT: 'BREAKOUT_ROOM/MOVE_PARTICIPANT', + AUTO_ASSIGN_PARTICPANTS: 'BREAKOUT_ROOM/AUTO_ASSIGN_PARTICPANTS', + MANUAL_ASSIGN_PARTICPANTS: 'BREAKOUT_ROOM/MANUAL_ASSIGN_PARTICPANTS', + MOVE_PARTICIPANT_TO_MAIN: 'BREAKOUT_ROOM/MOVE_PARTICIPANT_TO_MAIN', + MOVE_PARTICIPANT_TO_GROUP: 'BREAKOUT_ROOM/MOVE_PARTICIPANT_TO_GROUP', } as const; export type BreakoutRoomAction = + | { + type: typeof BreakoutGroupActionTypes.SYNC_STATE; + payload: { + sessionId: BreakoutRoomState['breakoutSessionId']; + switchRoom: BreakoutRoomState['canUserSwitchRoom']; + rooms: BreakoutRoomState['breakoutGroups']; + }; + } | { type: typeof BreakoutGroupActionTypes.SET_SESSION_ID; payload: {sessionId: string}; } + | { + type: typeof BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY; + payload: { + strategy: RoomAssignmentStrategy; + }; + } + | { + type: typeof BreakoutGroupActionTypes.SET_MANUAL_ASSIGNMENTS; + payload: { + assignments: ManualParticipantAssignment[]; + }; + } + | { + type: typeof BreakoutGroupActionTypes.CLEAR_MANUAL_ASSIGNMENTS; + } + | { + type: typeof BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM; + payload: { + canUserSwitchRoom: boolean; + }; + } | { type: typeof BreakoutGroupActionTypes.SET_GROUPS; payload: BreakoutGroup[]; } | { - type: typeof BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY; + type: typeof BreakoutGroupActionTypes.UPDATE_GROUPS_IDS; + payload: BreakoutGroup[]; + } + | {type: typeof BreakoutGroupActionTypes.CREATE_GROUP} + | { + type: typeof BreakoutGroupActionTypes.CLOSE_GROUP; payload: { - strategy: RoomAssignmentStrategy; + groupId: string; + }; + } + | {type: typeof BreakoutGroupActionTypes.CLOSE_ALL_GROUPS} + | { + type: typeof BreakoutGroupActionTypes.RENAME_GROUP; + payload: { + newName: string; + groupId: string; + }; + } + | { + type: typeof BreakoutGroupActionTypes.EXIT_GROUP; + payload: { + user: ContentInterface; + fromGroupId: string; }; } - | {type: typeof BreakoutGroupActionTypes.CREATE_GROUP} | { type: typeof BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS; payload: { @@ -89,15 +153,24 @@ export type BreakoutRoomAction = }; } | { - type: typeof BreakoutGroupActionTypes.ASSIGN_PARTICPANTS; + type: typeof BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS; + } + | { + type: typeof BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS; } | { - type: typeof BreakoutGroupActionTypes.MOVE_PARTICIPANT; + type: typeof BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN; payload: { - uid: UidType; + user: ContentInterface; + fromGroupId: string; + }; + } + | { + type: typeof BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP; + payload: { + user: ContentInterface; fromGroupId: string; toGroupId: string; - isHost: boolean; }; }; @@ -106,6 +179,20 @@ export const breakoutRoomReducer = ( action: BreakoutRoomAction, ): BreakoutRoomState => { switch (action.type) { + case BreakoutGroupActionTypes.SYNC_STATE: { + return { + ...state, + breakoutSessionId: action.payload.sessionId, + canUserSwitchRoom: action.payload.switchRoom, + breakoutGroups: action.payload.rooms.map(group => ({ + ...group, + participants: { + hosts: group.participants?.hosts ?? [], + attendees: group.participants?.attendees ?? [], + }, + })), + }; + } // group management cases case BreakoutGroupActionTypes.SET_SESSION_ID: { return {...state, breakoutSessionId: action.payload.sessionId}; @@ -124,6 +211,19 @@ export const breakoutRoomReducer = ( }; } + case BreakoutGroupActionTypes.UPDATE_GROUPS_IDS: { + return { + ...state, + breakoutGroups: action.payload.map(group => ({ + ...group, + participants: { + hosts: group.participants?.hosts ?? [], + attendees: group.participants?.attendees ?? [], + }, + })), + }; + } + case BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS: { return { ...state, @@ -138,13 +238,65 @@ export const breakoutRoomReducer = ( }; } - case BreakoutGroupActionTypes.ASSIGN_PARTICPANTS: { + case BreakoutGroupActionTypes.SET_MANUAL_ASSIGNMENTS: + return { + ...state, + manualAssignments: action.payload.assignments, + }; + + case BreakoutGroupActionTypes.CLEAR_MANUAL_ASSIGNMENTS: + return { + ...state, + manualAssignments: [], + }; + + case BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS: + // Only applies when strategy is MANUAL + const updatedGroups = state.breakoutGroups.map(group => { + const roomAssignments = state.manualAssignments.filter( + assignment => assignment.roomId === group.id, + ); + + const hostsToAdd = roomAssignments + .filter(assignment => assignment.isHost) + .map(assignment => assignment.uid); + + const attendeesToAdd = roomAssignments + .filter(assignment => !assignment.isHost) + .map(assignment => assignment.uid); + + return { + ...group, + participants: { + hosts: [...group.participants.hosts, ...hostsToAdd], + attendees: [...group.participants.attendees, ...attendeesToAdd], + }, + }; + }); + + return { + ...state, + breakoutGroups: updatedGroups, + manualAssignments: [], // Clear after applying + }; + + case BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM: { + return { + ...state, + canUserSwitchRoom: action.payload.canUserSwitchRoom, + }; + } + + case BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS: { const selectedStrategy = state.assignmentStrategy; - const roomAssignments = new Map(); + const roomAssignments = new Map< + string, + {hosts: UidType[]; attendees: UidType[]} + >(); // Initialize empty arrays for each room state.breakoutGroups.forEach(room => { - roomAssignments.set(room.id, []); + roomAssignments.set(room.id, {hosts: [], attendees: []}); }); let assignedParticipantUids: UidType[] = []; @@ -154,8 +306,13 @@ export const breakoutRoomReducer = ( const roomIds = state.breakoutGroups.map(room => room.id); state.unassignedParticipants.forEach(participant => { const currentRoomId = roomIds[roomIndex]; - // Assign participant to current room - roomAssignments.get(currentRoomId)!.push(participant.uid); + const roomAssignment = roomAssignments.get(currentRoomId)!; + // Assign participant based on their isHost status (string "true"/"false") + if (participant.user.isHost === 'true') { + roomAssignment.hosts.push(participant.uid); + } else { + roomAssignment.attendees.push(participant.uid); + } // Move it to assigned list assignedParticipantUids.push(participant.uid); // Move to next room for round-robin @@ -164,12 +321,15 @@ export const breakoutRoomReducer = ( } // Update breakoutGroups with new assignments const updatedBreakoutGroups = state.breakoutGroups.map(group => { - const roomParticipants = roomAssignments.get(group.id) || []; + const roomParticipants = roomAssignments.get(group.id) || { + hosts: [], + attendees: [], + }; return { ...group, participants: { - hosts: [], - attendees: roomParticipants || [], + hosts: roomParticipants.hosts, + attendees: roomParticipants.attendees, }, }; }); @@ -200,8 +360,80 @@ export const breakoutRoomReducer = ( }; } - case BreakoutGroupActionTypes.MOVE_PARTICIPANT: { - const {uid, fromGroupId, toGroupId, isHost} = action.payload; + case BreakoutGroupActionTypes.EXIT_GROUP: { + // Same logic as MOVE_PARTICIPANT_TO_MAIN but more explicit + const {user, fromGroupId} = action.payload; + return { + ...state, + breakoutGroups: state.breakoutGroups.map(group => { + if (group.id === fromGroupId) { + return { + ...group, + participants: { + hosts: group.participants.hosts.filter(uid => uid !== user.uid), + attendees: group.participants.attendees.filter( + uid => uid !== user.uid, + ), + }, + }; + } + return group; + }), + }; + } + + case BreakoutGroupActionTypes.CLOSE_GROUP: { + const {groupId} = action.payload; + return { + ...state, + breakoutGroups: state.breakoutGroups.filter( + room => room.id !== groupId, + ), + }; + } + + case BreakoutGroupActionTypes.CLOSE_ALL_GROUPS: { + return { + ...state, + breakoutGroups: [], + }; + } + + case BreakoutGroupActionTypes.RENAME_GROUP: { + const {groupId, newName} = action.payload; + return { + ...state, + breakoutGroups: state.breakoutGroups.map(group => + group.id === groupId ? {...group, name: newName} : group, + ), + }; + } + + case BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN: { + const {user, fromGroupId} = action.payload; + return { + ...state, + breakoutGroups: state.breakoutGroups.map(group => { + // Remove participant from their current breakout group + if (fromGroupId && group.id === fromGroupId) { + return { + ...group, + participants: { + ...group.participants, + hosts: group.participants.hosts.filter(id => id !== user.uid), + attendees: group.participants.attendees.filter( + id => id !== user.uid, + ), + }, + }; + } + return group; + }), + }; + } + + case BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP: { + const {user, fromGroupId, toGroupId} = action.payload; return { ...state, breakoutGroups: state.breakoutGroups.map(group => { @@ -211,26 +443,25 @@ export const breakoutRoomReducer = ( ...group, participants: { ...group.participants, - hosts: isHost - ? group.participants.hosts.filter(id => id !== uid) - : group.participants.hosts, - attendees: !isHost - ? group.participants.attendees.filter(id => id !== uid) - : group.participants.attendees, + hosts: group.participants.hosts.filter(id => id !== user.uid), + attendees: group.participants.attendees.filter( + id => id !== user.uid, + ), }, }; } // Add to target group if (group.id === toGroupId) { + const isHost = user.isHost === 'true'; return { ...group, participants: { ...group.participants, hosts: isHost - ? [...group.participants.hosts, uid] + ? [...group.participants.hosts, user.uid] : group.participants.hosts, attendees: !isHost - ? [...group.participants.attendees, uid] + ? [...group.participants.attendees, user.uid] : group.participants.attendees, }, }; diff --git a/template/src/components/breakout-room/state/types.ts b/template/src/components/breakout-room/state/types.ts index 6049b5ac1..4c1aa7bb0 100644 --- a/template/src/components/breakout-room/state/types.ts +++ b/template/src/components/breakout-room/state/types.ts @@ -33,12 +33,12 @@ export interface BreakoutChannelJoinEventPayload { }; } -export interface BreakoutRoomStateEventPayload { +export interface BreakoutRoomSyncStateEventPayload { data: { data: { switch_room: boolean; session_id: string; - breakout_room: BreakoutGroup; + breakout_room: BreakoutGroup[]; }; act: 'SYNC_STATE'; // e.g., "CHAN_JOIN" }; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx b/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx new file mode 100644 index 000000000..09820f26d --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx @@ -0,0 +1,136 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useState, useRef, useEffect} from 'react'; +import {View, useWindowDimensions} from 'react-native'; +import ActionMenu, {ActionMenuItem} from '../../../atoms/ActionMenu'; +import IconButton from '../../../atoms/IconButton'; +import {calculatePosition} from '../../../utils/common'; +import {useRoomInfo} from 'customization-api'; + +interface RoomActionMenuProps { + onDeleteRoom: () => void; + onRenameRoom: () => void; +} + +const BreakoutRoomActionMenu: React.FC = ({ + onDeleteRoom, + onRenameRoom, +}) => { + const [actionMenuVisible, setActionMenuVisible] = useState(false); + const [isPosCalculated, setIsPosCalculated] = useState(false); + const [modalPosition, setModalPosition] = useState({}); + const {width: globalWidth, height: globalHeight} = useWindowDimensions(); + const moreIconRef = useRef(null); + + const { + data: {isHost}, + } = useRoomInfo(); + + // Build action menu items based on context + const actionMenuItems: ActionMenuItem[] = []; + + // ACTION - Only show for hosts + if (isHost) { + actionMenuItems.push({ + order: 1, + icon: 'pencil-filled', + iconColor: $config.SECONDARY_ACTION_COLOR, + textColor: $config.SECONDARY_ACTION_COLOR, + title: 'Rename Room', + onPress: () => { + onRenameRoom(); + setActionMenuVisible(false); + }, + }); + actionMenuItems.push({ + order: 2, + icon: 'delete', + iconColor: $config.SEMANTIC_ERROR, + textColor: $config.SEMANTIC_ERROR, + title: 'Delete Room', + onPress: () => { + onDeleteRoom(); + setActionMenuVisible(false); + }, + }); + } + + // Calculate position when menu becomes visible + useEffect(() => { + if (actionMenuVisible) { + moreIconRef?.current?.measure( + ( + _fx: number, + _fy: number, + localWidth: number, + localHeight: number, + px: number, + py: number, + ) => { + const data = calculatePosition({ + px, + py, + localWidth, + localHeight, + globalHeight, + globalWidth, + }); + setModalPosition(data); + setIsPosCalculated(true); + }, + ); + } + }, [actionMenuVisible]); + + // Don't render if no actions available + if (actionMenuItems.length === 0) { + return null; + } + + return ( + <> + + + { + setActionMenuVisible(true); + }} + /> + + + ); +}; + +export default BreakoutRoomActionMenu; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomAnnouncementModal.tsx b/template/src/components/breakout-room/ui/BreakoutRoomAnnouncementModal.tsx new file mode 100644 index 000000000..a0f402178 --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomAnnouncementModal.tsx @@ -0,0 +1,136 @@ +import React, {SetStateAction, Dispatch} from 'react'; +import {View, StyleSheet, TextInput} from 'react-native'; +import GenericModal from '../../common/GenericModal'; +import ThemeConfig from '../../../theme'; +import TertiaryButton from '../../../atoms/TertiaryButton'; + +interface BreakoutRoomAnnouncementModalProps { + setModalOpen: Dispatch>; + onAnnouncement: (text: string) => void; +} + +export default function BreakoutRoomAnnouncementModal( + props: BreakoutRoomAnnouncementModalProps, +) { + const {setModalOpen, onAnnouncement} = props; + const [announcement, setAnnouncement] = React.useState(''); + + const disabled = announcement.trim() === ''; + + return ( + setModalOpen(false)} + showCloseIcon={true} + title={'Announcement'} + cancelable={true} + contentContainerStyle={style.contentContainer}> + + + + + + + { + setModalOpen(false); + }} + /> + + + { + onAnnouncement(announcement); + }} + /> + + + + + ); +} + +const style = StyleSheet.create({ + contentContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + flexShrink: 0, + width: '100%', + maxWidth: 500, + height: 314, + }, + fullBody: { + width: '100%', + height: '100%', + flex: 1, + }, + mbody: { + padding: 12, + borderTopColor: $config.CARD_LAYER_3_COLOR, + borderTopWidth: 1, + borderBottomColor: $config.CARD_LAYER_3_COLOR, + borderBottomWidth: 1, + }, + mfooter: { + padding: 12, + gap: 12, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + }, + inputBox: { + fontSize: ThemeConfig.FontSize.normal, + fontWeight: '400', + color: $config.FONT_COLOR, + padding: 20, + lineHeight: 20, + borderRadius: 8, + borderWidth: 1, + borderColor: $config.INPUT_FIELD_BORDER_COLOR, + backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR, + outline: 'none', + }, + actionBtnText: { + color: $config.SECONDARY_ACTION_COLOR, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + }, + cancelBtn: { + borderRadius: 4, + minWidth: 140, + borderColor: $config.SECONDARY_ACTION_COLOR, + backgroundColor: 'transparent', + }, + sendBtn: { + borderRadius: 4, + minWidth: 140, + borderColor: $config.PRIMARY_ACTION_BRAND_COLOR, + backgroundColor: $config.PRIMARY_ACTION_BRAND_COLOR, + }, + disabledSendBtn: { + borderRadius: 4, + minWidth: 140, + backgroundColor: $config.SEMANTIC_NEUTRAL, + }, +}); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index 3090394ae..fcad2ec56 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -10,8 +10,8 @@ ********************************************* */ -import React, {useState} from 'react'; -import {View, Text, StyleSheet, TouchableOpacity} from 'react-native'; +import React, {useState, useRef} from 'react'; +import {View, Text, StyleSheet} from 'react-native'; import IconButton from '../../../atoms/IconButton'; import ThemeConfig from '../../../theme'; import {UidType} from 'agora-rn-uikit'; @@ -20,14 +20,60 @@ import {BreakoutGroup} from '../state/reducer'; import {useContent} from 'customization-api'; import {videoRoomUserFallbackText} from '../../../language/default-labels/videoCallScreenLabels'; import {useString} from '../../../utils/useString'; +import UserActionMenuOptionsOptions from '../../participants/UserActionMenuOptions'; +import BreakoutRoomActionMenu from './BreakoutRoomActionMenu'; +import TertiaryButton from '../../../atoms/TertiaryButton'; +import BreakoutRoomAnnouncementModal from './BreakoutRoomAnnouncementModal'; +import {useModal} from '../../../utils/useModal'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import BreakoutRoomRenameModal from './BreakoutRoomRenameModal'; +import {useRoomInfo} from '../../room-info/useRoomInfo'; + +const BreakoutRoomGroupSettings: React.FC = () => { + const { + data: {isHost}, + } = useRoomInfo(); + + const { + breakoutGroups, + isUserInRoom, + exitRoom, + joinRoom, + closeRoom, + sendAnnouncement, + updateRoomName, + canUserSwitchRoom, + } = useBreakoutRoom(); + + const disableJoinBtn = !isHost && !canUserSwitchRoom; -interface Props { - groups: BreakoutGroup[]; -} -const BreakoutRoomGroupSettings: React.FC = ({groups}) => { // Render room card const {defaultContent} = useContent(); const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)(); + const memberMoreMenuRefs = useRef<{[key: string]: any}>({}); + const { + modalOpen: isAnnoucementModalOpen, + setModalOpen: setAnnouncementModal, + } = useModal(); + const { + modalOpen: isRenameRoomModalOpen, + setModalOpen: setRenameRoomModalOpen, + } = useModal(); + + const [roomToEdit, setRoomToEdit] = useState<{id: string; name: string}>( + null, + ); + + const [actionMenuVisible, setActionMenuVisible] = useState<{ + [key: string]: boolean; + }>({}); + + const showModal = (memberUId: UidType) => { + setActionMenuVisible(prev => ({ + ...prev, + [memberUId]: !prev[memberUId], + })); + }; const getName = (uid: UidType) => { return defaultContent[uid]?.name || remoteUserDefaultLabel; @@ -45,38 +91,80 @@ const BreakoutRoomGroupSettings: React.FC = ({groups}) => { setExpandedRooms(newExpanded); }; - const renderMember = (memberUId: UidType) => ( - - - - - {getName(memberUId)} - - + const renderMember = (memberUId: UidType) => { + // Create or get ref for this specific member + if (!memberMoreMenuRefs.current[memberUId]) { + memberMoreMenuRefs.current[memberUId] = React.createRef(); + } - {}}> - - - - ); + const memberRef = memberMoreMenuRefs.current[memberUId]; + const isMenuVisible = actionMenuVisible[memberUId] || false; + const hasRaisedHand = false; + return ( + + + + + {getName(memberUId)} + + + + + {hasRaisedHand ? '✋' : <>} + {isHost || canUserSwitchRoom ? ( + + + showModal(memberUId)} + /> + + + setActionMenuVisible(prev => ({ + ...prev, + [memberUId]: visible, + })) + } + user={defaultContent[memberUId]} + btnRef={memberRef} + from={'breakout-room'} + /> + + ) : ( + <> + )} + + + ); + }; const renderRoom = (room: BreakoutGroup) => { const isExpanded = expandedRooms.has(room.id); - const memberCount = room.participants.attendees.length || 0; + const memberCount = + room.participants.hosts.length || + 0 + room.participants.attendees.length || + 0; return ( = ({groups}) => { - {/* - {}} - /> - */} + + {isUserInRoom(room) ? ( + { + exitRoom(room.id); + }} + /> + ) : ( + { + joinRoom(room.id); + }} + /> + )} + {/* Only host can perform these actions */} + {isHost ? ( + { + console.log('supriya on delete clicked'); + closeRoom(room.id); + }} + onRenameRoom={() => { + setRoomToEdit({id: room.id, name: room.name}); + setRenameRoomModalOpen(true); + }} + /> + ) : ( + <> + )} + {/* Room Members (Expanded) */} {isExpanded && ( - {room.participants.attendees.length > 0 ? ( - room.participants.attendees.map(member => renderMember(member)) + {room.participants.hosts.length > 0 || + room.participants.attendees.length > 0 ? ( + <> + {room.participants.hosts.map(member => renderMember(member))} + {room.participants.attendees.map(member => + renderMember(member), + )} + ) : ( @@ -124,13 +244,59 @@ const BreakoutRoomGroupSettings: React.FC = ({groups}) => { ); }; + const onRoomNameChange = (newName: string) => { + if (newName && roomToEdit?.id) { + updateRoomName(newName, roomToEdit.id); + setRoomToEdit(null); + setRenameRoomModalOpen(false); + } + }; + + const onAnnouncement = (announcement: string) => { + if (announcement) { + sendAnnouncement(announcement); + setAnnouncementModal(false); + } + }; + return ( - All Rooms - {/* */} + + All Rooms + + {isHost ? ( + + { + setAnnouncementModal(true); + }} + /> + + ) : ( + <> + )} - {groups.map(renderRoom)} + {breakoutGroups.map(renderRoom)} + {isAnnoucementModalOpen && ( + + )} + {isRenameRoomModalOpen && roomToEdit?.id && ( + + )} ); }; @@ -145,10 +311,10 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingHorizontal: 20, + paddingHorizontal: 12, paddingVertical: 16, - // border: '1px solid yellow', }, + headerLeft: {}, headerTitle: { fontWeight: '600', fontSize: ThemeConfig.FontSize.small, @@ -156,6 +322,12 @@ const styles = StyleSheet.create({ color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, fontFamily: ThemeConfig.FontFamily.sansPro, }, + headerRight: { + display: 'flex', + marginLeft: 'auto', + alignItems: 'center', + justifyContent: 'center', + }, headerActions: { flexDirection: 'row', gap: 6, @@ -189,6 +361,27 @@ const styles = StyleSheet.create({ alignItems: 'center', flex: 1, }, + exitRoomBtn: { + backgroundColor: 'transparent', + borderColor: $config.SECONDARY_ACTION_COLOR, + height: 28, + }, + joinRoomBtn: { + backgroundColor: $config.PRIMARY_ACTION_BRAND_COLOR, + borderColor: $config.PRIMARY_ACTION_BRAND_COLOR, + height: 28, + }, + disabledBtn: { + backgroundColor: $config.SEMANTIC_NEUTRAL, + borderColor: $config.SEMANTIC_NEUTRAL, + height: 28, + }, + roomActionBtnText: { + color: $config.SECONDARY_ACTION_COLOR, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + fontWeight: '600', + }, roomHeaderInfo: { display: 'flex', flexDirection: 'column', @@ -209,7 +402,8 @@ const styles = StyleSheet.create({ roomHeaderRight: { flexDirection: 'row', alignItems: 'center', - gap: 8, + marginLeft: 'auto', + gap: 4, }, roomMembers: { paddingHorizontal: 8, @@ -270,9 +464,18 @@ const styles = StyleSheet.create({ }, memberMenu: { padding: 8, + marginLeft: 'auto', + display: 'flex', + alignItems: 'center', + gap: 4, }, - memberMenuText: { - fontSize: 16, + memberMenuMoreIcon: { + width: 24, + height: 24, + alignSelf: 'center', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 20, }, emptyRoom: { alignItems: 'center', diff --git a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx new file mode 100644 index 000000000..1b766f309 --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx @@ -0,0 +1,216 @@ +import React, {useEffect, useState, useCallback} from 'react'; +import {View, Text, StyleSheet} from 'react-native'; +import {useRTMCore} from '../../../rtm/RTMCoreProvider'; +import {nativeChannelTypeMapping} from '../../../../bridge/rtm/web/Types'; +import ThemeConfig from '../../../theme'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import UserAvatar from '../../../atoms/UserAvatar'; + +interface OnlineUser { + userId: string; + name?: string; +} + +const getUserNameFromAttributes = ( + attributes: any, + fallbackUserId: string, +): string => { + try { + const nameAttribute = attributes?.items?.find( + item => item.key === 'name', + )?.value; + if (!nameAttribute) { + return fallbackUserId; + } + + const firstParse = JSON.parse(nameAttribute); + if (firstParse?.payload) { + const secondParse = JSON.parse(firstParse.payload); + return secondParse?.name || fallbackUserId; + } + return firstParse?.name || fallbackUserId; + } catch (e) { + return fallbackUserId; + } +}; + +const BreakoutRoomMainRoomUsers: React.FC = () => { + const {client, onlineUsers} = useRTMCore(); + const {mainChannelId, breakoutGroups} = useBreakoutRoom(); + const [usersWithNames, setUsersWithNames] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + // Get all assigned users from breakout rooms + const getAssignedUsers = useCallback(() => { + const assigned = new Set(); + breakoutGroups.forEach(group => { + group.participants.hosts.forEach(uid => assigned.add(String(uid))); + group.participants.attendees.forEach(uid => assigned.add(String(uid))); + }); + return assigned; + }, [breakoutGroups]); + + // Fetch attributes only when online users change + useEffect(() => { + const fetchUserAttributes = async () => { + if (!client || !onlineUsers || onlineUsers.size === 0) { + setUsersWithNames([]); + return; + } + + setIsLoading(true); + setError(null); + + try { + console.log( + `Fetching attributes for online users: of channel ${mainChannelId}`, + Array.from(onlineUsers), + ); + const users = await Promise.all( + Array.from(onlineUsers).map(async userId => { + try { + const attributes = await client.storage.getUserMetadata({ + userId: userId, + }); + const username = getUserNameFromAttributes(attributes, userId); + return { + userId: userId, + name: username, + }; + } catch (e) { + console.warn(`Failed to get attributes for user ${userId}:`, e); + return { + userId: userId, + name: userId, + }; + } + }), + ); + + setUsersWithNames(users); + } catch (fetchError) { + console.error('Failed to fetch user attributes:', fetchError); + setError('Failed to fetch user information'); + } finally { + setIsLoading(false); + } + }; + + fetchUserAttributes(); + }, [client, onlineUsers, mainChannelId]); + + // Filter out users who are assigned to breakout rooms + const mainRoomOnlyUsers = usersWithNames.filter(user => { + const assignedUsers = getAssignedUsers(); + return !assignedUsers.has(user.userId); + }); + + if (!mainChannelId) { + return ( + + + Main channel not available + + + ); + } + + if (isLoading) { + return ( + + + Loading main room users... + + + ); + } + + if (error) { + return ( + + + Error: {error} + + + ); + } + + return ( + + + Main Room ({mainRoomOnlyUsers.length}) + + {mainRoomOnlyUsers.map(user => ( + + + + ))} + + {mainRoomOnlyUsers.length === 0 && ( + No users online in main room + )} + + + ); +}; + +export default BreakoutRoomMainRoomUsers; + +const style = StyleSheet.create({ + card: { + width: '100%', + padding: 16, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 8, + backgroundColor: $config.CARD_LAYER_2_COLOR, + borderColor: $config.CARD_LAYER_3_COLOR, + gap: 12, + }, + section: { + width: '100%', + display: 'flex', + flexDirection: 'column', + gap: 12, + }, + title: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + fontWeight: '500', + opacity: 0.8, + }, + participantContainer: { + display: 'flex', + flexDirection: 'row', + gap: 5, + }, + emptyText: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium, + fontSize: ThemeConfig.FontSize.tiny, + lineHeight: 16, + fontWeight: '400', + }, + userAvatarContainer: { + backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR, + width: 24, + height: 24, + borderRadius: 12, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + userAvatarText: { + fontSize: ThemeConfig.FontSize.tiny, + lineHeight: 12, + fontWeight: '600', + color: $config.BACKGROUND_COLOR, + }, +}); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx b/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx index b973d5839..662139bf5 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx @@ -15,31 +15,60 @@ import {View, Text, StyleSheet} from 'react-native'; import UserAvatar from '../../../atoms/UserAvatar'; import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; import ThemeConfig from '../../../theme'; +import ImageIcon from '../../../atoms/ImageIcon'; +import Tooltip from '../../../atoms/Tooltip'; interface Props { + isHost?: boolean; participants?: {uid: UidType; user: ContentInterface}[]; } -const BreakoutRoomParticipants: React.FC = ({participants}) => { +const BreakoutRoomParticipants: React.FC = ({ + participants, + isHost = false, +}) => { return ( <> - - Main Room ({participants.length} unassigned) - + 0 ? {} : styles.titleLowOpacity, + ]}> + + + + + Main Room {isHost ? `(${participants.length} unassigned)` : ''} + + {participants.length > 0 ? ( participants.map(item => ( - { + return ( + + ); + }} /> )) ) : ( - No participants available for breakout rooms + {isHost + ? 'No participants available for breakout rooms' + : 'No members'} )} @@ -48,12 +77,20 @@ const BreakoutRoomParticipants: React.FC = ({participants}) => { }; const styles = StyleSheet.create({ + titleLowOpacity: { + opacity: 0.2, + }, + titleContainer: { + display: 'flex', + flexDirection: 'row', + gap: 4, + alignItems: 'center', + }, title: { color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, fontSize: ThemeConfig.FontSize.small, lineHeight: 16, fontWeight: '500', - opacity: 0.2, }, participantContainer: { display: 'flex', diff --git a/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx b/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx new file mode 100644 index 000000000..40b8a9d3f --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import {View, StyleSheet, Text} from 'react-native'; +import ImageIcon from '../../../atoms/ImageIcon'; +import TertiaryButton from '../../../atoms/TertiaryButton'; +import ThemeConfig from '../../../theme'; + +export default function BreakoutRoomRaiseHand() { + return ( + + + + + Please wait, the meeting host will assign you to a room shortly. + + + + {}} + /> + + + ); +} + +const style = StyleSheet.create({ + card: { + width: '100%', + padding: 16, + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 8, + backgroundColor: $config.CARD_LAYER_2_COLOR, + borderColor: $config.CARD_LAYER_3_COLOR, + gap: 16, + }, + cardHeader: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + gap: 8, + }, + cardFooter: { + display: 'flex', + flex: 1, + width: '100%', + }, + infoText: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + fontSize: ThemeConfig.FontSize.tiny, + fontWeight: '400', + lineHeight: 16, + }, + raiseHandBtn: { + width: '100%', + borderRadius: 4, + borderColor: $config.SECONDARY_ACTION_COLOR, + backgroundColor: 'transparent', + }, + raiseHandBtnText: { + textAlign: 'center', + color: $config.SECONDARY_ACTION_COLOR, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + }, +}); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx b/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx new file mode 100644 index 000000000..c5d1046ef --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx @@ -0,0 +1,150 @@ +import React, {SetStateAction, Dispatch} from 'react'; +import {View, StyleSheet, TextInput, Text} from 'react-native'; +import GenericModal from '../../common/GenericModal'; +import ThemeConfig from '../../../theme'; +import TertiaryButton from '../../../atoms/TertiaryButton'; + +interface BreakoutRoomRenameModalProps { + setModalOpen: Dispatch>; + currentRoomName: string; + updateRoomName: (newName: string) => void; +} + +export default function BreakoutRoomRenameModal( + props: BreakoutRoomRenameModalProps, +) { + const {currentRoomName, setModalOpen, updateRoomName} = props; + const [roomName, setRoomName] = React.useState(currentRoomName); + + const disabled = roomName.trim() === ''; + + return ( + setModalOpen(false)} + showCloseIcon={true} + title={'Rename Room'} + cancelable={true} + contentContainerStyle={style.contentContainer}> + + + + Room name + + + + + + { + setModalOpen(false); + }} + /> + + + { + updateRoomName(roomName); + }} + /> + + + + + ); +} + +const style = StyleSheet.create({ + contentContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + flexShrink: 0, + width: '100%', + maxWidth: 500, + height: 235, + }, + fullBody: { + width: '100%', + height: '100%', + flex: 1, + }, + mbody: { + padding: 12, + borderTopColor: $config.CARD_LAYER_3_COLOR, + borderTopWidth: 1, + borderBottomColor: $config.CARD_LAYER_3_COLOR, + borderBottomWidth: 1, + }, + form: { + display: 'flex', + flexDirection: 'column', + gap: 12, + width: '100%', + }, + mfooter: { + padding: 12, + gap: 12, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + }, + label: { + fontSize: ThemeConfig.FontSize.small, + fontWeight: '500', + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + lineHeight: 16, + }, + inputBox: { + fontSize: ThemeConfig.FontSize.normal, + fontWeight: '400', + color: $config.FONT_COLOR, + padding: 20, + lineHeight: 20, + borderRadius: 8, + borderWidth: 1, + borderColor: $config.INPUT_FIELD_BORDER_COLOR, + backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR, + outline: 'none', + }, + actionBtnText: { + color: $config.SECONDARY_ACTION_COLOR, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + }, + cancelBtn: { + borderRadius: 4, + minWidth: 140, + borderColor: $config.SECONDARY_ACTION_COLOR, + backgroundColor: 'transparent', + }, + sendBtn: { + borderRadius: 4, + minWidth: 140, + borderColor: $config.PRIMARY_ACTION_BRAND_COLOR, + backgroundColor: $config.PRIMARY_ACTION_BRAND_COLOR, + }, + disabledSendBtn: { + borderRadius: 4, + minWidth: 140, + backgroundColor: $config.SEMANTIC_NEUTRAL, + }, +}); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx index 2eaa3c430..4d4455274 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx @@ -1,21 +1,43 @@ -import React from 'react'; -import {Text, View, StyleSheet} from 'react-native'; +import React, {useEffect} from 'react'; +import {View, StyleSheet, Text} from 'react-native'; import BreakoutRoomParticipants from './BreakoutRoomParticipants'; import SelectParticipantAssignmentStrategy from './SelectParticipantAssignmentStrategy'; import Divider from '../../common/Dividers'; -import Toggle from '../../../atoms/Toggle'; import ThemeConfig from '../../../theme'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import Toggle from '../../../atoms/Toggle'; +import ParticipantManualAssignmentModal from './ParticipantManualAssignmentModal'; +import {useModal} from '../../../utils/useModal'; +import {RoomAssignmentStrategy} from '../state/reducer'; +import TertiaryButton from '../../../atoms/TertiaryButton'; export default function BreakoutRoomSettings() { const { unsassignedParticipants, assignmentStrategy, setStrategy, - assignParticipants, + handleAssignParticipants, + canUserSwitchRoom, + toggleSwitchRooms, } = useBreakoutRoom(); - const disableAssignment = unsassignedParticipants.length === 0; + const disableAssignmentSelect = unsassignedParticipants.length === 0; + const disableHandleAssignment = + disableAssignmentSelect || + assignmentStrategy === RoomAssignmentStrategy.NO_ASSIGN; + + const { + modalOpen: isManualAssignmentModalOpen, + setModalOpen: setManualAssignmentModalOpen, + } = useModal(); + + useEffect(() => { + if (assignmentStrategy === RoomAssignmentStrategy.MANUAL_ASSIGN) { + setManualAssignmentModalOpen(true); + } else { + setManualAssignmentModalOpen(false); + } + }, [assignmentStrategy, setManualAssignmentModalOpen]); return ( @@ -28,22 +50,41 @@ export default function BreakoutRoomSettings() { + { + handleAssignParticipants(assignmentStrategy); + }} + text={'Assign participants'} /> - {/* + Allow people to switch rooms {}} + isEnabled={canUserSwitchRoom} + toggleSwitch={toggleSwitchRooms} circleColor={$config.FONT_COLOR} /> - */} + + {isManualAssignmentModalOpen && ( + + )} ); } diff --git a/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx b/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx new file mode 100644 index 000000000..1a2adc447 --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx @@ -0,0 +1,42 @@ +import React, {useEffect, useState} from 'react'; +import Loading from '../../../subComponents/Loading'; +import {View, StyleSheet, Text} from 'react-native'; +import ThemeConfig from '../../../theme'; + +const BreakoutRoomTransition = ({onTimeout}: {onTimeout: () => void}) => { + const [dots, setDots] = useState(''); + + useEffect(() => { + const interval = setInterval(() => { + setDots(prev => (prev.length >= 3 ? '' : prev + '.')); + }, 500); + + const timeout = setTimeout(onTimeout, 10000); // 10s timeout + + return () => { + clearInterval(interval); + clearTimeout(timeout); + }; + }, [onTimeout]); + + return ( + + + + ); +}; + +export default BreakoutRoomTransition; +const styles = StyleSheet.create({ + transitionContainer: { + height: '100%', + display: 'flex', + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, +}); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx new file mode 100644 index 000000000..c34ffc905 --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx @@ -0,0 +1,158 @@ +import React, {useEffect, useState} from 'react'; +import {View, StyleSheet, ScrollView} from 'react-native'; +import {useRoomInfo} from '../../room-info/useRoomInfo'; +import {useBreakoutRoom} from './../context/BreakoutRoomContext'; +import BreakoutRoomSettings from './BreakoutRoomSettings'; +import BreakoutRoomGroupSettings from './BreakoutRoomGroupSettings'; +import ThemeConfig from '../../../theme'; +import TertiaryButton from '../../../atoms/TertiaryButton'; +import {BreakoutRoomHeader} from '../../../pages/video-call/SidePanelHeader'; +import BreakoutRoomRaiseHand from './BreakoutRoomRaiseHand'; +import BreakoutRoomMainRoomUsers from './BreakoutRoomMainRoomUsers'; +import Loading from '../../../subComponents/Loading'; + +interface Props { + closeSidePanel: () => void; +} +export default function BreakoutRoomView({closeSidePanel}: Props) { + const [isInitializing, setIsInitializing] = useState(true); + + const { + data: {isHost}, + } = useRoomInfo(); + + const { + checkIfBreakoutRoomSessionExistsAPI, + createBreakoutRoomGroup, + upsertBreakoutRoomAPI, + closeAllRooms, + permissions, + } = useBreakoutRoom(); + + useEffect(() => { + const init = async () => { + try { + setIsInitializing(true); + const activeSession = await checkIfBreakoutRoomSessionExistsAPI(); + if (!activeSession && isHost) { + await upsertBreakoutRoomAPI('START'); + } + } catch (error) { + console.error('Failed to check breakout session:', error); + } finally { + setIsInitializing(false); + } + }; + init(); + }, []); + + return ( + <> + + + {isInitializing ? ( + + + + ) : ( + + {permissions.canRaiseHands ? : <>} + {permissions.canAssignParticipants ? ( + + ) : ( + + )} + + {permissions.canCreateRooms ? ( + createBreakoutRoomGroup()} + /> + ) : ( + <> + )} + + )} + + {!isInitializing && permissions.canCloseRooms ? ( + + + { + try { + closeAllRooms(); + closeSidePanel(); + } catch (error) { + console.error('Supriya Error while closing the room', error); + } + }} + text={'Close All Rooms'} + /> + + + ) : ( + <> + )} + + ); +} + +const style = StyleSheet.create({ + footer: { + width: '100%', + padding: 12, + height: 'auto', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: $config.CARD_LAYER_2_COLOR, + }, + contentCenter: { + height: '100%', + justifyContent: 'center', + }, + contentStart: { + justifyContent: 'flex-start', + }, + pannelOuterBody: { + display: 'flex', + flex: 1, + }, + panelInnerBody: { + display: 'flex', + flex: 1, + padding: 12, + gap: 12, + }, + fullWidth: { + display: 'flex', + flex: 1, + }, + createBtnContainer: { + backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR, + borderColor: $config.INPUT_FIELD_BORDER_COLOR, + borderRadius: 8, + }, + createBtnText: { + color: $config.PRIMARY_ACTION_BRAND_COLOR, + lineHeight: 20, + fontWeight: '500', + fontSize: ThemeConfig.FontSize.normal, + }, +}); diff --git a/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx b/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx new file mode 100644 index 000000000..861541786 --- /dev/null +++ b/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx @@ -0,0 +1,68 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React from 'react'; +import IconButton, {IconButtonProps} from '../../../atoms/IconButton'; +import {useToolbarMenu} from '../../../utils/useMenu'; +import ToolbarMenuItem from '../../../atoms/ToolbarMenuItem'; +import {useToolbarProps} from '../../../atoms/ToolbarItem'; +import {useActionSheet} from '../../../utils/useActionSheet'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; + +export interface ScreenshareButtonProps { + render?: (onPress: () => void) => JSX.Element; +} + +const ExitBreakoutRoomIconButton = (props: ScreenshareButtonProps) => { + const {label = null, onPress: onPressCustom = null} = useToolbarProps(); + const {isOnActionSheet, showLabel} = useActionSheet(); + const {exitRoom} = useBreakoutRoom(); + const {isToolbarMenuItem} = useToolbarMenu(); + + const onPress = () => { + exitRoom(); + }; + + let iconButtonProps: IconButtonProps = { + onPress: onPressCustom || onPress, + iconProps: { + name: 'close-room', + tintColor: $config.SECONDARY_ACTION_COLOR, + }, + btnTextProps: { + textColor: $config.FONT_COLOR, + text: showLabel ? label || 'Exit Room' : '', + }, + }; + + if (isOnActionSheet) { + iconButtonProps.btnTextProps.textStyle = { + color: $config.FONT_COLOR, + marginTop: 8, + fontSize: 12, + fontWeight: '400', + fontFamily: 'Source Sans Pro', + textAlign: 'center', + }; + } + iconButtonProps.isOnActionSheet = isOnActionSheet; + + return props?.render ? ( + props.render(onPress) + ) : isToolbarMenuItem ? ( + + ) : ( + + ); +}; + +export default ExitBreakoutRoomIconButton; diff --git a/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx b/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx new file mode 100644 index 000000000..00af89a64 --- /dev/null +++ b/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx @@ -0,0 +1,440 @@ +import React, {SetStateAction, Dispatch, useState} from 'react'; +import {View, StyleSheet, Text} from 'react-native'; +import GenericModal from '../../common/GenericModal'; +import {TableBody, TableHeader} from '../../common/data-table'; +import Loading from '../../../subComponents/Loading'; +import ThemeConfig from '../../../theme'; +import ImageIcon from '../../../atoms/ImageIcon'; +import Checkbox from '../../../atoms/Checkbox'; +import Dropdown from '../../../atoms/Dropdown'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; +import TertiaryButton from '../../../atoms/TertiaryButton'; +import {ManualParticipantAssignment} from '../state/reducer'; + +function EmptyParticipantsState() { + return ( + + + + + + + No text-tracks found for this meeting + + + + ); +} + +function ParticipantRow({ + participant, + assignment, + rooms, + onAssignmentChange, + onSelectionChange, +}: { + participant: {uid: UidType; user: ContentInterface}; + assignment: ManualParticipantAssignment; + rooms: {label: string; value: string}[]; + onAssignmentChange: (uid: UidType, roomId: string | null) => void; + onSelectionChange: (uid: UidType) => void; +}) { + console.log('supriya-manual individual assignment', assignment); + console.log('supriya-manual participant', participant); + const selectedValue = assignment?.roomId || 'unassigned'; + + return ( + + + onSelectionChange(participant.uid)} + label={participant.user.name} + /> + + + { + onAssignmentChange( + participant.uid, + value === 'unassigned' ? null : value, + ); + }} + selectedValue={selectedValue} + /> + + + ); +} + +interface ParticipantManualAssignmentModalProps { + setModalOpen: Dispatch>; +} + +export default function ParticipantManualAssignmentModal( + props: ParticipantManualAssignmentModalProps, +) { + const {setModalOpen} = props; + const { + getAllRooms, + unsassignedParticipants, + manualAssignments, + setManualAssignments, + applyManualAssignments, + } = useBreakoutRoom(); + + // Local state for assignments + const [localAssignments, setLocalAssignments] = useState< + ManualParticipantAssignment[] + >(() => { + if (manualAssignments.length > 0) { + // Restore previous manual assignments + return manualAssignments; + } + + // Create new manual assignments for unassigned participants + return unsassignedParticipants.map(participant => ({ + uid: participant.uid, + roomId: null, // Start unassigned + isHost: participant.user.isHost || false, + isSelected: false, + })); + }); + console.log('supriya-manual localAssignments', localAssignments); + // Rooms dropdown options + const rooms = [ + {label: 'Unassigned', value: 'unassigned'}, + ...getAllRooms().map(item => ({label: item.name, value: item.id})), + ]; + + // Update room assignment + const updateManualAssignment = (uid: UidType, roomId: string | null) => { + const selectedParticipants = localAssignments.filter(a => a.isSelected); + const clickedParticipant = localAssignments.find(a => a.uid === uid); + + if (selectedParticipants.length > 1 && clickedParticipant?.isSelected) { + // BULK BEHAVIOR: If multiple selected and clicked one is selected, + // assign ALL selected participants to the same room + setLocalAssignments(prev => + prev.map(assignment => + assignment.isSelected + ? { + ...assignment, + roomId: roomId === 'unassigned' ? null : roomId, + isSelected: false, // Deselect after assignment + } + : assignment, + ), + ); + } else { + // INDIVIDUAL BEHAVIOR: Normal single assignment + setLocalAssignments(prev => + prev.map(assignment => + assignment.uid === uid + ? { + ...assignment, + roomId: roomId === 'unassigned' ? null : roomId, + isSelected: false, // Deselect this one too + } + : assignment, + ), + ); + } + }; + const handleRoomDropdownChange = (uid: UidType, roomId: string | null) => { + const clickedParticipant = localAssignments.find(a => a.uid === uid); + if (!clickedParticipant?.isSelected) { + // User clicked dropdown of non-selected participant + // Deselect everyone first, then assign + setLocalAssignments(prev => + prev.map(assignment => ({ + ...assignment, + isSelected: false, // Deselect all + roomId: + assignment.uid === uid + ? roomId === 'unassigned' + ? null + : roomId + : assignment.roomId, + })), + ); + } else { + // Use the bulk/individual logic + updateManualAssignment(uid, roomId); + } + }; + // Toggle selection for specific participant + const toggleParticipantSelection = (uid: UidType) => { + setLocalAssignments(prev => + prev.map(assignment => + assignment.uid === uid + ? {...assignment, isSelected: !assignment.isSelected} + : assignment, + ), + ); + }; + + const allSelected = + localAssignments.length > 0 && localAssignments.every(a => a.isSelected); + + // Select/deselect all + const toggleSelectAll = () => { + const areAllSelected = localAssignments.every(a => a.isSelected); + setLocalAssignments(prev => + prev.map(assignment => ({ + ...assignment, + isSelected: !areAllSelected, + })), + ); + }; + + // More descriptive Select All label + const getSelectAllLabel = () => { + if (selectedCount === 0) { + return 'Select All'; + } else if (allSelected) { + return 'Deselect All'; + } else { + return `Select All (${selectedCount}/${localAssignments.length})`; + } + }; + + const handleCancel = () => { + setModalOpen(false); + }; + + const handleSaveManualAssignments = () => { + setManualAssignments(localAssignments); + setModalOpen(false); + }; + + const selectedCount = localAssignments.filter(a => a.isSelected).length; + + return ( + setModalOpen(false)} + showCloseIcon={true} + title="Assign Participants" + cancelable={false} + contentContainerStyle={style.contentContainer}> + + + 0 ? {} : style.titleLowOpacity, + ]}> + + + + {localAssignments.length} + + ({localAssignments.filter(a => !a.roomId).length} Unassigned) + + + + + toggleSelectAll()} + label={getSelectAllLabel()} + /> + + + {selectedCount > 0 && ( + + {selectedCount} of {localAssignments.length} participants + selected + + )} + + + + + + } + bodyStyle={style.tbodyContainer} + renderRow={participant => { + const assignment = localAssignments.find( + a => a.uid === participant.uid, + ); + return ( + + ); + }} + emptyComponent={} + /> + + + + + { + handleCancel(); + }} + /> + + + { + handleSaveManualAssignments(); + }} + /> + + + + + ); +} + +const style = StyleSheet.create({ + contentContainer: { + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + flexShrink: 0, + width: '100%', + }, + fullBody: { + width: '100%', + height: '100%', + flex: 1, + }, + mbody: { + flex: 1, + padding: 12, + borderTopColor: $config.CARD_LAYER_3_COLOR, + borderTopWidth: 1, + borderBottomColor: $config.CARD_LAYER_3_COLOR, + borderBottomWidth: 1, + }, + mfooter: { + padding: 12, + gap: 12, + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-end', + marginTop: 'auto', + backgroundColor: $config.CARD_LAYER_2_COLOR, + borderBottomLeftRadius: 8, + borderBottomRightRadius: 8, + }, + titleLowOpacity: { + opacity: 0.2, + }, + titleContainer: { + display: 'flex', + flexDirection: 'row', + gap: 4, + alignItems: 'center', + paddingVertical: 16, + }, + title: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + fontWeight: '500', + }, + infotextContainer: { + display: 'flex', + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + infoText: { + fontSize: 14, + fontWeight: '500', + fontFamily: 'Source Sans Pro', + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, + }, + participantTableControls: { + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + flexDirection: 'row', + paddingBottom: 12, + }, + participantTable: { + flex: 1, + backgroundColor: $config.BACKGROUND_COLOR, + }, + tHeadRow: { + borderTopLeftRadius: 2, + borderTopRightRadius: 2, + }, + tbodyContainer: { + backgroundColor: $config.BACKGROUND_COLOR, + borderRadius: 2, + }, + tbrow: { + display: 'flex', + alignSelf: 'stretch', + minHeight: 48, + flexDirection: 'row', + paddingVertical: 8, + }, + td: { + flex: 1, + alignSelf: 'center', + justifyContent: 'center', + gap: 10, + }, + actionBtnText: { + color: $config.SECONDARY_ACTION_COLOR, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + }, + cancelBtn: { + borderRadius: 4, + minWidth: 140, + borderColor: $config.SECONDARY_ACTION_COLOR, + backgroundColor: 'transparent', + }, + saveBtn: { + borderRadius: 4, + minWidth: 140, + borderColor: $config.PRIMARY_ACTION_BRAND_COLOR, + backgroundColor: $config.PRIMARY_ACTION_BRAND_COLOR, + }, +}); diff --git a/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx b/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx index 23878afb3..7e31b7aef 100644 --- a/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx +++ b/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx @@ -2,11 +2,9 @@ import React from 'react'; import {Text, StyleSheet} from 'react-native'; import {Dropdown} from 'customization-api'; import ThemeConfig from '../../../theme'; -import TertiaryButton from '../../../atoms/TertiaryButton'; import {RoomAssignmentStrategy} from '../state/reducer'; interface Props { - assignParticipants: () => void; selectedStrategy: RoomAssignmentStrategy; onStrategyChange: (strategy: RoomAssignmentStrategy) => void; disabled: boolean; @@ -30,7 +28,6 @@ const SelectParticipantAssignmentStrategy: React.FC = ({ selectedStrategy, onStrategyChange, disabled = false, - assignParticipants, }) => { return ( <> @@ -44,17 +41,6 @@ const SelectParticipantAssignmentStrategy: React.FC = ({ onStrategyChange(value as RoomAssignmentStrategy); }} /> - { - assignParticipants(); - }} - text={'Assign participants'} - /> ); }; diff --git a/template/src/components/controls/toolbar-items/ExitBreakoutRoomToolbarItem.tsx b/template/src/components/controls/toolbar-items/ExitBreakoutRoomToolbarItem.tsx new file mode 100644 index 000000000..1de5be208 --- /dev/null +++ b/template/src/components/controls/toolbar-items/ExitBreakoutRoomToolbarItem.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import ToolbarItem, {ToolbarItemProps} from '../../../atoms/ToolbarItem'; +import ExitBreakoutRoomIconButton from '../../breakout-room/ui/ExitBreakoutRoomIconButton'; + +export interface Props extends ToolbarItemProps {} + +export const ExitBreakoutRoomToolbarItem = (props: Props) => { + return ( + + + + ); +}; diff --git a/template/src/components/participants/UserActionMenuOptions.tsx b/template/src/components/participants/UserActionMenuOptions.tsx index dcdb26390..22ce6f262 100644 --- a/template/src/components/participants/UserActionMenuOptions.tsx +++ b/template/src/components/participants/UserActionMenuOptions.tsx @@ -77,7 +77,10 @@ import { DEFAULT_ACTION_KEYS, UserActionMenuItemsConfig, } from '../../atoms/UserActionMenuPreset'; -import {useBreakoutRoom} from '../breakout-room/context/BreakoutRoomContext'; +import { + MemberDropdownOption, + useBreakoutRoom, +} from '../breakout-room/context/BreakoutRoomContext'; interface UserActionMenuOptionsOptionsProps { user: ContentInterface; @@ -152,7 +155,9 @@ export default function UserActionMenuOptionsOptions( const moreBtnSpotlightLabel = useString(moreBtnSpotlight); const {chatConnectionStatus} = useChatUIControls(); const chatErrNotConnectedText = useString(chatErrorNotConnected)(); - const {breakoutGroups, addUserIntoGroup} = useBreakoutRoom(); + const {getRoomMemberDropdownOptions, presenters: breakoutRoomPresenters} = + useBreakoutRoom(); + useEffect(() => { customEvents.on('DisableChat', data => { // for other users @@ -171,25 +176,23 @@ export default function UserActionMenuOptionsOptions( useEffect(() => { const items: ActionMenuItem[] = []; - if (from === 'breakout-room' && $config.ENABLE_BREAKOUT_ROOM) { - if (breakoutGroups && breakoutGroups?.length) { - breakoutGroups.map(({name, id}, index) => { - items.push({ - order: index + 1, - icon: 'add', - onHoverIcon: 'add', - iconColor: $config.SECONDARY_ACTION_COLOR, - textColor: $config.SECONDARY_ACTION_COLOR, - title: `Move to ${name}`, - onPress: () => { - setActionMenuVisible(false); - addUserIntoGroup(user.uid, id, false); - }, - }); - }); - setActionMenuitems(items); - } + const memberOptions = getRoomMemberDropdownOptions(user.uid); + // Transform to your UI format + const breakoutRoomMenuDropdowItems = memberOptions.map( + (option: MemberDropdownOption, index) => ({ + order: index + 1, + icon: option.icon, + iconColor: $config.SECONDARY_ACTION_COLOR, + textColor: $config.SECONDARY_ACTION_COLOR, + title: option.title, + onPress: () => { + setActionMenuVisible(false); + option?.onOptionPress(); + }, + }), + ); + setActionMenuitems(breakoutRoomMenuDropdowItems); return; } @@ -757,7 +760,7 @@ export default function UserActionMenuOptionsOptions( currentLayout, spotlightUid, from, - breakoutGroups, + breakoutRoomPresenters, ]); const {width: globalWidth, height: globalHeight} = useWindowDimensions(); diff --git a/template/src/logger/AppBuilderLogger.tsx b/template/src/logger/AppBuilderLogger.tsx index 1f47bcaa0..5293adb87 100644 --- a/template/src/logger/AppBuilderLogger.tsx +++ b/template/src/logger/AppBuilderLogger.tsx @@ -53,7 +53,13 @@ export enum LogSource { } type LogType = { - [LogSource.AgoraSDK]: 'Log' | 'API' | 'Event' | 'Service' | 'AI_AGENT'; + [LogSource.AgoraSDK]: + | 'Log' + | 'API' + | 'Event' + | 'Service' + | 'AI_AGENT' + | 'RTMConfigure'; [LogSource.Internals]: | 'AUTH' | 'CREATE_MEETING' diff --git a/template/src/pages/VideoCall.tsx b/template/src/pages/VideoCall.tsx index 81499e34d..7377a9761 100644 --- a/template/src/pages/VideoCall.tsx +++ b/template/src/pages/VideoCall.tsx @@ -9,7 +9,6 @@ information visit https://appbuilder.agora.io. ********************************************* */ -// @ts-nocheck import React, {useState, useContext, useEffect, useRef, useMemo} from 'react'; import {View, StyleSheet, Text} from 'react-native'; import { @@ -20,6 +19,7 @@ import { LocalUserContext, UidType, CallbacksInterface, + RtcPropsInterface, } from '../../agora-rn-uikit'; import styles from '../components/styles'; import {useParams, useHistory} from '../components/Router'; @@ -85,68 +85,24 @@ import {VideoRoomOrchestratorState} from './VideoCallRoomOrchestrator'; import VideoCallStateSetup from './video-call/VideoCallStateSetup'; import {BreakoutRoomProvider} from '../components/breakout-room/context/BreakoutRoomContext'; -export enum RnEncryptionEnum { - /** - * @deprecated - * 0: This mode is deprecated. - */ - None = 0, - /** - * 1: (Default) 128-bit AES encryption, XTS mode. - */ - AES128XTS = 1, - /** - * 2: 128-bit AES encryption, ECB mode. - */ - AES128ECB = 2, - /** - * 3: 256-bit AES encryption, XTS mode. - */ - AES256XTS = 3, - /** - * 4: 128-bit SM4 encryption, ECB mode. - * - * @since v3.1.2. - */ - SM4128ECB = 4, - /** - * 6: 256-bit AES encryption, GCM mode. - * - * @since v3.1.2. - */ - AES256GCM = 6, - - /** - * 7: 128-bit GCM encryption, GCM mode. - * - * @since v3.4.5 - */ - AES128GCM2 = 7, - /** - * 8: 256-bit GCM encryption, GCM mode. - * @since v3.1.2. - * Compared to AES256GCM encryption mode, AES256GCM2 encryption mode is more secure and requires you to set the salt (encryptionKdfSalt). - */ - AES256GCM2 = 8, -} - -export interface VideoCallProps { - setMainRtcEngine?: (engine: IRtcEngine) => void; - setMainChannelDetails?: ( - details: VideoRoomOrchestratorState['channelDetails'], - ) => void; - storedEngine?: IRtcEngine | null; - storedChannelDetails?: ChannelDetails; +interface VideoCallProps { + callActive: boolean; + setCallActive: React.Dispatch>; + rtcProps: RtcPropsInterface; + setRtcProps: React.Dispatch>>; + callbacks: CallbacksInterface; + styleProps: any; } const VideoCall = (videoCallProps: VideoCallProps) => { - const hasBrandLogo = useHasBrandLogo(); - const joiningLoaderLabel = useString(videoRoomStartingCallText)(); - const bannedUserText = useString(userBannedText)(); - - const {setGlobalErrorMessage} = useContext(ErrorContext); - const {awake, release} = useWakeLock(); - const {isRecordingBot} = useIsRecordingBot(); + const { + callActive, + setCallActive, + rtcProps, + setRtcProps, + callbacks, + styleProps, + } = videoCallProps; /** * Should we set the callscreen to active ?? * a) If Recording bot( i.e prop: recordingBot) is TRUE then it means, @@ -156,35 +112,10 @@ const VideoCall = (videoCallProps: VideoCallProps) => { * b) If Recording bot( i.e prop: recordingBot) is FALSE then we should set * the callActive depending upon the value of magic variable - $config.PRECALL */ - const shouldCallBeSetToActive = isRecordingBot - ? true - : $config.PRECALL - ? false - : true; - const [callActive, setCallActive] = useState(shouldCallBeSetToActive); const [isRecordingActive, setRecordingActive] = useState(false); - const [queryComplete, setQueryComplete] = useState(false); - const [waitingRoomAttendeeJoined, setWaitingRoomAttendeeJoined] = - useState(false); const [sttAutoStarted, setSttAutoStarted] = useState(false); const [recordingAutoStarted, setRecordingAutoStarted] = useState(false); - const {phrase} = useParams<{phrase: string}>(); - - const {store} = useContext(StorageContext); - const { - join: SdkJoinState, - microphoneDevice: sdkMicrophoneDevice, - cameraDevice: sdkCameraDevice, - clearState, - } = useContext(SdkApiContext); - - // commented for v1 release - const afterEndCall = useCustomization( - data => - data?.lifecycle?.useAfterEndCall && data?.lifecycle?.useAfterEndCall(), - ); - const {PrefereceWrapper} = useCustomization(data => { let components: { PrefereceWrapper: React.ComponentType; @@ -202,457 +133,150 @@ const VideoCall = (videoCallProps: VideoCallProps) => { return components; }); - const [rtcProps, setRtcProps] = React.useState({ - appId: $config.APP_ID, - channel: null, - uid: null, - token: null, - rtm: null, - screenShareUid: null, - screenShareToken: null, - profile: $config.PROFILE, - screenShareProfile: $config.SCREEN_SHARE_PROFILE, - dual: true, - encryption: $config.ENCRYPTION_ENABLED - ? {key: null, mode: RnEncryptionEnum.AES128GCM2, screenKey: null} - : false, - role: ClientRoleType.ClientRoleBroadcaster, - geoFencing: $config.GEO_FENCING, - audioRoom: $config.AUDIO_ROOM, - activeSpeaker: $config.ACTIVE_SPEAKER, - preferredCameraId: - sdkCameraDevice.deviceId || store?.activeDeviceId?.videoinput || null, - preferredMicrophoneId: - sdkMicrophoneDevice.deviceId || store?.activeDeviceId?.audioinput || null, - recordingBot: isRecordingBot ? true : false, - }); - - const history = useHistory(); - const currentMeetingPhrase = useRef(history.location.pathname); - - const useJoin = useJoinRoom(); - const {setRoomInfo} = useSetRoomInfo(); - const {isJoinDataFetched, data, isInWaitingRoom, waitingRoomStatus} = - useRoomInfo(); - - useEffect(() => { - if (!isJoinDataFetched) { - return; - } - logger.log(LogSource.Internals, 'SET_MEETING_DETAILS', 'Room details', { - user_id: data?.uid || '', - meeting_title: data?.meetingTitle || '', - channel_id: data?.channel, - host_id: data?.roomId?.host || '', - attendee_id: data?.roomId?.attendee || '', - }); - }, [isJoinDataFetched, data, phrase]); - - React.useEffect(() => { - return () => { - logger.debug( - LogSource.Internals, - 'VIDEO_CALL_ROOM', - 'Videocall unmounted', - ); - setRoomInfo(prevState => { - return { - ...RoomInfoDefaultValue, - loginToken: prevState?.loginToken, - }; - }); - if (awake) { - release(); - } - }; - }, []); - - useEffect(() => { - if (!SdkJoinState.phrase) { - useJoin(phrase, RoomInfoDefaultValue.roomPreference) - .then(() => { - logger.log( - LogSource.Internals, - 'JOIN_MEETING', - 'Join channel success', - ); - }) - .catch(error => { - const errorCode = error?.code; - if (AuthErrorCodes.indexOf(errorCode) !== -1 && isSDK()) { - SDKEvents.emit('unauthorized', error); - } - logger.error( - LogSource.Internals, - 'JOIN_MEETING', - 'Join channel error', - JSON.stringify(error || {}), - ); - setGlobalErrorMessage(error); - history.push('/'); - }); - } - }, []); - - useEffect(() => { - if (!isSDK() || !SdkJoinState.initialized) { - return; - } - const { - phrase: sdkMeetingPhrase, - meetingDetails: sdkMeetingDetails, - skipPrecall, - promise, - preference, - } = SdkJoinState; - - const sdkMeetingPath = `/${sdkMeetingPhrase}`; - - setCallActive(skipPrecall); - - if (sdkMeetingDetails) { - setQueryComplete(false); - setRoomInfo(roomInfo => { - return { - ...roomInfo, - isJoinDataFetched: true, - data: { - ...roomInfo.data, - ...sdkMeetingDetails, - }, - roomPreference: preference, - }; - }); - } else if (sdkMeetingPhrase) { - setQueryComplete(false); - currentMeetingPhrase.current = sdkMeetingPath; - useJoin(sdkMeetingPhrase, preference) - .then(() => { - logger.log( - LogSource.Internals, - 'JOIN_MEETING', - 'Join channel success', - ); - }) - .catch(error => { - const errorCode = error?.code; - if (AuthErrorCodes.indexOf(errorCode) !== -1 && isSDK()) { - SDKEvents.emit('unauthorized', error); - } - logger.error( - LogSource.Internals, - 'JOIN_MEETING', - 'Join channel error', - JSON.stringify(error || {}), - ); - setGlobalErrorMessage(error); - history.push('/'); - currentMeetingPhrase.current = ''; - promise.rej(error); - }); - } - }, [SdkJoinState]); - - React.useEffect(() => { - if ( - //isJoinDataFetched === true && (!queryComplete || !isInWaitingRoom) - //non waiting room - host/attendee - (!$config.ENABLE_WAITING_ROOM && - isJoinDataFetched === true && - !queryComplete) || - //waiting room - host - ($config.ENABLE_WAITING_ROOM && - isJoinDataFetched === true && - data.isHost && - !queryComplete) || - //waiting room - attendee - ($config.ENABLE_WAITING_ROOM && - isJoinDataFetched === true && - !data.isHost && - (!queryComplete || !isInWaitingRoom) && - !waitingRoomAttendeeJoined) - ) { - setRtcProps(prevRtcProps => ({ - ...prevRtcProps, - channel: data.channel, - uid: data.uid, - token: data.token, - rtm: data.rtmToken, - encryption: $config.ENCRYPTION_ENABLED - ? { - key: data.encryptionSecret, - mode: data.encryptionMode, - screenKey: data.encryptionSecret, - salt: data.encryptionSecretSalt, - } - : false, - screenShareUid: data.screenShareUid, - screenShareToken: data.screenShareToken, - role: data.isHost - ? ClientRoleType.ClientRoleBroadcaster - : ClientRoleType.ClientRoleAudience, - preventJoin: - !$config.ENABLE_WAITING_ROOM || - ($config.ENABLE_WAITING_ROOM && data.isHost) || - ($config.ENABLE_WAITING_ROOM && - !data.isHost && - waitingRoomStatus === WaitingRoomStatus.APPROVED) - ? false - : true, - })); - - if ( - $config.ENABLE_WAITING_ROOM && - !data.isHost && - waitingRoomStatus === WaitingRoomStatus.APPROVED - ) { - setWaitingRoomAttendeeJoined(true); - } - // 1. Store the display name from API - // if (data.username) { - // setUsername(data.username); - // } - setQueryComplete(true); - } - }, [isJoinDataFetched, data, queryComplete]); - - const callbacks: CallbacksInterface = { - // RtcLeft: () => {}, - // RtcJoined: () => { - // if (SdkJoinState.phrase && SdkJoinState.skipPrecall) { - // SdkJoinState.promise?.res(); - // } - // }, - EndCall: () => { - clearState('join'); - setTimeout(() => { - // TODO: These callbacks are being called twice - SDKEvents.emit('leave'); - if (afterEndCall) { - afterEndCall(data.isHost, history as unknown as History); - } else { - history.push('/'); - } - }, 0); - }, - UserJoined: (uid: UidType) => { - console.log('UIKIT Callback: UserJoined', uid); - SDKEvents.emit('rtc-user-joined', uid); - }, - UserOffline: (uid: UidType) => { - console.log('UIKIT Callback: UserOffline', uid); - SDKEvents.emit('rtc-user-left', uid); - }, - RemoteAudioStateChanged: (uid: UidType, status: 0 | 2) => { - console.log('UIKIT Callback: RemoteAudioStateChanged', uid, status); - if (status === 0) { - SDKEvents.emit('rtc-user-unpublished', uid, 'audio'); - } else { - SDKEvents.emit('rtc-user-published', uid, 'audio'); - } - }, - RemoteVideoStateChanged: (uid: UidType, status: 0 | 2) => { - console.log('UIKIT Callback: RemoteVideoStateChanged', uid, status); - if (status === 0) { - SDKEvents.emit('rtc-user-unpublished', uid, 'video'); - } else { - SDKEvents.emit('rtc-user-published', uid, 'video'); - } - }, - UserBanned(isBanned) { - console.log('UIKIT Callback: UserBanned', isBanned); - Toast.show({ - leadingIconName: 'alert', - type: 'error', - text1: bannedUserText, - visibilityTime: 3000, - }); - }, - }; - return ( - <> - {queryComplete ? ( - queryComplete || !callActive ? ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - {!isMobileUA() && ( - - )} - - - - {/* + + + + + + + + + + + + + + + + + + + + + + + {!isMobileUA() && ( + + )} + + + + {/* */} - - {callActive ? ( - - - - - - - - - - ) : $config.PRECALL ? ( - - - - ) : ( - <> - )} - - {/* */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - - {hasBrandLogo() && } - {joiningLoaderLabel} - - ) - ) : ( - <> - )} - + + {callActive ? ( + + + + + + + + + + ) : $config.PRECALL ? ( + + + + ) : ( + <> + )} + + {/* */} + + + + + + + + + + + + + + + + + + + + + + + + + + ); }; -const styleProps = { - maxViewStyles: styles.temp, - minViewStyles: styles.temp, - localBtnContainer: styles.bottomBar, - localBtnStyles: { - muteLocalAudio: styles.localButton, - muteLocalVideo: styles.localButton, - switchCamera: styles.localButton, - endCall: styles.endCall, - fullScreen: styles.localButton, - recording: styles.localButton, - screenshare: styles.localButton, - }, - theme: $config.PRIMARY_ACTION_BRAND_COLOR, - remoteBtnStyles: { - muteRemoteAudio: styles.remoteButton, - muteRemoteVideo: styles.remoteButton, - remoteSwap: styles.remoteButton, - minCloseBtnStyles: styles.minCloseBtn, - liveStreamHostControlBtns: styles.liveStreamHostControlBtns, - }, - BtnStyles: styles.remoteButton, -}; //change these to inline styles or sth -const style = StyleSheet.create({ - full: { - flex: 1, - flexDirection: 'column', - overflow: 'hidden', - }, - videoView: videoView, - loader: { - flex: 1, - alignSelf: 'center', - justifyContent: 'center', - }, - loaderLogo: { - alignSelf: 'center', - justifyContent: 'center', - marginBottom: 30, - }, - loaderText: {fontWeight: '500', color: $config.FONT_COLOR}, -}); +// const style = StyleSheet.create({ +// full: { +// flex: 1, +// flexDirection: 'column', +// overflow: 'hidden', +// }, +// videoView: videoView, +// loader: { +// flex: 1, +// alignSelf: 'center', +// justifyContent: 'center', +// }, +// loaderLogo: { +// alignSelf: 'center', +// justifyContent: 'center', +// marginBottom: 30, +// }, +// loaderText: {fontWeight: '500', color: $config.FONT_COLOR}, +// }); export default VideoCall; diff --git a/template/src/pages/VideoCallRoomOrchestrator.tsx b/template/src/pages/VideoCallRoomOrchestrator.tsx index ab3cb755c..ef224046b 100644 --- a/template/src/pages/VideoCallRoomOrchestrator.tsx +++ b/template/src/pages/VideoCallRoomOrchestrator.tsx @@ -9,26 +9,22 @@ information visit https://appbuilder.agora.io. ********************************************* */ - -import React, {useState, useEffect, useContext, useCallback} from 'react'; -import {IRtcEngine} from 'react-native-agora'; +import React, {useState, useEffect, useCallback} from 'react'; import VideoCall from '../pages/VideoCall'; import BreakoutRoomVideoCall from './BreakoutRoomVideoCall'; +import {RTMCoreProvider} from '../rtm/RTMCoreProvider'; import {useParams, useHistory, useLocation} from '../components/Router'; import events from '../rtm-events-api'; import {EventNames} from '../rtm-events'; import {BreakoutChannelJoinEventPayload} from '../components/breakout-room/state/types'; export interface VideoRoomOrchestratorState { - rtcEngine: IRtcEngine | null; - channelDetails: { - channel: string; - uid: number | string; - token: string; - screenShareUid: number | string; - screenShareToken: string; - rtmToken: string; - }; + channel: string; + uid: number | string; + token: string; + screenShareUid: number | string; + screenShareToken: string; + rtmToken: string; } const VideoCallRoomOrchestrator: React.FC = () => { @@ -36,166 +32,119 @@ const VideoCallRoomOrchestrator: React.FC = () => { const history = useHistory(); const location = useLocation(); - // Parse query parameters from location.search + // Parse query parameters const searchParams = new URLSearchParams(location.search); - const callActive = searchParams.get('call') === 'true'; const isBreakoutRoomActive = searchParams.get('breakout') === 'true'; + const breakoutChannelName = searchParams.get('channelName'); // Main room state const [mainRoomState, setMainRoomState] = useState({ - rtcEngine: null, - channelDetails: { - channel: null, - uid: null, - token: null, - screenShareUid: null, - screenShareToken: null, - rtmToken: null, - }, + channel: phrase, // Use phrase as main channel + uid: null, + token: null, + screenShareUid: null, + screenShareToken: null, + rtmToken: null, }); // Breakout room state const [breakoutRoomState, setBreakoutRoomState] = useState({ - rtcEngine: null, - channelDetails: { - channel: null, - uid: null, - token: null, - screenShareUid: null, - screenShareToken: null, - rtmToken: null, - }, + channel: breakoutChannelName || null, + uid: null, + token: null, + screenShareUid: null, + screenShareToken: null, + rtmToken: null, }); + // Listen for breakout room join events useEffect(() => { - const handleBreakoutJoin = evtData => { - const {payload, sender, ts, source} = evtData; + const handleBreakoutJoin = (evtData: any) => { + const {payload} = evtData; const data: BreakoutChannelJoinEventPayload = JSON.parse(payload); - console.log( - 'supriya [VideoCallRoomOrchestrator] onBreakoutRoomJoinDetailsReceived data: ', - data, - ); - const {channel_name, mainUser, screenShare, chat} = data.data.data; + console.log('[VideoCallRoomOrchestrator] Breakout join data:', data); + + const {channel_name, mainUser, screenShare} = data.data.data; try { - setBreakoutRoomState(prev => ({ - ...prev, - channelDetails: { - channel: channel_name, - token: mainUser.rtc, - uid: mainUser?.uid || 0, - screenShareToken: screenShare.rtc, - screenShareUid: screenShare.uid, - rtmToken: mainUser.rtm, - }, - })); - // Navigate to breakout roo - history.push(`/${phrase}?call=true&breakout=true`); + // Update breakout room state + setBreakoutRoomState({ + channel: channel_name, + token: mainUser.rtc, + uid: mainUser?.uid, + screenShareToken: screenShare.rtc, + screenShareUid: screenShare.uid, + rtmToken: mainUser.rtm, + }); + + // Navigate to breakout room with proper URL + history.push(`/${phrase}?breakout=true&channelName=${channel_name}`); } catch (error) { - console.log( - ' handleBreakoutJoin [VideoCallRoomOrchestrator] error: ', + console.error( + '[VideoCallRoomOrchestrator] Breakout join error:', error, ); } }; + events.on(EventNames.BREAKOUT_ROOM_JOIN_DETAILS, handleBreakoutJoin); + return () => { events.off(EventNames.BREAKOUT_ROOM_JOIN_DETAILS, handleBreakoutJoin); }; }, [history, phrase]); - // // RTM listeners for breakout events - // useEffect(() => { - // const handleBreakoutLeave = () => { - // console.log( - // `[VideoCallRoomOrchestrator] Leaving breakout room, returning to main`, - // ); - - // // Return to main room - // history.push(`/${phrase}?call=true`); - // }; - - // // TODO: Implement RTM event listeners - // // RTMManager.on('BREAKOUT_JOIN', handleBreakoutJoin); - // // RTMManager.on('BREAKOUT_LEAVE', handleBreakoutLeave); - - // // For now, we'll expose these functions globally for testing - // if (typeof window !== 'undefined') { - // (window as any).joinBreakoutRoom = handleBreakoutJoin; - // (window as any).leaveBreakoutRoom = handleBreakoutLeave; - // } - - // return () => { - // // RTMManager.off('BREAKOUT_JOIN', handleBreakoutJoin); - // // RTMManager.off('BREAKOUT_LEAVE', handleBreakoutLeave); - - // if (typeof window !== 'undefined') { - // delete (window as any).joinBreakoutRoom; - // delete (window as any).leaveBreakoutRoom; - // } - // }; - // }, [phrase, history, roomInfo]); - - // Helper functions to update RTC engines - const setMainRtcEngine = useCallback((engine: IRtcEngine) => { - console.log('supriya [VideoCallRoomOrchestrator] Setting main RTC engine'); - setMainRoomState(prev => ({ - ...prev, - rtcEngine: engine, - })); - }, []); + // Handle leaving breakout room + const handleLeaveBreakout = useCallback(() => { + console.log('[VideoCallRoomOrchestrator] Leaving breakout room'); + + // Clear breakout state + setBreakoutRoomState({ + channel: null, + uid: null, + token: null, + screenShareUid: null, + screenShareToken: null, + rtmToken: null, + }); + + // Return to main room + history.push(`/${phrase}`); + }, [history, phrase]); + // Update main room details const setMainChannelDetails = useCallback( - (channelDetails: VideoRoomOrchestratorState['channelDetails']) => { - console.log( - 'supriya [VideoCallRoomOrchestrator] Setting main RTC engine', - ); + (channelInfo: VideoRoomOrchestratorState) => { + console.log('[VideoCallRoomOrchestrator] Setting main channel details'); setMainRoomState(prev => ({ ...prev, - channelDetails: {...channelDetails}, + ...channelInfo, })); }, [], ); - const setBreakoutRtcEngine = useCallback((engine: IRtcEngine) => { - console.log('[VideoCallRoomOrchestrator] Setting breakout RTC engine'); - setBreakoutRoomState(prev => ({ - ...prev, - rtcEngine: engine, - })); - }, []); - - // // Handle return to main room - // const handleReturnToMain = () => { - // console.log('[VideoCallRoomOrchestrator] Returning to main room'); - // history.push(`/${phrase}?call=true`); - // }; - console.log('[VideoCallRoomOrchestrator] Rendering:', { - isBreakoutRoom: isBreakoutRoomActive, - callActive, + isBreakoutRoomActive, phrase, - mainChannel: {...mainRoomState}, - breakoutChannel: {...breakoutRoomState}, + mainChannel: mainRoomState, + breakoutChannel: breakoutRoomState, }); return ( - <> - {isBreakoutRoomActive && breakoutRoomState?.channelDetails?.channel ? ( + + {isBreakoutRoomActive && breakoutRoomState?.channel ? ( ) : ( - + )} - + ); }; diff --git a/template/src/pages/video-call/BreakoutVideoCallContent.tsx b/template/src/pages/video-call/BreakoutVideoCallContent.tsx new file mode 100644 index 000000000..090e3a9c9 --- /dev/null +++ b/template/src/pages/video-call/BreakoutVideoCallContent.tsx @@ -0,0 +1,241 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ +import React, {useState, useEffect, SetStateAction, useMemo} from 'react'; +import { + RtcConfigure, + PropsProvider, + ChannelProfileType, + LocalUserContext, + RtcPropsInterface, +} from '../../../agora-rn-uikit'; +import RtmConfigure from '../../components/RTMConfigure'; +import DeviceConfigure from '../../components/DeviceConfigure'; +import {isMobileUA} from '../../utils/common'; +import {LiveStreamContextProvider} from '../../components/livestream'; +import ScreenshareConfigure from '../../subComponents/screenshare/ScreenshareConfigure'; +import {LayoutProvider} from '../../utils/useLayout'; +import {RecordingProvider} from '../../subComponents/recording/useRecording'; +import {SidePanelProvider} from '../../utils/useSidePanel'; +import {NetworkQualityProvider} from '../../components/NetworkQualityContext'; +import {ChatNotificationProvider} from '../../components/chat-notification/useChatNotification'; +import {ChatUIControlsProvider} from '../../components/chat-ui/useChatUIControls'; +import {ScreenShareProvider} from '../../components/contexts/ScreenShareContext'; +import {LiveStreamDataProvider} from '../../components/contexts/LiveStreamDataContext'; +import {VideoMeetingDataProvider} from '../../components/contexts/VideoMeetingDataContext'; +import {UserPreferenceProvider} from '../../components/useUserPreference'; +import EventsConfigure from '../../components/EventsConfigure'; +import PermissionHelper from '../../components/precall/PermissionHelper'; +import {FocusProvider} from '../../utils/useFocus'; +import {VideoCallProvider} from '../../components/useVideoCall'; +import {CaptionProvider} from '../../subComponents/caption/useCaption'; +import SdkMuteToggleListener from '../../components/SdkMuteToggleListener'; +import {NoiseSupressionProvider} from '../../app-state/useNoiseSupression'; +import {VideoQualityContextProvider} from '../../app-state/useVideoQuality'; +import {VBProvider} from '../../components/virtual-background/useVB'; +import {DisableChatProvider} from '../../components/disable-chat/useDisableChat'; +import {WaitingRoomProvider} from '../../components/contexts/WaitingRoomContext'; +import {ChatMessagesProvider} from '../../components/chat-messages/useChatMessages'; +import VideoCallScreenWrapper from './../video-call/VideoCallScreenWrapper'; +import {BeautyEffectProvider} from '../../components/beauty-effect/useBeautyEffects'; +import {UserActionMenuProvider} from '../../components/useUserActionMenu'; +import {BreakoutRoomProvider} from '../../components/breakout-room/context/BreakoutRoomContext'; +import { + BreakoutChannelDetails, + VideoCallContentProps, +} from './VideoCallContent'; +import BreakoutRoomEventsConfigure from '../../components/breakout-room/events/BreakoutRoomEventsConfigure'; +import {useRTMCore} from '../../rtm/RTMCoreProvider'; +import RTMEngine from '../../rtm/RTMEngine'; + +interface BreakoutVideoCallContentProps extends VideoCallContentProps { + rtcProps: RtcPropsInterface; + breakoutChannelDetails: BreakoutChannelDetails; + onLeave: () => void; +} + +const BreakoutVideoCallContent: React.FC = ({ + rtcProps, + breakoutChannelDetails, + callActive, + callbacks, + styleProps, + onLeave, +}) => { + const [isRecordingActive, setRecordingActive] = useState(false); + const [sttAutoStarted, setSttAutoStarted] = useState(false); + const [recordingAutoStarted, setRecordingAutoStarted] = useState(false); + const [breakoutRoomRTCProps, setBreakoutRoomRtcProps] = useState({ + ...rtcProps, + channel: breakoutChannelDetails.channel, + uid: breakoutChannelDetails.uid as number, + token: breakoutChannelDetails.token, + rtm: breakoutChannelDetails.rtmToken, + screenShareUid: breakoutChannelDetails?.screenShareUid as number, + screenShareToken: breakoutChannelDetails?.screenShareToken || '', + }); + console.log('supriya breakoutRoomRTCProps', breakoutRoomRTCProps); + + const {client, isLoggedIn} = useRTMCore(); + + useEffect(() => { + // Cleanup on unmount + if (client && isLoggedIn && rtcProps.channel) { + console.log( + `Breakout room unmounting, subsribing to: ${rtcProps.channel}`, + ); + try { + client.subscribe(rtcProps.channel); + RTMEngine.getInstance().addChannel(rtcProps.channel, false); + } catch (error) { + console.error('Failed to unsubscribe on unmount:', error); + } + } + return () => { + if (rtcProps.channel) { + console.log( + `Breakout room unmounting, unsubscribing from: ${rtcProps.channel}`, + ); + try { + client.unsubscribe(rtcProps.channel); + RTMEngine.getInstance().removeChannel(rtcProps.channel); + } catch (error) { + console.error('Failed to unsubscribe on unmount:', error); + } + } + }; + }, [client, isLoggedIn, rtcProps.channel]); + + // Modified callbacks that use the onLeave prop + const endCallModifiedCallbacks = useMemo( + () => ({ + ...callbacks, + EndCall: () => { + console.log('Breakout room end call triggered'); + // Use the parent's onLeave callback + onLeave?.(); + }, + }), + [callbacks, onLeave], + ); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + {!isMobileUA() && ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default BreakoutVideoCallContent; diff --git a/template/src/pages/video-call/MainVideoCallContent.tsx b/template/src/pages/video-call/MainVideoCallContent.tsx new file mode 100644 index 000000000..65d561a69 --- /dev/null +++ b/template/src/pages/video-call/MainVideoCallContent.tsx @@ -0,0 +1,212 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useState} from 'react'; +import {useCustomization} from 'customization-implementation'; +import { + RtcConfigure, + PropsProvider, + ChannelProfileType, + LocalUserContext, + CallbacksInterface, + RtcPropsInterface, +} from '../../../agora-rn-uikit'; +import RtmConfigure from '../../components/RTMConfigure'; +import DeviceConfigure from '../../components/DeviceConfigure'; +import {NetworkQualityProvider} from '../../components/NetworkQualityContext'; +import {ChatNotificationProvider} from '../../components/chat-notification/useChatNotification'; +import {ChatUIControlsProvider} from '../../components/chat-ui/useChatUIControls'; +import {ScreenShareProvider} from '../../components/contexts/ScreenShareContext'; +import {LiveStreamDataProvider} from '../../components/contexts/LiveStreamDataContext'; +import {VideoMeetingDataProvider} from '../../components/contexts/VideoMeetingDataContext'; +import {UserPreferenceProvider} from '../../components/useUserPreference'; +import EventsConfigure from '../../components/EventsConfigure'; +import PermissionHelper from '../../components/precall/PermissionHelper'; +import {LiveStreamContextProvider} from '../../components/livestream'; +import {PreCallProvider} from '../../components/precall/usePreCall'; +import Precall from '../../components/Precall'; +import {VideoCallProvider} from '../../components/useVideoCall'; +import SdkMuteToggleListener from '../../components/SdkMuteToggleListener'; +import {VBProvider} from '../../components/virtual-background/useVB'; +import {DisableChatProvider} from '../../components/disable-chat/useDisableChat'; +import {WaitingRoomProvider} from '../../components/contexts/WaitingRoomContext'; +import {ChatMessagesProvider} from '../../components/chat-messages/useChatMessages'; +import {BeautyEffectProvider} from '../../components/beauty-effect/useBeautyEffects'; +import {UserActionMenuProvider} from '../../components/useUserActionMenu'; +import {CaptionProvider} from '../../subComponents/caption/useCaption'; +import ScreenshareConfigure from '../../subComponents/screenshare/ScreenshareConfigure'; +import {RecordingProvider} from '../../subComponents/recording/useRecording'; +import {isMobileUA, isValidReactComponent} from '../../utils/common'; +import {LayoutProvider} from '../../utils/useLayout'; +import {SidePanelProvider} from '../../utils/useSidePanel'; +import {FocusProvider} from '../../utils/useFocus'; +import {NoiseSupressionProvider} from '../../app-state/useNoiseSupression'; +import {VideoQualityContextProvider} from '../../app-state/useVideoQuality'; +import VideoCallScreenWrapper from './../video-call/VideoCallScreenWrapper'; + +interface MainVideoCallContentProps { + callActive: boolean; + setCallActive: React.Dispatch>; + rtcProps: RtcPropsInterface; + setRtcProps: React.Dispatch>>; + callbacks: CallbacksInterface; + styleProps: any; +} + +const MainVideoCallContent: React.FC = ({ + callActive, + setCallActive, + rtcProps, + setRtcProps, + callbacks, + styleProps, +}) => { + const [isRecordingActive, setRecordingActive] = useState(false); + const [sttAutoStarted, setSttAutoStarted] = useState(false); + const [recordingAutoStarted, setRecordingAutoStarted] = useState(false); + const {PrefereceWrapper} = useCustomization(data => { + let components: { + PrefereceWrapper: React.ComponentType; + } = { + PrefereceWrapper: React.Fragment, + }; + if ( + data?.components?.preferenceWrapper && + typeof data?.components?.preferenceWrapper !== 'object' && + isValidReactComponent(data?.components?.preferenceWrapper) + ) { + components.PrefereceWrapper = data?.components?.preferenceWrapper; + } + + return components; + }); + + return ( + + + + + + + + + + + + + + + + + + + + + + + + {!isMobileUA() && ( + + )} + + + + {/* */} + + {callActive ? ( + + + + + + + + ) : $config.PRECALL ? ( + + + + ) : ( + <> + )} + + {/* */} + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default MainVideoCallContent; diff --git a/template/src/pages/video-call/VideoCallContent.tsx b/template/src/pages/video-call/VideoCallContent.tsx new file mode 100644 index 000000000..19c375641 --- /dev/null +++ b/template/src/pages/video-call/VideoCallContent.tsx @@ -0,0 +1,156 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useState, useEffect, useRef, useCallback} from 'react'; +import {useParams, useLocation, useHistory} from '../../components/Router'; +import events from '../../rtm-events-api'; +import {BreakoutChannelJoinEventPayload} from '../../components/breakout-room/state/types'; +import {CallbacksInterface, RtcPropsInterface} from 'agora-rn-uikit'; +import VideoCall from '../VideoCall'; +import BreakoutVideoCallContent from './BreakoutVideoCallContent'; +import {BreakoutRoomEventNames} from '../../components/breakout-room/events/constants'; +import BreakoutRoomTransition from '../../components/breakout-room/ui/BreakoutRoomTransition'; + +export interface BreakoutChannelDetails { + channel: string; + uid: number | string; + token: string; + screenShareUid: number | string; + screenShareToken: string; + rtmToken: string; +} + +export interface VideoCallContentProps { + callActive: boolean; + setCallActive: React.Dispatch>; + rtcProps: RtcPropsInterface; + setRtcProps: React.Dispatch>>; + callbacks: CallbacksInterface; + styleProps: any; +} + +const VideoCallContent: React.FC = props => { + const {phrase} = useParams<{phrase: string}>(); + const location = useLocation(); + const history = useHistory(); + + // Parse URL to determine current mode + const searchParams = new URLSearchParams(location.search); + const isBreakoutMode = searchParams.get('breakout') === 'true'; + + const breakoutTimeoutRef = useRef | null>(null); + + // Breakout channel details (populated by RTM events) + const [breakoutChannelDetails, setBreakoutChannelDetails] = + useState(null); + + // Listen for breakout room join events + useEffect(() => { + const handleBreakoutJoin = (evtData: any) => { + try { + // Clear any existing timeout + if (breakoutTimeoutRef.current) { + clearTimeout(breakoutTimeoutRef.current); + } + // Process the event payload + const {payload} = evtData; + const data: BreakoutChannelJoinEventPayload = JSON.parse(payload); + console.log('supriya Breakout room join event received', data); + if (data?.data?.act === 'CHAN_JOIN') { + const {channel_name, mainUser, screenShare, chat} = data.data.data; + // Extract breakout channel details + const breakoutDetails: BreakoutChannelDetails = { + channel: channel_name, + token: mainUser.rtc, + uid: mainUser?.uid || 0, + screenShareToken: screenShare.rtc, + screenShareUid: screenShare.uid, + rtmToken: mainUser.rtm, + }; + // Set breakout state active + history.push(`/${phrase}?breakout=true`); + setBreakoutChannelDetails(null); + // Add state after a delay to show transitioning screen + breakoutTimeoutRef.current = setTimeout(() => { + setBreakoutChannelDetails(prev => ({ + ...prev, + ...breakoutDetails, + })); + breakoutTimeoutRef.current = null; + }, 800); + } + } catch (error) { + console.error(' supriya Failed to process breakout join event'); + } + }; + + // Register breakout join event listener + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_JOIN_DETAILS, + handleBreakoutJoin, + ); + + return () => { + // Cleanup event listener + events.off( + BreakoutRoomEventNames.BREAKOUT_ROOM_JOIN_DETAILS, + handleBreakoutJoin, + ); + }; + }, [phrase]); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (breakoutTimeoutRef.current) { + clearTimeout(breakoutTimeoutRef.current); + } + }; + }, []); + + // Handle leaving breakout room + const handleLeaveBreakout = useCallback(() => { + console.log('Leaving breakout room, returning to main room'); + // Clear breakout channel details + setBreakoutChannelDetails(null); + // Navigate back to main room + history.push(`/${phrase}`); + }, [history, phrase]); + + // Conditional rendering based on URL params + return ( + <> + {isBreakoutMode ? ( + breakoutChannelDetails?.channel ? ( + // Breakout Room Mode - Fresh component instance + + ) : ( + { + setBreakoutChannelDetails(null); + }} + /> + ) + ) : ( + // Main Room Mode - Fresh component instance + + )} + + ); +}; + +export default VideoCallContent; diff --git a/template/src/pages/video-call/VideoCallStateSetup.tsx b/template/src/pages/video-call/VideoCallStateSetup.tsx index c17ec52ba..526ad48f0 100644 --- a/template/src/pages/video-call/VideoCallStateSetup.tsx +++ b/template/src/pages/video-call/VideoCallStateSetup.tsx @@ -13,7 +13,7 @@ const VideoCallStateSetup: React.FC = ({ useEffect(() => { if ($config.ENABLE_BREAKOUT_ROOM && RtcEngineUnsafe && setMainRtcEngine) { console.log( - 'supriya [VideoCallStateSetup] Engine ready, storing in orchestrator', + 'supriya [VideoCallStateSetup] Engine ready, storing in state', ); setMainRtcEngine(RtcEngineUnsafe); } @@ -27,7 +27,7 @@ const VideoCallStateSetup: React.FC = ({ setMainChannelDetails ) { console.log( - 'supriya [VideoCallStateSetup] Channel details ready, storing in orchestrator', + 'supriya [VideoCallStateSetup] Channel details ready, storing in state', ); setMainChannelDetails({ channel: roomInfo.channel || '', diff --git a/template/src/pages/video-call/VideoCallStateWrapper.tsx b/template/src/pages/video-call/VideoCallStateWrapper.tsx new file mode 100644 index 000000000..326329086 --- /dev/null +++ b/template/src/pages/video-call/VideoCallStateWrapper.tsx @@ -0,0 +1,485 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ +import React, {useState, useContext, useEffect, useRef} from 'react'; +import {View, StyleSheet, Text} from 'react-native'; +import {useCustomization} from 'customization-implementation'; +import { + ClientRoleType, + UidType, + CallbacksInterface, +} from '../../../agora-rn-uikit'; +import styles from '../../components/styles'; +import {ErrorContext} from '../../components/common/index'; +import {useWakeLock} from '../../components/useWakeLock'; +import {useParams, useHistory} from '../../components/Router'; +import StorageContext from '../../components/StorageContext'; +import {useSetRoomInfo} from '../../components/room-info/useSetRoomInfo'; +import {SdkApiContext} from '../../components/SdkApiContext'; +import { + useRoomInfo, + RoomInfoDefaultValue, + WaitingRoomStatus, +} from '../../components/room-info/useRoomInfo'; +import {useIsRecordingBot} from '../../subComponents/recording/useIsRecordingBot'; +import Logo from '../../subComponents/Logo'; +import SDKEvents from '../../utils/SdkEvents'; +import isSDK from '../../utils/isSDK'; +import {useHasBrandLogo} from '../../utils/common'; +import useJoinRoom from '../../utils/useJoinRoom'; +import {useString} from '../../utils/useString'; +import {AuthErrorCodes} from '../../utils/common'; +import { + userBannedText, + videoRoomStartingCallText, +} from '../../language/default-labels/videoCallScreenLabels'; +import {LogSource, logger} from '../../logger/AppBuilderLogger'; +import Toast from '../../../react-native-toast-message'; +import {RTMCoreProvider} from '../../rtm/RTMCoreProvider'; +import {videoView} from '../../../theme.json'; +import VideoCallContent from './VideoCallContent'; + +export enum RnEncryptionEnum { + /** + * @deprecated + * 0: This mode is deprecated. + */ + None = 0, + /** + * 1: (Default) 128-bit AES encryption, XTS mode. + */ + AES128XTS = 1, + /** + * 2: 128-bit AES encryption, ECB mode. + */ + AES128ECB = 2, + /** + * 3: 256-bit AES encryption, XTS mode. + */ + AES256XTS = 3, + /** + * 4: 128-bit SM4 encryption, ECB mode. + * + * @since v3.1.2. + */ + SM4128ECB = 4, + /** + * 6: 256-bit AES encryption, GCM mode. + * + * @since v3.1.2. + */ + AES256GCM = 6, + + /** + * 7: 128-bit GCM encryption, GCM mode. + * + * @since v3.4.5 + */ + AES128GCM2 = 7, + /** + * 8: 256-bit GCM encryption, GCM mode. + * @since v3.1.2. + * Compared to AES256GCM encryption mode, AES256GCM2 encryption mode is more secure and requires you to set the salt (encryptionKdfSalt). + */ + AES256GCM2 = 8, +} + +const VideoCallStateWrapper = () => { + const hasBrandLogo = useHasBrandLogo(); + const joiningLoaderLabel = useString(videoRoomStartingCallText)(); + const {isRecordingBot} = useIsRecordingBot(); + const {setRoomInfo} = useSetRoomInfo(); + const {setGlobalErrorMessage} = useContext(ErrorContext); + const bannedUserText = useString(userBannedText)(); + + /** + * Should we set the callscreen to active ?? + * a) If Recording bot( i.e prop: recordingBot) is TRUE then it means, + * the recording bot is accessing the screen - so YES we should set + * the callActive as true and we need not check for whether + * $config.PRECALL is enabled or not. + * b) If Recording bot( i.e prop: recordingBot) is FALSE then we should set + * the callActive depending upon the value of magic variable - $config.PRECALL + */ + const shouldCallBeSetToActive = isRecordingBot + ? true + : $config.PRECALL + ? false + : true; + const [callActive, setCallActive] = useState(shouldCallBeSetToActive); + const [queryComplete, setQueryComplete] = useState(false); + const [waitingRoomAttendeeJoined, setWaitingRoomAttendeeJoined] = + useState(false); + const {isJoinDataFetched, data, isInWaitingRoom, waitingRoomStatus} = + useRoomInfo(); + const {store} = useContext(StorageContext); + const { + join: SdkJoinState, + microphoneDevice: sdkMicrophoneDevice, + cameraDevice: sdkCameraDevice, + clearState, + } = useContext(SdkApiContext); + const useJoin = useJoinRoom(); + + const {phrase} = useParams<{phrase: string}>(); + const history = useHistory(); + const currentMeetingPhrase = useRef(history.location.pathname); + const {awake, release} = useWakeLock(); + + const [rtcProps, setRtcProps] = React.useState({ + appId: $config.APP_ID, + channel: null, + uid: null, + token: null, + rtm: null, + screenShareUid: null, + screenShareToken: null, + profile: $config.PROFILE, + screenShareProfile: $config.SCREEN_SHARE_PROFILE, + dual: true, + encryption: $config.ENCRYPTION_ENABLED + ? {key: null, mode: RnEncryptionEnum.AES128GCM2, screenKey: null} + : false, + role: ClientRoleType.ClientRoleBroadcaster, + geoFencing: $config.GEO_FENCING, + audioRoom: $config.AUDIO_ROOM, + activeSpeaker: $config.ACTIVE_SPEAKER, + preferredCameraId: + sdkCameraDevice.deviceId || store?.activeDeviceId?.videoinput || null, + preferredMicrophoneId: + sdkMicrophoneDevice.deviceId || store?.activeDeviceId?.audioinput || null, + recordingBot: isRecordingBot ? true : false, + }); + + React.useEffect(() => { + if ( + //isJoinDataFetched === true && (!queryComplete || !isInWaitingRoom) + //non waiting room - host/attendee + (!$config.ENABLE_WAITING_ROOM && + isJoinDataFetched === true && + !queryComplete) || + //waiting room - host + ($config.ENABLE_WAITING_ROOM && + isJoinDataFetched === true && + data.isHost && + !queryComplete) || + //waiting room - attendee + ($config.ENABLE_WAITING_ROOM && + isJoinDataFetched === true && + !data.isHost && + (!queryComplete || !isInWaitingRoom) && + !waitingRoomAttendeeJoined) + ) { + setRtcProps(prevRtcProps => ({ + ...prevRtcProps, + channel: data.channel, + uid: data.uid, + token: data.token, + rtm: data.rtmToken, + encryption: $config.ENCRYPTION_ENABLED + ? { + key: data.encryptionSecret, + mode: data.encryptionMode, + screenKey: data.encryptionSecret, + salt: data.encryptionSecretSalt, + } + : false, + screenShareUid: data.screenShareUid, + screenShareToken: data.screenShareToken, + role: data.isHost + ? ClientRoleType.ClientRoleBroadcaster + : ClientRoleType.ClientRoleAudience, + preventJoin: + !$config.ENABLE_WAITING_ROOM || + ($config.ENABLE_WAITING_ROOM && data.isHost) || + ($config.ENABLE_WAITING_ROOM && + !data.isHost && + waitingRoomStatus === WaitingRoomStatus.APPROVED) + ? false + : true, + })); + if ( + $config.ENABLE_WAITING_ROOM && + !data.isHost && + waitingRoomStatus === WaitingRoomStatus.APPROVED + ) { + setWaitingRoomAttendeeJoined(true); + } + // 1. Store the display name from API + // if (data.username) { + // setUsername(data.username); + // } + setQueryComplete(true); + } + }, [isJoinDataFetched, data, queryComplete]); + + useEffect(() => { + if (!isJoinDataFetched) { + return; + } + logger.log(LogSource.Internals, 'SET_MEETING_DETAILS', 'Room details', { + user_id: data?.uid || '', + meeting_title: data?.meetingTitle || '', + channel_id: data?.channel, + host_id: data?.roomId?.host || '', + attendee_id: data?.roomId?.attendee || '', + }); + }, [isJoinDataFetched, data, phrase]); + + // SDK related code + useEffect(() => { + if (!isSDK() || !SdkJoinState.initialized) { + return; + } + const { + phrase: sdkMeetingPhrase, + meetingDetails: sdkMeetingDetails, + skipPrecall, + promise, + preference, + } = SdkJoinState; + + const sdkMeetingPath = `/${sdkMeetingPhrase}`; + + setCallActive(skipPrecall); + + if (sdkMeetingDetails) { + setQueryComplete(false); + setRoomInfo(roomInfo => { + return { + ...roomInfo, + isJoinDataFetched: true, + data: { + ...roomInfo.data, + ...sdkMeetingDetails, + }, + roomPreference: preference, + }; + }); + } else if (sdkMeetingPhrase) { + setQueryComplete(false); + currentMeetingPhrase.current = sdkMeetingPath; + useJoin(sdkMeetingPhrase, preference) + .then(() => { + logger.log( + LogSource.Internals, + 'JOIN_MEETING', + 'Join channel success', + ); + }) + .catch(error => { + const errorCode = error?.code; + if (AuthErrorCodes.indexOf(errorCode) !== -1 && isSDK()) { + SDKEvents.emit('unauthorized', error); + } + logger.error( + LogSource.Internals, + 'JOIN_MEETING', + 'Join channel error', + JSON.stringify(error || {}), + ); + setGlobalErrorMessage(error); + history.push('/'); + currentMeetingPhrase.current = ''; + promise.rej(error); + }); + } + }, [SdkJoinState]); + + useEffect(() => { + if (!SdkJoinState?.phrase) { + useJoin(phrase, RoomInfoDefaultValue.roomPreference) + .then(() => { + logger.log( + LogSource.Internals, + 'JOIN_MEETING', + 'Join channel success', + ); + }) + .catch(error => { + const errorCode = error?.code; + if (AuthErrorCodes.indexOf(errorCode) !== -1 && isSDK()) { + SDKEvents.emit('unauthorized', error); + } + logger.error( + LogSource.Internals, + 'JOIN_MEETING', + 'Join channel error', + JSON.stringify(error || {}), + ); + setGlobalErrorMessage(error); + history.push('/'); + }); + } + }, []); + + React.useEffect(() => { + return () => { + logger.debug( + LogSource.Internals, + 'VIDEO_CALL_ROOM', + 'Videocall unmounted', + ); + setRoomInfo(prevState => { + return { + ...RoomInfoDefaultValue, + loginToken: prevState?.loginToken, + }; + }); + if (awake) { + release(); + } + }; + }, []); + + // commented for v1 release + const afterEndCall = useCustomization( + data => + data?.lifecycle?.useAfterEndCall && data?.lifecycle?.useAfterEndCall(), + ); + + const callbacks: CallbacksInterface = { + // RtcLeft: () => {}, + // RtcJoined: () => { + // if (SdkJoinState.phrase && SdkJoinState.skipPrecall) { + // SdkJoinState.promise?.res(); + // } + // }, + EndCall: () => { + clearState('join'); + setTimeout(() => { + // TODO: These callbacks are being called twice + SDKEvents.emit('leave'); + if (afterEndCall) { + afterEndCall(data.isHost, history as unknown as History); + } else { + history.push('/'); + } + }, 0); + }, + // @ts-ignore + UserJoined: (uid: UidType) => { + console.log('UIKIT Callback: UserJoined', uid); + SDKEvents.emit('rtc-user-joined', uid); + }, + // @ts-ignore + UserOffline: (uid: UidType) => { + console.log('UIKIT Callback: UserOffline', uid); + SDKEvents.emit('rtc-user-left', uid); + }, + // @ts-ignore + RemoteAudioStateChanged: (uid: UidType, status: 0 | 2) => { + console.log('UIKIT Callback: RemoteAudioStateChanged', uid, status); + if (status === 0) { + SDKEvents.emit('rtc-user-unpublished', uid, 'audio'); + } else { + SDKEvents.emit('rtc-user-published', uid, 'audio'); + } + }, + // @ts-ignore + RemoteVideoStateChanged: (uid: UidType, status: 0 | 2) => { + console.log('UIKIT Callback: RemoteVideoStateChanged', uid, status); + if (status === 0) { + SDKEvents.emit('rtc-user-unpublished', uid, 'video'); + } else { + SDKEvents.emit('rtc-user-published', uid, 'video'); + } + }, + // @ts-ignore + UserBanned(isBanned) { + console.log('UIKIT Callback: UserBanned', isBanned); + Toast.show({ + leadingIconName: 'alert', + type: 'error', + text1: bannedUserText, + visibilityTime: 3000, + }); + }, + }; + + return ( + <> + {queryComplete ? ( + queryComplete || !callActive ? ( + + + + ) : ( + + {hasBrandLogo() && } + {joiningLoaderLabel} + + ) + ) : ( + <> + )} + + ); +}; + +const styleProps = { + maxViewStyles: styles.temp, + minViewStyles: styles.temp, + localBtnContainer: styles.bottomBar, + localBtnStyles: { + muteLocalAudio: styles.localButton, + muteLocalVideo: styles.localButton, + switchCamera: styles.localButton, + endCall: styles.endCall, + fullScreen: styles.localButton, + recording: styles.localButton, + screenshare: styles.localButton, + }, + theme: $config.PRIMARY_ACTION_BRAND_COLOR, + remoteBtnStyles: { + muteRemoteAudio: styles.remoteButton, + muteRemoteVideo: styles.remoteButton, + remoteSwap: styles.remoteButton, + minCloseBtnStyles: styles.minCloseBtn, + liveStreamHostControlBtns: styles.liveStreamHostControlBtns, + }, + BtnStyles: styles.remoteButton, +}; +//change these to inline styles or sth +const style = StyleSheet.create({ + full: { + flex: 1, + flexDirection: 'column', + overflow: 'hidden', + }, + videoView: videoView, + loader: { + flex: 1, + alignSelf: 'center', + justifyContent: 'center', + }, + loaderLogo: { + alignSelf: 'center', + justifyContent: 'center', + marginBottom: 30, + }, + loaderText: {fontWeight: '500', color: $config.FONT_COLOR}, +}); + +export default VideoCallStateWrapper; diff --git a/template/src/rtm-events-api/Events.ts b/template/src/rtm-events-api/Events.ts index e6988ed0a..db8cd7e41 100644 --- a/template/src/rtm-events-api/Events.ts +++ b/template/src/rtm-events-api/Events.ts @@ -104,11 +104,13 @@ class Events { * * @param {Object} rtmPayload payload to be sent across * @param {ReceiverUid} toUid uid or uids[] of user + * @param {string} channelId optional specific channel ID, defaults to primary channel * @api private */ private _send = async ( rtmPayload: RTMAttributePayload, toUid?: ReceiverUid, + channelId?: string, ) => { const to = typeof toUid === 'string' ? parseInt(toUid, 10) : toUid; @@ -131,13 +133,14 @@ class Events { 'case 1 executed - sending in channel', ); try { - const channelId = RTMEngine.getInstance().channelUid; - if (!channelId || channelId.trim() === '') { + const targetChannelId = channelId || RTMEngine.getInstance().channelUid; + console.log('supriya targetChannelId: ', targetChannelId); + if (!targetChannelId || targetChannelId.trim() === '') { throw new Error( - 'Channel ID is not set. Cannot send channel attributes.', + 'Channel ID is not set. Cannot send channel messages.', ); } - await rtmEngine.publish(channelId, text, { + await rtmEngine.publish(targetChannelId, text, { channelType: nativeChannelTypeMapping.MESSAGE, // 1 is message }); } catch (error) { @@ -159,6 +162,7 @@ class Events { ); const adjustedUID = adjustUID(to); try { + console.log('supriya 2 '); await rtmEngine.publish(`${adjustedUID}`, text, { channelType: nativeChannelTypeMapping.USER, // user }); @@ -217,7 +221,10 @@ class Events { } }; - private _sendAsChannelAttribute = async (rtmPayload: RTMAttributePayload) => { + private _sendAsChannelAttribute = async ( + rtmPayload: RTMAttributePayload, + channelId?: string, + ) => { // Case 1: send to channel logger.debug( LogSource.Events, @@ -231,16 +238,14 @@ class Events { } const rtmEngine: RTMClient = RTMEngine.getInstance().engine; - const channelId = RTMEngine.getInstance().channelUid; - if (!channelId || channelId.trim() === '') { - throw new Error( - 'Channel ID is not set. Cannot send channel attributes.', - ); + const targetChannelId = RTMEngine.getInstance().channelUid; + if (!targetChannelId || targetChannelId.trim() === '') { + throw new Error('Channel ID is not set. Cannot send channel messages.'); } const rtmAttribute = [{key: rtmPayload.evt, value: rtmPayload.value}]; await rtmEngine.storage.setChannelMetadata( - channelId, + targetChannelId, nativeChannelTypeMapping.MESSAGE, { items: rtmAttribute, @@ -345,6 +350,7 @@ class Events { * @param {String} payload (optional) Additional data to be sent along with the event. * @param {Enum} persistLevel (optional) set different levels of persistance. Default value is Level 1 * @param {ReceiverUid} receiver (optional) uid or uid array. Default mode sends message in channel. + * @param {String} channelId (optional) specific channel to send to, defaults to primary channel. * @api public * */ send = async ( @@ -352,6 +358,7 @@ class Events { payload: string = '', persistLevel: PersistanceLevel = PersistanceLevel.None, receiver: ReceiverUid = -1, + channelId?: string, ) => { try { if (!this._validateEvt(eventName)) { @@ -397,9 +404,9 @@ class Events { persistValue, ); if (persistLevel === PersistanceLevel.Channel) { - await this._sendAsChannelAttribute(rtmPayload); + await this._sendAsChannelAttribute(rtmPayload, channelId); } else { - await this._send(rtmPayload, receiver); + await this._send(rtmPayload, receiver, channelId); } } catch (error) { logger.error( diff --git a/template/src/rtm-events/constants.ts b/template/src/rtm-events/constants.ts index 04bb65d98..04fbfadb2 100644 --- a/template/src/rtm-events/constants.ts +++ b/template/src/rtm-events/constants.ts @@ -40,9 +40,6 @@ const BOARD_COLOR_CHANGED = 'BOARD_COLOR_CHANGED'; const WHITEBOARD_LAST_IMAGE_UPLOAD_POSITION = 'WHITEBOARD_L_I_U_P'; const RECORDING_DELETED = 'RECORDING_DELETED'; const SPOTLIGHT_USER_CHANGED = 'SPOTLIGHT_USER_CHANGED'; -// 9. BREAKOUT ROOM -const BREAKOUT_ROOM_JOIN_DETAILS = 'BREAKOUT_ROOM_BREAKOUT_ROOM_JOIN_DETAILS'; -const BREAKOUT_ROOM_STATE = 'BREAKOUT_ROOM_BREAKOUT_ROOM_STATE'; const EventNames = { RECORDING_STATE_ATTRIBUTE, @@ -65,8 +62,6 @@ const EventNames = { WHITEBOARD_LAST_IMAGE_UPLOAD_POSITION, RECORDING_DELETED, SPOTLIGHT_USER_CHANGED, - BREAKOUT_ROOM_JOIN_DETAILS, - BREAKOUT_ROOM_STATE, }; /** ***** EVENT NAMES ENDS ***** */ diff --git a/template/src/rtm/RTMConfigure-v2.tsx b/template/src/rtm/RTMConfigure-v2.tsx new file mode 100644 index 000000000..87436f157 --- /dev/null +++ b/template/src/rtm/RTMConfigure-v2.tsx @@ -0,0 +1,774 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useState, useContext, useEffect, useRef} from 'react'; +import { + type GetChannelMetadataResponse, + type GetOnlineUsersResponse, + type MessageEvent, + type PresenceEvent, + type StorageEvent, + type GetUserMetadataResponse, + type SetOrUpdateUserMetadataOptions, + type Metadata, +} from 'agora-react-native-rtm'; +import { + ContentInterface, + DispatchContext, + PropsContext, + useLocalUid, +} from '../../agora-rn-uikit'; +import ChatContext from '../components/ChatContext'; +import {Platform} from 'react-native'; +import {backOff} from 'exponential-backoff'; +import {isAndroid} from '../utils/common'; +import {useContent} from 'customization-api'; +import { + safeJsonParse, + timeNow, + hasJsonStructure, + getMessageTime, + get32BitUid, +} from '../rtm/utils'; +import {EventUtils, EventsQueue} from '../rtm-events'; +import {PersistanceLevel} from '../rtm-events-api'; +import {filterObject} from '../utils'; +import SDKEvents from '../utils/SdkEvents'; +import isSDK from '../utils/isSDK'; +import { + WaitingRoomStatus, + useRoomInfo, +} from '../components/room-info/useRoomInfo'; +import LocalEventEmitter, { + LocalEventsEnum, +} from '../rtm-events-api/LocalEvents'; +import {controlMessageEnum} from '../components/ChatContext'; +import {LogSource, logger} from '../logger/AppBuilderLogger'; +import {RECORDING_BOT_UID} from '../utils/constants'; +import { + nativeChannelTypeMapping, + nativePresenceEventTypeMapping, + nativeStorageEventTypeMapping, +} from '../../bridge/rtm/web/Types'; +import {useRTMCore} from './RTMCoreProvider'; +import RTMEngine from './RTMEngine'; + +export enum UserType { + ScreenShare = 'screenshare', +} + +const eventTimeouts = new Map>(); + +interface RTMConfigureProps { + children: React.ReactNode; + channelName: string; + callActive: boolean; +} + +const RTMConfigure = (props: RTMConfigureProps) => { + const {channelName, callActive, children} = props; + const {client, isLoggedIn} = useRTMCore(); + const localUid = useLocalUid(); + const {dispatch} = useContext(DispatchContext); + const {defaultContent, activeUids} = useContent(); + const { + waitingRoomStatus, + data: {isHost}, + } = useRoomInfo(); + + const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); + const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); + const [onlineUsersCount, setTotalOnlineUsers] = useState(0); + const timerValueRef: any = useRef(5); + const rtmInitTimstamp = useRef(new Date().getTime()); + + // Subscribe to main channel - traditional approach for core functionality + useEffect(() => { + if (!isLoggedIn || !channelName || !client || !callActive) { + return; + } + const subscribeToMainChannel = async () => { + try { + logger.log( + LogSource.AgoraSDK, + 'RTMConfigure', + `Subscribing to main channel: ${channelName}`, + ); + + await client.subscribe(channelName, { + withMessage: true, + withPresence: true, + withMetadata: true, + withLock: false, + }); + RTMEngine.getInstance().addChannel(channelName); + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM setChannelId as subscribe is successful', + channelName, + ); + + logger.debug( + LogSource.SDK, + 'Event', + 'Emitting rtm joined', + channelName, + ); + // @ts-ignore + SDKEvents.emit('_rtm-joined', rtcProps.channel); + timerValueRef.current = 5; + await getMembers(); + await readAllChannelAttributes(); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + '❌ Main channel subscription failed:', + error, + ); + setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); + subscribeToMainChannel(); + }, timerValueRef.current * 1000); + } + }; + + const runQueuedEvents = async () => { + try { + while (!EventsQueue.isEmpty()) { + const currEvt = EventsQueue.dequeue(); + await eventDispatcher(currEvt.data, `${currEvt.uid}`, currEvt.ts); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Error running queued events', + error, + ); + } + }; + timerValueRef.current = 5; + subscribeToMainChannel(); + setHasUserJoinedRTM(true); + await runQueuedEvents(); + setIsInitialQueueCompleted(true); + return () => { + // Cleanup: unsubscribe from main channel + if (client && channelName) { + client.unsubscribe(channelName).catch(error => { + logger.warn( + LogSource.AgoraSDK, + 'RTMConfigure', + `Failed to unsubscribe from ${channelName}:`, + error, + ); + }); + logger.log( + LogSource.AgoraSDK, + 'RTMConfigure', + `🔌 Unsubscribed from main channel: ${channelName}`, + ); + } + }; + }, [isLoggedIn, channelName, client, callActive]); + + /** + * State refs for event callbacks + */ + const isHostRef = useRef({isHost: isHost}); + useEffect(() => { + isHostRef.current.isHost = isHost; + }, [isHost]); + + const waitingRoomStatusRef = useRef({waitingRoomStatus: waitingRoomStatus}); + useEffect(() => { + waitingRoomStatusRef.current.waitingRoomStatus = waitingRoomStatus; + }, [waitingRoomStatus]); + + const activeUidsRef = useRef({activeUids: activeUids}); + useEffect(() => { + activeUidsRef.current.activeUids = activeUids; + }, [activeUids]); + + const defaultContentRef = useRef({defaultContent: defaultContent}); + useEffect(() => { + defaultContentRef.current.defaultContent = defaultContent; + }, [defaultContent]); + + // Cleanup timeouts on unmount + const isRTMMounted = useRef(true); + useEffect(() => { + return () => { + isRTMMounted.current = false; + for (const timeout of eventTimeouts.values()) { + clearTimeout(timeout); + } + eventTimeouts.clear(); + }; + }, []); + + // Set online users count + useEffect(() => { + setTotalOnlineUsers( + Object.keys( + filterObject( + defaultContent, + ([k, v]) => + v?.type === 'rtc' && + !v.offline && + activeUidsRef.current.activeUids.indexOf(v?.uid) !== -1, + ), + ).length, + ); + }, [defaultContent]); + + // Handle channel joined - fetch initial data + const handleChannelJoined = async () => { + try { + await Promise.all([getMembers(), readAllChannelAttributes()]); + await runQueuedEvents(); + setIsInitialQueueCompleted(true); + logger.log( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Channel initialization completed', + ); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Channel initialization failed', + error, + ); + } + }; + + const getMembers = async () => { + if (!client || !channelName) { + return; + } + try { + const data: GetOnlineUsersResponse = await client.presence.getOnlineUsers( + channelName, + 1, + ); + logger.log( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Online users fetched', + data, + ); + + await Promise.all( + data.occupants?.map(async member => { + try { + const backoffAttributes = await fetchUserAttributesWithBackoffRetry( + member.userId, + ); + await processUserUidAttributes(backoffAttributes, member.userId); + + // Add user attributes to queue for processing + backoffAttributes?.items?.forEach(item => { + try { + if (hasJsonStructure(item.value as string)) { + const eventData = { + evt: item.key, + value: item.value, + }; + EventsQueue.enqueue({ + data: eventData, + uid: member.userId, + ts: timeNow(), + }); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + `Failed to process user attribute for ${member.userId}`, + error, + ); + } + }); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + `Could not retrieve data for ${member.userId}`, + error, + ); + } + }) || [], + ); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Failed to get members', + error, + ); + } + }; + + const readAllChannelAttributes = async () => { + if (!client || !channelName) { + return; + } + try { + const data: GetChannelMetadataResponse = + await client.storage.getChannelMetadata(channelName, 1); + + for (const item of data.items) { + try { + const {key, value, authorUserId, updateTs} = item; + if (hasJsonStructure(value as string)) { + const evtData = { + evt: key, + value, + }; + EventsQueue.enqueue({ + data: evtData, + uid: authorUserId, + ts: updateTs, + }); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + `Failed to process channel attribute: ${JSON.stringify(item)}`, + error, + ); + } + } + logger.log( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Channel attributes read successfully', + data, + ); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Failed to read channel attributes', + error, + ); + } + }; + + const fetchUserAttributesWithBackoffRetry = async ( + userId: string, + ): Promise => { + if (!client) throw new Error('RTM client not available'); + + return backOff( + async () => { + const attr: GetUserMetadataResponse = + await client.storage.getUserMetadata({ + userId: userId, + }); + + if (!attr || !attr.items || attr.items.length === 0) { + throw new Error('No attributes found'); + } + + return attr; + }, + { + retry: (e, idx) => { + logger.debug( + LogSource.AgoraSDK, + 'RTMConfigure', + `Retrying user attributes fetch for ${userId}, attempt ${idx}`, + e, + ); + return true; + }, + }, + ); + }; + + const processUserUidAttributes = async ( + attr: GetUserMetadataResponse, + userId: string, + ) => { + try { + const uid = parseInt(userId, 10); + const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); + const isHostItem = attr?.items?.find(item => item.key === 'isHost'); + const screenUid = screenUidItem?.value + ? parseInt(screenUidItem.value, 10) + : undefined; + + // Update user data in RTC + const userData = { + screenUid: screenUid, + type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', + uid, + offline: false, + isHost: isHostItem?.value === 'true', + lastMessageTimeStamp: 0, + }; + updateRenderListState(uid, userData); + + // Update screenshare data in RTC + if (screenUid) { + const screenShareUser = { + type: UserType.ScreenShare, + parentUid: uid, + }; + updateRenderListState(screenUid, screenShareUser); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + `Failed to process user data for ${userId}`, + error, + ); + } + }; + + const updateRenderListState = ( + uid: number, + data: Partial, + ) => { + dispatch({type: 'UpdateRenderList', value: [uid, data]}); + }; + + const eventDispatcher = async ( + data: { + evt: string; + value: string; + feat?: string; + etyp?: string; + }, + sender: string, + ts: number, + ) => { + let evt = '', + value = ''; + + if (data?.feat === 'BREAKOUT_ROOM') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + data: data.data, + action: data.act, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } else if (data?.feat === 'WAITING_ROOM') { + if (data?.etyp === 'REQUEST') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + attendee_uid: data.data.data.attendee_uid, + attendee_screenshare_uid: data.data.data.attendee_screenshare_uid, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } + if (data?.etyp === 'RESPONSE') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + approved: data.data.data.approved, + channelName: data.data.data.channel_name, + mainUser: data.data.data.mainUser, + screenShare: data.data.data.screenShare, + whiteboard: data.data.data.whiteboard, + chat: data.data.data?.chat, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } + } else { + if ( + $config.ENABLE_WAITING_ROOM && + !isHostRef.current?.isHost && + waitingRoomStatusRef.current?.waitingRoomStatus !== + WaitingRoomStatus.APPROVED + ) { + if ( + data.evt === controlMessageEnum.muteAudio || + data.evt === controlMessageEnum.muteVideo + ) { + return; + } else { + evt = data.evt; + value = data.value; + } + } else { + evt = data.evt; + value = data.value; + } + } + + try { + let parsedValue; + try { + parsedValue = typeof value === 'string' ? JSON.parse(value) : value; + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Failed to parse event value', + error, + ); + return; + } + + const {payload, persistLevel, source} = parsedValue; + + // Set local attributes if needed + if (persistLevel === PersistanceLevel.Session && client) { + const rtmAttribute = {key: evt, value: value}; + const options: SetOrUpdateUserMetadataOptions = { + userId: `${localUid}`, + }; + await client.storage.setUserMetadata( + { + items: [rtmAttribute], + }, + options, + ); + } + + // Emit the event + EventUtils.emitEvent(evt, source, {payload, persistLevel, sender, ts}); + + // Handle name events with timeout + if (evt === 'name') { + if (eventTimeouts.has(sender)) { + clearTimeout(eventTimeouts.get(sender)!); + } + const timeout = setTimeout(() => { + if (!isRTMMounted.current) { + return; + } + EventUtils.emitEvent(evt, source, { + payload, + persistLevel, + sender, + ts, + }); + eventTimeouts.delete(sender); + }, 200); + eventTimeouts.set(sender, timeout); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Error dispatching event', + error, + ); + } + }; + + // Event listeners setup + useEffect(() => { + if (!client) { + return; + } + + const handleStorageEvent = (storage: StorageEvent) => { + if ( + storage.eventType === nativeStorageEventTypeMapping.SET || + storage.eventType === nativeStorageEventTypeMapping.UPDATE + ) { + try { + if (storage.data?.items && Array.isArray(storage.data.items)) { + storage.data.items.forEach(item => { + try { + if (!item || !item.key) return; + const {key, value, authorUserId, updateTs} = item; + const timestamp = getMessageTime(updateTs); + const sender = Platform.OS + ? get32BitUid(authorUserId) + : parseInt(authorUserId, 10); + eventDispatcher({evt: key, value}, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Failed to process storage item', + error, + ); + } + }); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Error handling storage event', + error, + ); + } + } + }; + + const handlePresenceEvent = async (presence: PresenceEvent) => { + if (`${localUid}` === presence.publisher) { + return; + } + + if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { + logger.log( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Remote user joined', + presence, + ); + try { + const backoffAttributes = await fetchUserAttributesWithBackoffRetry( + presence.publisher, + ); + await processUserUidAttributes(backoffAttributes, presence.publisher); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Failed to process joined user', + error, + ); + } + } + + if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { + logger.log( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Remote user left', + presence, + ); + const uid = presence?.publisher + ? parseInt(presence.publisher, 10) + : undefined; + if (uid) { + SDKEvents.emit('_rtm-left', uid); + updateRenderListState(uid, {offline: true}); + } + } + }; + + const handleMessageEvent = (message: MessageEvent) => { + if (`${localUid}` === message.publisher) { + return; + } + + if (message.channelType === nativeChannelTypeMapping.MESSAGE) { + const { + publisher: uid, + channelName: msgChannelName, + message: text, + timestamp: ts, + } = message; + + // Whiteboard upload handling + if (parseInt(uid, 10) === 1010101) { + const [err, res] = safeJsonParse(text); + if (!err && res?.data?.data?.images) { + LocalEventEmitter.emit( + LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, + res.data.data.images, + ); + } + return; + } + + // Regular messages + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Failed to parse message', + err, + ); + return; + } + + const timestamp = getMessageTime(ts); + const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); + + if (msgChannelName === channelName) { + eventDispatcher(msg, `${sender}`, timestamp); + } + } + + if (message.channelType === nativeChannelTypeMapping.USER) { + const {publisher: peerId, timestamp: ts, message: text} = message; + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Failed to parse user message', + err, + ); + return; + } + + const timestamp = getMessageTime(ts); + const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); + eventDispatcher(msg, `${sender}`, timestamp); + } + }; + + // Add event listeners + client.addEventListener('storage', handleStorageEvent); + client.addEventListener('presence', handlePresenceEvent); + client.addEventListener('message', handleMessageEvent); + + return () => { + // Remove event listeners + client.removeEventListener('storage', handleStorageEvent); + client.removeEventListener('presence', handlePresenceEvent); + client.removeEventListener('message', handleMessageEvent); + }; + }, [client, channelName, localUid]); + + return ( + + {children} + + ); +}; + +export default RTMConfigure; diff --git a/template/src/rtm/RTMCoreProvider.tsx b/template/src/rtm/RTMCoreProvider.tsx new file mode 100644 index 000000000..8155624ae --- /dev/null +++ b/template/src/rtm/RTMCoreProvider.tsx @@ -0,0 +1,329 @@ +import React, { + useRef, + useState, + useEffect, + createContext, + useContext, + useCallback, + useMemo, +} from 'react'; +import type { + RTMClient, + LinkStateEvent, + MessageEvent, + PresenceEvent, + StorageEvent, + Metadata, + SetOrUpdateUserMetadataOptions, +} from 'agora-react-native-rtm'; +import {UidType} from '../../agora-rn-uikit'; +import RTMEngine from '../rtm/RTMEngine'; +import {nativePresenceEventTypeMapping} from '../../bridge/rtm/web/Types'; + +// Event callback types +type MessageCallback = (message: MessageEvent) => void; +type PresenceCallback = (presence: PresenceEvent) => void; +type StorageCallback = (storage: StorageEvent) => void; + +interface EventCallbacks { + message?: MessageCallback; + presence?: PresenceCallback; + storage?: StorageCallback; +} + +interface RTMContextType { + client: RTMClient | null; + connectionState: number; + error: Error | null; + isLoggedIn: boolean; + onlineUsers: Set; + // Callback registration methods + registerCallbacks: (channelName: string, callbacks: EventCallbacks) => void; + unregisterCallbacks: (channelName: string) => void; +} + +const RTMContext = createContext({ + client: null, + connectionState: 0, + error: null, + isLoggedIn: false, + onlineUsers: new Set(), + registerCallbacks: () => {}, + unregisterCallbacks: () => {}, +}); + +interface RTMCoreProviderProps { + children: React.ReactNode; + userInfo: { + localUid: UidType; + screenShareUid: UidType; + isHost: boolean; + rtmToken?: string; + }; +} + +export const RTMCoreProvider: React.FC = ({ + userInfo, + children, +}) => { + const [client, setClient] = useState(null); // Use state instead + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [connectionState, setConnectionState] = useState(0); + console.log('supriya-rtm-restest connectionState: ', connectionState); + const [error, setError] = useState(null); + const [onlineUsers, setOnlineUsers] = useState>(new Set()); + // Callback registration storage + const callbackRegistry = useRef>(new Map()); + + // Memoize userInfo to prevent unnecessary re-renders + const stableUserInfo = useMemo( + () => ({ + localUid: userInfo.localUid, + screenShareUid: userInfo.screenShareUid, + isHost: userInfo.isHost, + rtmToken: userInfo.rtmToken, + }), + [ + userInfo.localUid, + userInfo.screenShareUid, + userInfo.isHost, + userInfo.rtmToken, + ], + ); + + // Login function + const loginToRTM = async ( + rtmClient: RTMClient, + loginToken: string, + retryCount = 0, + ) => { + try { + try { + // 1. Handle ghost sessions, so do logout to leave any ghost sessions + await rtmClient.logout(); + // 2. Wait for sometime + await new Promise(resolve => setTimeout(resolve, 500)); + // 3. Login again + await rtmClient.login({token: loginToken}); + // 4. Wait for sometime + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (logoutError) { + console.log('logoutError: ', logoutError); + } + } catch (loginError) { + if (retryCount < 5) { + // Retry with exponential backoff (capped at 30s) + const delay = Math.min(1000 * Math.pow(2, retryCount), 30000); + await new Promise(resolve => setTimeout(resolve, delay)); + return loginToRTM(rtmClient, loginToken, retryCount + 1); + } else { + const contextError = new Error( + `RTM login failed after retries: ${error.message}`, + ); + setError(contextError); + } + } + }; + + const setAttribute = useCallback(async (rtmClient: RTMClient, userInfo) => { + const rtmAttributes = [ + {key: 'screenUid', value: String(userInfo.screenShareUid)}, + {key: 'isHost', value: String(userInfo.isHost)}, + ]; + try { + const data: Metadata = { + items: rtmAttributes, + }; + const options: SetOrUpdateUserMetadataOptions = { + userId: `${userInfo.localUid}`, + }; + await rtmClient.storage.setUserMetadata(data, options); + } catch (setAttributeError) { + console.log('setAttributeError: ', setAttributeError); + } + }, []); + + // Callback registration methods + const registerCallbacks = ( + channelName: string, + callbacks: EventCallbacks, + ) => { + callbackRegistry.current.set(channelName, callbacks); + }; + + const unregisterCallbacks = (channelName: string) => { + callbackRegistry.current.delete(channelName); + }; + + // Global event listeners - centralized in RTMCoreProvider + useEffect(() => { + if (!client) { + return; + } + const handleGlobalStorageEvent = (storage: StorageEvent) => { + console.log( + 'supriya-rtm-global ********************** ---StorageEvent event: ', + storage, + ); + // Distribute to all registered callbacks + callbackRegistry.current.forEach((callbacks, channelName) => { + if (callbacks.storage) { + try { + callbacks.storage(storage); + } catch (globalStorageCbError) { + console.log('globalStorageCbError: ', globalStorageCbError); + } + } + }); + }; + + const handleGlobalPresenceEvent = (presence: PresenceEvent) => { + console.log( + 'supriya-rtm-global @@@@@@@@@@@@@@@@@@@@@@@ ---PresenceEvent: ', + presence, + ); + if (presence.type === nativePresenceEventTypeMapping.SNAPSHOT) { + // Initial snapshot - set all online users + setOnlineUsers( + new Set(presence.snapshot?.userStateList.map(u => u.userId) || []), + ); + } else if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { + setOnlineUsers(prev => new Set([...prev, presence.publisher])); + } else if ( + presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE + ) { + setOnlineUsers(prev => { + const newSet = new Set(prev); + newSet.delete(presence.publisher); + return newSet; + }); + } + // Distribute to all registered callbacks + callbackRegistry.current.forEach((callbacks, channelName) => { + if (callbacks.presence) { + try { + callbacks.presence(presence); + } catch (globalPresenceCbError) { + console.log('globalPresenceCbError: ', globalPresenceCbError); + } + } + }); + }; + + const handleGlobalMessageEvent = (message: MessageEvent) => { + console.log( + 'supriya-rtm-global ######################## ---MessageEvent event: ', + message, + ); + console.log('supriya callbackRegistry', callbackRegistry); + // Distribute to all registered callbacks + callbackRegistry.current.forEach((callbacks, channelName) => { + if (callbacks.message) { + try { + callbacks.message(message); + } catch (globalMessageCbError) { + console.log('globalMessageCbError: ', globalMessageCbError); + } + } + }); + }; + + client.addEventListener('storage', handleGlobalStorageEvent); + client.addEventListener('presence', handleGlobalPresenceEvent); + client.addEventListener('message', handleGlobalMessageEvent); + + return () => { + console.log('supriya removing up global listeners'); + // Remove global event listeners + client.removeEventListener('storage', handleGlobalStorageEvent); + client.removeEventListener('presence', handleGlobalPresenceEvent); + client.removeEventListener('message', handleGlobalMessageEvent); + }; + }, [client]); + + useEffect(() => { + if (client) { + return; + } + const initializeRTM = async () => { + // 1, Check if engine is already connected + // 2. Initialize RTM Engine + if (!RTMEngine.getInstance()?.isEngineReady) { + RTMEngine.getInstance().setLocalUID(stableUserInfo.localUid); + } + const rtmClient = RTMEngine.getInstance().engine; + if (!rtmClient) { + throw new Error('Failed to create RTM client'); + } + setClient(rtmClient); // Set client after successful setup + + // 3. Global linkState listener + const onLink = async (evt: LinkStateEvent) => { + setConnectionState(evt.currentState); + if (evt.currentState === 0 /* DISCONNECTED */) { + setIsLoggedIn(false); + console.warn('RTM disconnected. Attempting re-login...'); + if (stableUserInfo.rtmToken) { + try { + await loginToRTM(rtmClient, stableUserInfo.rtmToken); + await setAttribute(rtmClient, stableUserInfo); + console.log('RTM re-login successful.'); + } catch (err) { + console.error('RTM re-login failed:', err); + } + } + } + }; + rtmClient.addEventListener('linkState', onLink); + + try { + // 4. Client Login + if (stableUserInfo.rtmToken) { + await loginToRTM(rtmClient, stableUserInfo.rtmToken); + // 5. Set user attributes after successful login + await setAttribute(rtmClient, stableUserInfo); + setIsLoggedIn(true); + } + } catch (err) { + console.error('RTM login failed', err); + } + }; + + initializeRTM(); + + return () => { + // Cleanup + console.log('supriya-rtm-retest RTM cleanup is happening'); + if (client) { + console.log('supriya RTM cleanup is happening'); + client.removeAllListeners(); + client.logout().catch(() => {}); + RTMEngine.getInstance().destroy(); + setClient(null); + } + }; + }, [client, stableUserInfo, setAttribute]); + + return ( + + {children} + + ); +}; + +export const useRTMCore = () => { + const context = useContext(RTMContext); + if (!context) { + throw new Error('useRTMCore must be used within RTMCoreProvider'); + } + return context; +}; diff --git a/template/src/rtm/RTMEngine.ts b/template/src/rtm/RTMEngine.ts index 7b1dd00d5..202dd38b4 100644 --- a/template/src/rtm/RTMEngine.ts +++ b/template/src/rtm/RTMEngine.ts @@ -20,8 +20,9 @@ import {isAndroid, isIOS} from '../utils/common'; class RTMEngine { private _engine?: RTMClient; private localUID: string = ''; - private channelId: string = ''; - + private primaryChannelId: string = ''; + // track multiple subscribed channels + private channels: Set = new Set(); private static _instance: RTMEngine | null = null; private constructor() { @@ -32,6 +33,7 @@ class RTMEngine { return RTMEngine._instance; } + /** Get the singleton instance */ public static getInstance() { // We are only creating the instance but not creating the rtm client yet if (!RTMEngine._instance) { @@ -40,42 +42,49 @@ class RTMEngine { return RTMEngine._instance; } + /** Sets UID and initializes the client if needed */ setLocalUID(localUID: string | number) { - if (localUID === null || localUID === undefined) { + if (localUID == null) { throw new Error('setLocalUID: localUID cannot be null or undefined'); } - - const newUID = String(localUID); - if (newUID.trim() === '') { - throw new Error( - 'setLocalUID: localUID cannot be empty after string conversion', - ); + const newUID = String(localUID).trim(); + if (!newUID) { + throw new Error('setLocalUID: localUID cannot be empty'); } - - // If UID is changing and we have an existing engine, throw error if (this._engine && this.localUID !== newUID) { throw new Error( - `RTMEngine: Cannot change UID from '${this.localUID}' to '${newUID}' while engine is active. ` + - `Please call destroy() first, then setLocalUID() with the new UID.`, + `Cannot change UID from '${this.localUID}' to '${newUID}'. Call destroy() first.`, ); } - this.localUID = newUID; - if (!this._engine) { this.createClientInstance(); } } - setChannelId(channelID: string) { + addChannel(channelID: string, primary?: boolean) { if ( !channelID || typeof channelID !== 'string' || channelID.trim() === '' ) { - throw new Error('setChannelId: channelID must be a non-empty string'); + throw new Error( + 'addSecondaryChannel: channelID must be a non-empty string', + ); + } + this.channels.add(channelID); + if (primary) { + this.primaryChannelId = channelID; + } + } + + removeChannel(channelID: string) { + if (this.channels.has(channelID)) { + this.channels.delete(channelID); + if (channelID === this.primaryChannelId) { + this.primaryChannelId = ''; + } } - this.channelId = channelID; } get localUid() { @@ -83,13 +92,29 @@ class RTMEngine { } get channelUid() { - return this.channelId; + return this.primaryChannelId; + } + + get primaryChannel() { + return this.primaryChannelId; } + get allChannels() { + const channels = []; + this.channels.forEach(channel => channels.push(channel)); + return channels.filter(channel => channel.trim() !== ''); + } + + hasChannel(channelID: string): boolean { + return this.channels.has(channelID); + } + + /** Engine readiness flag */ get isEngineReady() { return !!this._engine && !!this.localUID; } + /** Access the RTMClient instance */ get engine(): RTMClient { this.ensureEngineReady(); return this._engine!; @@ -97,12 +122,11 @@ class RTMEngine { private ensureEngineReady() { if (!this.isEngineReady) { - throw new Error( - 'RTM Engine not ready. Please call setLocalUID() with a valid UID first.', - ); + throw new Error('RTM Engine not ready. Call setLocalUID() first.'); } } + /** Create the Agora RTM client */ private createClientInstance() { try { if (!this.localUID || this.localUID.trim() === '') { @@ -128,50 +152,47 @@ class RTMEngine { } private async destroyClientInstance() { + if (!this._engine) { + return; + } + + // Unsubscribe from all tracked channels + for (const channel of this.allChannels) { + try { + await this._engine.unsubscribe(channel); + } catch (err) { + console.warn(`Failed to unsubscribe from '${channel}':`, err); + } + } + + // 2. Remove all listeners if supported + try { + this._engine.removeAllListeners?.(); + } catch { + console.warn('Failed to remove listeners:'); + } + + // 3. Logout and release resources try { - if (this._engine) { - // 1. Unsubscribe from channel if we have one - if (this.channelId) { - try { - await this._engine.unsubscribe(this.channelId); - } catch (error) { - console.warn( - `Failed to unsubscribe from channel '${this.channelId}':`, - error, - ); - // Continue with cleanup even if unsubscribe fails - } - } - // 2. Remove all listeners - try { - this._engine.removeAllListeners?.(); - } catch (error) { - console.warn('Failed to remove listeners:', error); - } - // 3. Logout - try { - await this._engine.logout(); - if (isAndroid() || isIOS()) { - this._engine.release(); - } - } catch (error) { - console.warn('Failed to logout:', error); - } + await this._engine.logout(); + if (isAndroid() || isIOS()) { + this._engine.release(); } - } catch (error) { - console.error('Error during client instance destruction:', error); - // Don't re-throw - we want cleanup to complete + } catch (err) { + console.warn('RTM logout/release failed:', err); } } - async destroy() { + /** Fully destroy the singleton and cleanup */ + public async destroy() { try { if (!this._engine) { return; } - await this.destroyClientInstance(); - this.channelId = ''; + this.primaryChannelId = ''; + this.channels.clear(); + // Reset state this.localUID = ''; this._engine = undefined; RTMEngine._instance = null; diff --git a/template/src/rtm/hooks/useSubscribeChannel.tsx b/template/src/rtm/hooks/useSubscribeChannel.tsx new file mode 100644 index 000000000..09b5e7ba4 --- /dev/null +++ b/template/src/rtm/hooks/useSubscribeChannel.tsx @@ -0,0 +1,39 @@ +/* -------------------------------------------------------------------------- + * useSubscribeChannel.ts + * -------------------------------------------------------------------------- */ +import {useEffect} from 'react'; +import type {RTMClient} from 'agora-react-native-rtm'; +import RTMEngine from '../../rtm/RTMEngine'; + +export function useSubscribeChannel( + client: RTMClient | null, + channelId: string | null, +) { + useEffect(() => { + if (!client || !channelId) { + return; + } + let cancelled = false; + (async () => { + try { + await client.subscribe(channelId, { + withMessage: true, + withPresence: true, + withMetadata: true, + withLock: false, + }); + RTMEngine.getInstance().addChannel(channelId); + } catch (err) { + console.warn(`Failed to subscribe to secondary ${channelId}`, err); + } + })(); + + return () => { + if (!cancelled) { + client.unsubscribe(channelId).catch(() => {}); + RTMEngine.getInstance().removeChannel(channelId); + } + cancelled = true; + }; + }, [client, channelId]); +} diff --git a/template/src/subComponents/screenshare/ScreenshareButton.tsx b/template/src/subComponents/screenshare/ScreenshareButton.tsx index d34b607ac..f47475c5c 100644 --- a/template/src/subComponents/screenshare/ScreenshareButton.tsx +++ b/template/src/subComponents/screenshare/ScreenshareButton.tsx @@ -25,6 +25,7 @@ import { toolbarItemShareText, } from '../../language/default-labels/videoCallScreenLabels'; import {useToolbarProps} from '../../atoms/ToolbarItem'; +import {useBreakoutRoom} from '../../components/breakout-room/context/BreakoutRoomContext'; /** * A component to start and stop screen sharing on web clients. * Screen sharing is not yet implemented on mobile platforms. @@ -53,6 +54,8 @@ const ScreenshareButton = (props: ScreenshareButtonProps) => { const {setShowStartScreenSharePopup} = useVideoCall(); const screenShareButtonLabel = useString(toolbarItemShareText); const lstooltip = useString(livestreamingShareTooltipText); + const {permissions} = useBreakoutRoom(); + const onPress = () => { if (isScreenshareActive) { stopScreenshare(); @@ -104,6 +107,9 @@ const ScreenshareButton = (props: ScreenshareButtonProps) => { iconButtonProps.toolTipMessage = lstooltip(isHandRaised(local.uid)); iconButtonProps.disabled = true; } + if (!permissions.canScreenshare) { + iconButtonProps.disabled = true; + } return props?.render ? ( props.render(onPress, isScreenshareActive) From 4ab475a2c4fa72b582d71e685af19ccbba2887b3 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 8 Sep 2025 10:20:28 +0530 Subject: [PATCH 03/56] Sub feature/breakout room comphrensive check (#744) * close all rooms. * add icon * breakout room empty state for attendee * fix the label * add route check, modify logs,exit room and add polling * allow people to switch * supriya assignment * add and remove some files * add raise hand fill icon * remove unused file * reintroduce raise hand * polling state race condition * fix polling issue --- template/src/assets/font-styles.css | 8 + template/src/assets/fonts/icomoon.ttf | Bin 43152 -> 43744 bytes template/src/assets/selection.json | 2 +- template/src/atoms/CustomIcon.tsx | 1 + template/src/components/Controls.tsx | 10 +- .../context/BreakoutRoomContext.tsx | 255 ++++++++++++++---- .../events/BreakoutRoomEventsConfigure.tsx | 43 +-- .../BreakoutRoomMainEventsConfigure.tsx | 44 +++ .../hooks/useBreakoutRoomExit.ts | 20 +- .../components/breakout-room/state/reducer.ts | 2 +- .../ui/BreakoutRoomGroupSettings.tsx | 41 ++- .../ui/BreakoutRoomRaiseHand.tsx | 44 ++- .../breakout-room/ui/BreakoutRoomSettings.tsx | 24 +- .../ui/BreakoutRoomTransition.tsx | 18 +- .../breakout-room/ui/BreakoutRoomView.tsx | 11 +- .../ui/ParticipantManualAssignmentModal.tsx | 12 +- template/src/pages/BreakoutRoomVideoCall.tsx | 239 ---------------- template/src/pages/VideoCall.tsx | 10 +- .../src/pages/VideoCallRoomOrchestrator.tsx | 151 ----------- .../video-call/BreakoutVideoCallContent.tsx | 21 +- .../pages/video-call/MainVideoCallContent.tsx | 212 --------------- .../src/pages/video-call/VideoCallContent.tsx | 47 +++- .../pages/video-call/VideoCallStateSetup.tsx | 47 ---- template/src/rtm/RTMCoreProvider.tsx | 46 +++- .../src/rtm/hooks/useSubscribeChannel.tsx | 39 --- 25 files changed, 505 insertions(+), 842 deletions(-) create mode 100644 template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx delete mode 100644 template/src/pages/BreakoutRoomVideoCall.tsx delete mode 100644 template/src/pages/VideoCallRoomOrchestrator.tsx delete mode 100644 template/src/pages/video-call/MainVideoCallContent.tsx delete mode 100644 template/src/pages/video-call/VideoCallStateSetup.tsx delete mode 100644 template/src/rtm/hooks/useSubscribeChannel.tsx diff --git a/template/src/assets/font-styles.css b/template/src/assets/font-styles.css index ae7bf03c2..cad040807 100644 --- a/template/src/assets/font-styles.css +++ b/template/src/assets/font-styles.css @@ -24,6 +24,14 @@ -moz-osx-font-smoothing: grayscale; } +.icon-raise-hand-fill:before { + content: '\e9ac'; + color: #ffab00; +} +.icon-open-room:before { + content: '\e9ab'; + color: #fff; +} .icon-people-assigned:before { content: '\e9a4'; color: #e8eaed; diff --git a/template/src/assets/fonts/icomoon.ttf b/template/src/assets/fonts/icomoon.ttf index 2762c18eaf6fe73eb682220308c52979638ad5ad..420b2d10f3332eefb86cb852cdf4a931adccb266 100644 GIT binary patch delta 594 zcmbPmk?Fxzrg{cO1_lOhh6V;^1_S?KeItG$wwpkaJwTk0oSRr69(LK7fq_v5$PYR2gv`xz`%4QCqLOTQS*MN5d*{WEkHw3auX{G7>+SsVPIG(0o0(7mzbM6 z(~I*rkZ%E$Z!5?zE&)0e2v!}KxWk=s%_K%|#xsZEX?HccMn(* zV>YTe5F;mZC_LBYXJ=yvB7Uw_%%Va^h28f~v#{Z~f&3Rh`L=@m;u4@kfnY_!#2xO8t0pmeGp^d~!^pZ&8RQQjP<%5nJD%U>D+4zR pnE&KVn=pt0q9^ZIV$QgJ^M@sSm^N=&p1{Zk_Wb(Iaw`up0sx25P9FdO diff --git a/template/src/assets/selection.json b/template/src/assets/selection.json index 874a4d5ac..665de4929 100644 --- a/template/src/assets/selection.json +++ b/template/src/assets/selection.json @@ -1 +1 @@ -{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M371.106 408.733c-36.8 0-68.042-12.842-93.728-38.528-25.686-25.692-38.528-56.938-38.528-93.738s12.842-68.043 38.528-93.728c25.686-25.692 56.928-38.538 93.728-38.538s68.046 12.846 93.739 38.538c25.685 25.686 38.528 56.928 38.528 93.728s-12.843 68.046-38.528 93.738c-25.693 25.686-56.939 38.528-93.739 38.528zM759.255 888.315c-51.292 0-94.879-17.935-130.765-53.811-35.871-35.876-53.811-79.457-53.811-130.749 0-51.231 17.94-94.909 53.811-131.041 35.886-36.132 79.473-54.2 130.765-54.2 51.22 0 94.899 18.068 131.031 54.2s54.195 79.811 54.195 131.041c0 51.292-18.063 94.874-54.195 130.749s-79.811 53.811-131.031 53.811zM735.171 799.462l131.604-131.2-32.983-32.573-98.621 98.217-50.381-51.446-32.978 33.633 83.359 83.369zM85.25 699.515v-73.513c0-16.297 3.897-30.587 11.69-42.87 7.794-12.278 18.684-22.508 32.672-30.674 36.992-21.258 74.741-37.617 113.248-49.079 38.514-11.456 81.263-17.184 128.245-17.184 37.853 0 73.408 4.061 106.667 12.182s64.901 19.543 94.935 34.263c-22.154 22.641-39.818 48.184-53.002 76.626-13.186 28.447-20.87 58.532-23.053 90.25h-411.403z","M622.454 379.679c25.687-25.692 38.528-56.938 38.528-93.738s-12.841-68.043-38.528-93.728c-25.682-25.693-56.924-38.539-93.727-38.539-1.864 0-3.369-0.025-4.511-0.075-1.152-0.057-2.657-0.057-4.511 0 15.037 18.432 27.167 38.627 36.383 60.586 9.216 21.966 13.824 45.871 13.824 71.712 0 25.834-4.516 49.834-13.548 72-9.037 22.165-21.258 42.297-36.659 60.394 2.673 0.598 4.178 0.733 4.511 0.406 0.328-0.327 1.828-0.491 4.511-0.491 36.803 0 68.045-12.842 93.727-38.528z"],"attrs":[{"fill":"rgb(232, 234, 237)"},{"fill":"rgb(232, 234, 237)"}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["people-assigned"]},"attrs":[{"fill":"rgb(232, 234, 237)"},{"fill":"rgb(232, 234, 237)"}],"properties":{"order":869,"id":141,"name":"people-assigned","prevSize":32,"code":59812},"setIdx":0,"setId":1,"iconIdx":0},{"icon":{"paths":["M512 574.673l-173.781 173.129c-5.909 5.905-13.227 9.033-21.952 9.387-8.725 0.358-16.398-2.773-23.019-9.387-6.179-6.187-9.269-13.679-9.269-22.485 0-8.802 3.090-16.299 9.269-22.485l172.727-172.715c12.523-12.523 27.861-18.782 46.025-18.782s33.506 6.259 46.029 18.782l172.723 172.715c5.901 5.909 9.033 13.227 9.387 21.952s-2.773 16.401-9.387 23.019c-6.187 6.182-13.683 9.271-22.485 9.271s-16.299-3.089-22.485-9.271l-173.781-173.129zM512 318.672l-173.781 173.129c-5.909 5.905-13.227 9.033-21.952 9.387-8.725 0.358-16.398-2.773-23.019-9.387-6.179-6.187-9.269-13.679-9.269-22.485 0-8.802 3.090-16.299 9.269-22.485l172.727-172.714c12.523-12.523 27.861-18.784 46.025-18.784s33.506 6.261 46.029 18.784l172.723 172.714c5.901 5.909 9.033 13.227 9.387 21.952s-2.773 16.401-9.387 23.019c-6.187 6.182-13.683 9.271-22.485 9.271s-16.299-3.089-22.485-9.271l-173.781-173.129z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["double-up-arrow"]},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":868,"id":140,"name":"double-up-arrow","prevSize":32,"code":59813},"setIdx":0,"setId":1,"iconIdx":1},{"icon":{"paths":["M350.783 832.073c-78.001 0-144.476-26.79-199.424-80.371-54.947-53.577-82.421-119.228-82.421-196.962 0-74.176 26.229-137.272 78.688-189.29s116.239-81.063 191.339-87.136l-55.627-56.949c-6.187-6.621-9.28-14.112-9.28-22.475 0-8.37 3.061-15.616 9.184-21.739 6.677-6.677 14.489-9.949 23.435-9.813s16.37 3.157 22.272 9.067l106.017 106.005c7.706 7.715 11.563 16.715 11.563 26.997s-3.857 19.282-11.563 26.997l-105.356 105.354c-6.62 6.613-14.222 9.92-22.805 9.92-8.59 0-16.224-3.307-22.901-9.92-6.123-6.618-9.116-14.293-8.981-23.018s3.157-16.043 9.067-21.952l53.173-53.163c-56.782 6.67-105.017 29.629-144.704 68.875-39.68 39.255-59.52 86.666-59.52 142.239 0 59.571 21.344 110.016 64.032 151.339 42.695 41.331 94.101 61.995 154.219 61.995h75.487c9.067 0 16.666 3.068 22.797 9.207 6.135 6.135 9.203 13.739 9.203 22.805 0 9.071-3.068 16.67-9.203 22.793-6.131 6.131-13.73 9.195-22.797 9.195h-75.892zM604.096 465.301c-11.123 0-20.361-3.695-27.712-11.085-7.36-7.386-11.042-16.546-11.042-27.477v-195.935c0-11.107 3.695-20.338 11.085-27.691 7.394-7.36 16.555-11.040 27.477-11.040h274.688c11.119 0 20.356 3.698 27.712 11.093 7.36 7.389 11.038 16.547 11.038 27.477v195.936c0 11.099-3.695 20.332-11.093 27.692-7.386 7.351-16.546 11.029-27.477 11.029h-274.675zM604.096 832.073c-11.123 0-20.361-3.695-27.712-11.093-7.36-7.386-11.042-16.546-11.042-27.477v-195.934c0-11.102 3.695-20.331 11.085-27.691 7.394-7.356 16.555-11.029 27.477-11.029h274.688c11.119 0 20.356 3.695 27.712 11.081 7.36 7.39 11.038 16.55 11.038 27.477v195.938c0 11.106-3.695 20.335-11.093 27.691-7.386 7.36-16.546 11.038-27.477 11.038h-274.675zM629.342 768.073h224v-145.237h-224v145.237z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["move-up"]},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":867,"id":139,"name":"move-up","prevSize":32,"code":59814},"setIdx":0,"setId":1,"iconIdx":2},{"icon":{"paths":["M26.961 130.157c14.291-15.673 37.964-17.691 54.64-5.32l3.24 2.64 839.241 765.48c13.824 15.462 13.992 39.215-0.283 54.878-14.287 15.663-37.966 17.678-54.637 5.321l-3.24-2.638c-98.587-89.883-188.695-172.073-286.962-261.64v117.637c0 10.49-3.543 19.309-10.641 26.403-7.090 7.098-15.909 10.637-26.399 10.637h-357.6c-8.702 0-15.996-2.949-21.88-8.839-5.888-5.886-8.836-13.181-8.84-21.881 0-8.704 2.954-16.003 8.84-21.877 5.884-5.886 13.176-8.843 21.88-8.843h51.2v-406.399l-205.88-187.68-2.96-3c-13.836-15.462-13.994-39.212 0.28-54.88zM541.921 147.236c10.494 0 19.309 3.541 26.399 10.64 7.098 7.092 10.641 15.909 10.641 26.4v3.92h131.518c10.494 0 19.309 3.541 26.403 10.64 7.098 7.092 10.637 15.908 10.637 26.4v383.478l-61.44-56.439v-302.64h-107.119v204.319l-327.4-300.559c6.019-4.103 13.022-6.16 21-6.16h269.36zM446.48 495.395c-20.353 0-36.868 16.49-36.88 36.844 0 20.357 16.523 36.876 36.88 36.876 20.353-0.008 36.839-16.523 36.839-36.876-0.008-20.349-16.49-36.835-36.839-36.844z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"width":983,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["close-room"]},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":866,"id":138,"name":"close-room","prevSize":32,"code":59815},"setIdx":0,"setId":1,"iconIdx":3},{"icon":{"paths":["M853.070 526.139h-86.651c-8.704 0-15.999-2.945-21.885-8.835-5.89-5.89-8.835-13.189-8.835-21.893 0-8.712 2.945-16.007 8.835-21.885 5.886-5.882 13.181-8.827 21.885-8.827h86.651c8.704 0 15.995 2.949 21.881 8.839 5.89 5.89 8.839 13.189 8.839 21.893 0 8.712-2.949 16.003-8.839 21.881-5.886 5.886-13.177 8.827-21.881 8.827zM682.684 688.484c5.251-7.193 11.96-11.346 20.132-12.452 8.163-1.098 15.843 0.979 23.040 6.226l69.079 51.909c7.197 5.255 11.342 11.969 12.444 20.132 1.106 8.163-0.971 15.843-6.226 23.040-5.251 7.188-11.96 11.334-20.124 12.44s-15.843-0.967-23.040-6.222l-69.079-51.909c-7.197-5.251-11.342-11.956-12.44-20.124-1.106-8.163 0.963-15.843 6.214-23.040zM793.358 255.099l-69.079 51.907c-7.197 5.25-14.877 7.325-23.040 6.226-8.163-1.106-14.877-5.256-20.132-12.452-5.251-7.188-7.32-14.868-6.214-23.040 1.098-8.165 5.243-14.872 12.44-20.122l69.079-51.907c7.197-5.25 14.877-7.325 23.040-6.226 8.163 1.106 14.877 5.253 20.132 12.442 5.251 7.195 7.324 14.875 6.218 23.040-1.102 8.172-5.247 14.882-12.444 20.132zM215.034 600.973h-41.748c-20.364 0-37.796-7.25-52.296-21.75s-21.75-31.928-21.75-52.294v-63.017c0-20.365 7.25-37.798 21.75-52.298 14.5-14.498 31.932-21.748 52.296-21.748h158.331l127.365-76.165c12.341-7.407 24.789-7.448 37.335-0.123 12.554 7.318 18.833 17.992 18.833 32.020v299.643c0 14.029-6.279 24.703-18.833 32.018-12.546 7.328-24.994 7.287-37.335-0.123l-127.365-76.165h-55.142v129.18c0 8.704-2.945 15.999-8.837 21.881-5.891 5.894-13.189 8.839-21.893 8.839-8.711 0-16.005-2.945-21.883-8.839-5.885-5.882-8.827-13.177-8.827-21.881v-129.18zM571.859 623.186v-255.529c16.069 14.548 29.012 32.908 38.83 55.079 9.822 22.176 14.733 46.432 14.733 72.778 0 26.35-4.911 50.582-14.733 72.692-9.818 22.106-22.761 40.432-38.83 54.981z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"width":983,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["announcement"]},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":865,"id":137,"name":"announcement","prevSize":32,"code":59816},"setIdx":0,"setId":1,"iconIdx":4},{"icon":{"paths":["M429.294 884.478c-9.191 0-17.146-3.084-23.867-9.257-6.722-6.169-10.765-13.849-12.13-23.040l-11.894-92.471c-10.975-3.678-22.226-8.823-33.753-15.442-11.527-6.615-21.832-13.705-30.917-21.266l-85.543 36.078c-8.77 3.83-17.474 4.276-26.112 1.335s-15.426-8.454-20.362-16.54l-61.596-107.282c-4.674-8.086-6.118-16.724-4.332-25.915 1.785-9.187 6.38-16.593 13.784-22.213l74.043-56.005c-0.945-6.091-1.615-12.21-2.009-18.354s-0.591-12.259-0.591-18.35c0-5.833 0.197-11.751 0.591-17.764s1.064-12.591 2.009-19.73l-74.043-56.007c-7.404-5.618-12.064-13.023-13.98-22.213s-0.408-17.827 4.529-25.915l61.596-106.494c4.936-8.087 11.658-13.601 20.166-16.542s17.145-2.494 25.914 1.339l85.543 35.682c9.873-7.825 20.415-14.98 31.626-21.465s22.227-11.698 33.044-15.636l12.288-92.474c1.365-9.19 5.409-16.87 12.13-23.040s14.676-9.256 23.867-9.256h124.060c9.187 0 17.211 3.086 24.064 9.256s10.961 13.85 12.325 23.040l11.895 92.869c12.288 4.463 23.409 9.675 33.362 15.635 9.949 5.96 19.993 12.984 30.126 21.072l87.122-35.682c8.77-3.834 17.408-4.346 25.911-1.536 8.507 2.809 15.233 8.257 20.169 16.344l61.596 106.888c4.936 8.087 6.443 16.725 4.526 25.915-1.913 9.19-6.574 16.594-13.98 22.213l-75.616 57.187c1.47 6.615 2.269 12.8 2.4 18.551s0.197 11.538 0.197 17.367c0 5.566-0.131 11.227-0.393 16.978-0.262 5.747-1.208 12.325-2.834 19.73l74.83 56.398c7.406 5.62 12.063 13.025 13.98 22.213 1.917 9.191 0.41 17.83-4.526 25.915l-61.6 106.652c-4.936 8.086-11.825 13.64-20.677 16.658-8.847 3.019-17.658 2.613-26.427-1.221l-84.677-36.073c-10.134 8.086-20.48 15.241-31.035 21.463s-21.373 11.305-32.453 15.241l-11.895 92.869c-1.364 9.191-5.472 16.871-12.325 23.040-6.853 6.173-14.877 9.257-24.064 9.257h-124.060zM450.56 823.038h80.503l14.729-109.724c20.898-5.464 40.002-13.222 57.303-23.278 17.306-10.056 33.989-22.987 50.057-38.793l101.773 42.77 40.329-69.632-88.855-66.953c3.416-10.609 5.738-21.004 6.971-31.191 1.237-10.187 1.851-20.48 1.851-30.88 0-10.658-0.614-20.951-1.851-30.876-1.233-9.925-3.555-20.062-6.971-30.405l89.641-67.743-40.329-69.632-102.953 43.402c-13.705-14.651-30.126-27.596-49.271-38.833-19.141-11.238-38.502-19.194-58.089-23.867l-12.919-109.726h-81.289l-13.55 109.331c-20.898 4.936-40.198 12.498-57.895 22.686s-34.58 23.316-50.648 39.385l-101.77-42.378-40.33 69.632 88.458 65.932c-3.414 9.712-5.803 19.821-7.168 30.323s-2.048 21.557-2.048 33.165c0 10.658 0.682 21.107 2.048 31.347s3.623 20.349 6.774 30.327l-88.064 66.953 40.33 69.632 101.376-43.008c15.544 15.966 32.164 29.041 49.861 39.227s37.257 18.014 58.683 23.474l13.312 109.33zM491.991 618.238c34.083 0 63.082-11.96 86.999-35.877 23.921-23.921 35.881-52.92 35.881-87.003 0-34.079-11.96-63.078-35.881-86.999-23.917-23.919-52.916-35.879-86.999-35.879-34.501 0-63.603 11.96-87.314 35.879-23.709 23.921-35.564 52.92-35.564 86.999 0 34.083 11.855 63.082 35.564 87.003 23.711 23.917 52.813 35.877 87.314 35.877z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"width":983,"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["settings-outlined"]},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":864,"id":136,"name":"settings-outlined","prevSize":32,"code":59817},"setIdx":0,"setId":1,"iconIdx":5},{"icon":{"paths":["M690.876 334.375c21.547 0.001 39.778 7.451 54.707 22.375 14.933 14.933 22.417 33.196 22.417 54.75v407.376c0 21.551-7.484 39.774-22.417 54.707s-33.156 22.417-54.707 22.417h-400.417c-21.551 0-39.776-7.484-54.708-22.417s-22.415-33.156-22.417-54.707v-407.376c0-21.554 7.483-39.817 22.417-54.75 14.93-14.924 33.163-22.374 54.708-22.375h400.417zM439.834 688.043c-21.666 0-41.581 2.603-59.667 7.748-18.071 5.15-36.043 12.467-53.917 21.961-8.365 4.591-15.077 11.025-20.083 19.29-5.004 8.265-7.5 17.681-7.5 28.207v14.379c0.001 8.614 2.773 15.957 8.333 21.999 5.559 6.033 12.318 9.041 20.25 9.041h225.126c7.932 0 14.69-3.106 20.25-9.335 5.559-6.234 8.333-13.483 8.333-21.705v-14.379c0-10.526-2.496-19.942-7.501-28.207-5.001-8.26-11.682-14.699-20.041-19.29-17.877-9.498-35.878-16.811-53.956-21.961-18.078-5.146-37.965-7.748-59.627-7.748zM583.791 693.082c11.115 8.704 19.699 19.14 25.749 31.292 6.046 12.151 9.084 24.93 9.084 38.336v16.917c0 5.419-0.631 10.79-1.873 16.081-1.242 5.286-3.055 10.274-5.419 14.959h42.752c7.932 0 14.69-3.008 20.25-9.041 5.559-6.042 8.333-13.385 8.333-21.999v-16.917c-0.004-9.045-2.667-17.404-7.957-25.041-5.295-7.637-12.787-14.289-22.502-19.917-10.295-5.841-21.124-10.884-32.457-15.168-11.341-4.279-23.339-7.458-35.959-9.502zM439.834 512c-18.113 0-33.645 6.993-46.542 21.001-12.898 14.003-19.333 30.874-19.333 50.543 0.004 19.657 6.439 36.493 19.333 50.5 12.898 14.003 28.428 20.996 46.542 20.996 18.103-0.004 33.604-7.002 46.498-20.996 12.894-14.007 19.332-30.844 19.337-50.5 0-19.669-6.438-36.54-19.337-50.543-12.894-13.999-28.395-20.996-46.498-21.001zM528.333 512.457c-2.697 0.316-5.389 0.998-8.081 2.044 7.262 9.847 12.89 20.663 16.956 32.375 4.062 11.708 6.127 23.91 6.127 36.625s-2.018 24.981-5.999 36.791c-3.989 11.802-9.668 22.562-17.084 32.247 2.112 0.627 4.809 1.186 8.081 1.711 3.281 0.525 6.003 0.789 8.124 0.789 18.108 0 33.609-7.002 46.502-20.996 12.894-14.007 19.371-30.844 19.375-50.5 0-19.669-6.477-36.54-19.375-50.543-12.894-13.999-28.395-21.001-46.502-21.001-2.701 0-5.423 0.141-8.124 0.457zM701.542 209.625c7.334 0.001 13.402 2.417 18.206 7.208 4.804 4.796 7.206 10.871 7.211 18.208 0 7.339-2.402 13.436-7.211 18.25-4.804 4.808-10.867 7.207-18.206 7.209h-422.167c-7.343-0.001-13.402-2.41-18.208-7.209-4.798-4.8-7.208-10.864-7.208-18.208 0.002-7.334 2.411-13.396 7.208-18.208 4.806-4.812 10.866-7.249 18.208-7.25h422.167zM660.501 85.333c7.343 0 13.44 2.409 18.249 7.209 4.8 4.798 7.206 10.868 7.206 18.208 0 7.339-2.398 13.436-7.206 18.25-4.804 4.811-10.906 7.208-18.249 7.208h-340.084c-7.353 0-13.45-2.408-18.25-7.208-4.8-4.798-7.208-10.876-7.208-18.208 0-7.343 2.404-13.437 7.208-18.25 4.8-4.815 10.897-7.209 18.25-7.209h340.084z"],"attrs":[{"fill":"rgb(217, 217, 217)"}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["breakout-room"]},"attrs":[{"fill":"rgb(217, 217, 217)"}],"properties":{"order":801,"id":135,"name":"breakout-room","prevSize":32,"code":59818},"setIdx":0,"setId":1,"iconIdx":6},{"icon":{"paths":["M409.65 499.15c-36.966 0-68.25-12.97-93.85-38.912-25.6-25.975-38.4-57.088-38.4-93.338 0-37.001 12.8-68.301 38.4-93.901s56.883-38.4 93.85-38.4c36.966 0 68.25 12.8 93.85 38.4 25.602 25.6 38.402 56.9 38.402 93.901 0 36.25-12.8 67.362-38.402 93.338-25.6 25.942-56.883 38.912-93.85 38.912zM123.8 789.299v-73.626c0-16.348 3.908-30.566 11.725-42.65s18.842-22.39 33.075-30.925c37.683-21.33 76.083-37.699 115.2-49.101 39.117-11.366 81.066-17.050 125.85-17.050 7.1 0 13.67 0.174 19.712 0.512 6.042 0.379 12.63 0.922 19.763 1.638-3.55 9.216-6.042 18.452-7.475 27.699-1.434 9.252-2.509 18.145-3.226 26.675l-28.774-1.075c-36.966 0-72.874 4.27-107.725 12.8-34.85 8.535-69.7 23.47-104.55 44.8-6.383 3.553-11.008 7.475-13.875 11.776-2.833 4.234-4.25 9.201-4.25 14.899v18.125h267.725c2.15 9.252 5.171 18.673 9.062 28.262 3.926 9.626 8.38 18.708 13.363 27.238h-345.6zM409.65 443.701c21.334 0 39.458-7.646 54.374-22.938 14.95-15.292 22.426-33.604 22.426-54.938 0-20.617-7.475-38.4-22.426-53.35-14.916-14.916-33.041-22.374-54.374-22.374s-39.458 7.458-54.374 22.374c-14.95 14.95-22.426 33.092-22.426 54.426s7.475 39.458 22.426 54.374c14.916 14.95 33.041 22.426 54.374 22.426z","M590.264 819.2l33.597-132.050-111.862-89.17 146.724-11.986 58.076-125.194 58.076 125.768 146.724 11.412-111.862 89.17 33.597 132.050-126.536-69.745-126.536 69.745z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["spotlight"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":857,"id":0,"name":"spotlight","prevSize":32,"code":59811},"setIdx":0,"setId":1,"iconIdx":7},{"icon":{"paths":["M386.464 746.65c-21.554 0-39.798-7.465-54.731-22.4s-22.4-33.178-22.4-54.728v-485.741c0-21.554 7.466-39.798 22.4-54.731s33.177-22.4 54.731-22.4h357.738c21.555 0 39.798 7.466 54.733 22.4 14.93 14.933 22.4 33.177 22.4 54.731v485.741c0 21.55-7.47 39.793-22.4 54.728-14.935 14.935-33.178 22.4-54.733 22.4h-357.738zM386.464 682.65h357.738c3.287 0 6.292-1.367 9.027-4.106 2.734-2.729 4.106-5.74 4.106-9.021v-485.741c0-3.286-1.372-6.294-4.106-9.024-2.734-2.738-5.74-4.107-9.027-4.107h-357.738c-3.286 0-6.294 1.369-9.024 4.107-2.738 2.73-4.107 5.738-4.107 9.024v485.741c0 3.282 1.369 6.292 4.107 9.021 2.73 2.739 5.738 4.106 9.024 4.106zM237.131 895.985c-21.554 0-39.797-7.47-54.731-22.4-14.934-14.935-22.4-33.178-22.4-54.733v-549.738h64v549.738c0 3.287 1.369 6.292 4.107 9.027 2.73 2.734 5.738 4.106 9.024 4.106h421.736v64h-421.736z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["clipboard_outlined"],"grid":0},"attrs":[{}],"properties":{"order":791,"id":1,"name":"clipboard_outlined","prevSize":32,"code":59810},"setIdx":0,"setId":1,"iconIdx":8},{"icon":{"paths":["M487.97 882.235c-50.869 0-98.778-9.621-143.725-28.872-44.948-19.246-84.29-45.729-118.026-79.448-33.736-33.724-60.233-73.031-79.489-117.931s-28.884-92.872-28.884-143.921c0-51.051 9.625-98.88 28.876-143.489s45.74-83.782 79.466-117.518c33.727-33.736 72.995-60.233 117.803-79.489s92.685-28.884 143.633-28.884c24.015 0 47.991 2.136 71.929 6.408s46.899 11.213 68.886 20.821v58.025c-22.508-10.732-45.446-18.82-68.813-24.264s-47.367-8.166-72.002-8.166c-87.824 0-162.605 30.893-224.344 92.678s-92.61 136.621-92.61 224.508c0 87.888 30.893 162.645 92.678 224.276s136.621 92.443 224.51 92.443c87.884 0 162.64-30.871 224.271-92.609s92.448-136.519 92.448-224.344c0-24.869-2.789-48.994-8.363-72.376s-13.858-46.194-24.849-68.438h59.387c9.221 22.661 15.965 45.76 20.241 69.297 4.272 23.537 6.407 47.376 6.407 71.518 0 50.946-9.626 98.826-28.872 143.633s-45.734 84.075-79.453 117.804c-33.719 33.724-72.953 60.216-117.692 79.462-44.739 19.251-92.545 28.877-143.414 28.877zM790.591 209.728h-46.85c-7.519 0-13.819-2.532-18.905-7.597-5.081-5.065-7.626-11.341-7.626-18.828s2.545-13.759 7.626-18.815c5.086-5.056 11.386-7.583 18.905-7.583h46.85v-46.731c0-7.483 2.54-13.756 7.631-18.818 5.086-5.062 11.342-7.594 18.768-7.594s13.648 2.531 18.666 7.594c5.013 5.062 7.519 11.335 7.519 18.818v46.731h46.519c7.422 0 13.717 2.512 18.881 7.536s7.743 11.249 7.743 18.676c0 7.426-2.531 13.679-7.592 18.758s-11.337 7.617-18.817 7.617h-46.733v46.849c0 7.518-2.531 13.819-7.597 18.904s-11.342 7.627-18.827 7.627c-7.285 0-13.468-2.531-18.544-7.594-5.081-5.062-7.617-11.335-7.617-18.818v-46.731zM617.901 468.073c14.785 0 27.326-5.175 37.615-15.526s15.433-22.918 15.433-37.704c0-14.786-5.179-27.323-15.526-37.612-10.352-10.289-22.918-15.433-37.708-15.433-14.785 0-27.321 5.175-37.61 15.526s-15.433 22.918-15.433 37.704c0 14.786 5.174 27.323 15.526 37.612 10.347 10.289 22.918 15.433 37.703 15.433zM357.526 468.073c14.786 0 27.323-5.175 37.612-15.526s15.435-22.918 15.435-37.704c0-14.786-5.176-27.323-15.526-37.612s-22.919-15.433-37.704-15.433c-14.786 0-27.323 5.175-37.612 15.526s-15.433 22.918-15.433 37.704c0 14.786 5.175 27.323 15.525 37.612s22.919 15.433 37.704 15.433zM487.172 695.003c32.030 0 61.931-6.773 89.691-20.319 27.765-13.541 51.546-32.402 71.344-56.574 3.959-6.042 4.403-12.127 1.331-18.247-3.077-6.12-7.948-9.182-14.614-9.182h-294.602c-6.668 0-11.54 3.062-14.613 9.182s-2.687 12.205 1.159 18.247c19.233 24.171 42.888 43.032 70.967 56.574 28.079 13.546 57.858 20.319 89.338 20.319z"],"width":975,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["add_reaction"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":856,"id":2,"name":"add_reaction","prevSize":32,"code":59809},"setIdx":0,"setId":1,"iconIdx":9},{"icon":{"paths":["M261.968 771.872h104.086c7.483 0 13.758 2.516 18.824 7.539 5.059 5.027 7.589 11.249 7.589 18.671 0 7.431-2.53 13.722-7.589 18.876-5.066 5.159-11.341 7.743-18.824 7.743h-124.241c-9.258 0-17.016-3.135-23.274-9.396-6.264-6.261-9.397-14.019-9.397-23.274v-124.245c0-7.48 2.513-13.756 7.538-18.822 5.025-5.057 11.249-7.587 18.671-7.587 7.429 0 13.721 2.531 18.875 7.587 5.16 5.066 7.741 11.342 7.741 18.822v104.087zM798.735 771.872v-104.087c0-7.48 2.511-13.756 7.539-18.822 5.027-5.057 11.254-7.587 18.681-7.587 7.422 0 13.717 2.531 18.876 7.587 5.154 5.066 7.729 11.342 7.729 18.822v124.245c0 9.255-3.126 17.013-9.387 23.274s-14.019 9.396-23.269 9.396h-124.255c-7.48 0-13.756-2.516-18.812-7.539-5.066-5.027-7.597-11.249-7.597-18.671 0-7.431 2.531-13.722 7.597-18.876 5.057-5.164 11.332-7.743 18.812-7.743h104.087zM261.968 235.492v104.086c0 7.483-2.513 13.758-7.538 18.824-5.025 5.059-11.249 7.589-18.671 7.589-7.43 0-13.721-2.53-18.875-7.589-5.16-5.066-7.741-11.341-7.741-18.824v-124.241c0-9.258 3.132-17.015 9.397-23.274 6.258-6.264 14.016-9.397 23.274-9.397h124.241c7.483 0 13.758 2.513 18.824 7.538 5.059 5.025 7.589 11.249 7.589 18.671 0 7.429-2.53 13.721-7.589 18.875-5.066 5.16-11.341 7.741-18.824 7.741h-104.086zM798.735 235.492h-104.087c-7.48 0-13.756-2.513-18.812-7.538-5.066-5.025-7.597-11.249-7.597-18.671 0-7.429 2.531-13.721 7.597-18.875 5.057-5.16 11.332-7.741 18.812-7.741h124.255c9.25 0 17.008 3.132 23.269 9.397 6.261 6.258 9.387 14.016 9.387 23.274v124.241c0 7.483-2.506 13.758-7.524 18.824-5.027 5.059-11.254 7.589-18.686 7.589-7.426 0-13.717-2.53-18.871-7.589-5.164-5.066-7.743-11.341-7.743-18.824v-104.086z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["fullscreen"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":855,"id":3,"name":"fullscreen","prevSize":32,"code":59808},"setIdx":0,"setId":1,"iconIdx":10},{"icon":{"paths":["M952.264 512c0-226.216-183.383-409.6-409.6-409.6s-409.601 183.384-409.601 409.6c0 226.217 183.384 409.6 409.601 409.6s409.6-183.383 409.6-409.6zM911.304 512c0 203.592-165.048 368.64-368.64 368.64-203.595 0-368.641-165.048-368.641-368.64 0-203.594 165.046-368.64 368.641-368.64 203.592 0 368.64 165.046 368.64 368.64z","M696.264 512c0 84.831-68.769 153.6-153.6 153.6s-153.6-68.769-153.6-153.6c0-84.831 68.769-153.6 153.6-153.6s153.6 68.769 153.6 153.6z"],"width":1075,"attrs":[{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["recording-status"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"}],"properties":{"order":5,"id":4,"name":"recording-status","prevSize":32,"code":59807},"setIdx":0,"setId":1,"iconIdx":11},{"icon":{"paths":["M308.847 697.119c-51.214 0-94.87-18.068-130.966-54.211-36.096-36.137-54.144-79.841-54.144-131.112 0-51.278 18.048-94.912 54.144-130.902 36.096-35.997 79.751-53.994 130.966-53.994h116.021c7.858 0 14.442 2.638 19.754 7.914 5.319 5.276 7.978 11.815 7.978 19.616 0 7.794-2.659 14.4-7.978 19.819-5.312 5.411-11.897 8.117-19.754 8.117h-116.075c-35.797 0-66.343 12.65-91.637 37.952-25.302 25.301-37.952 55.865-37.952 91.692 0 35.825 12.65 66.386 37.952 91.689 25.294 25.303 55.84 37.955 91.637 37.955h116.075c7.858 0 14.442 2.637 19.754 7.91 5.319 5.274 7.978 11.807 7.978 19.61 0 7.798-2.659 14.408-7.978 19.814-5.312 5.422-11.897 8.131-19.754 8.131h-116.021zM397.103 539.74c-7.843 0-14.474-2.637-19.893-7.91-5.426-5.279-8.138-11.812-8.138-19.61s2.652-14.405 7.957-19.816c5.312-5.419 11.89-8.128 19.734-8.128h230.141c7.844 0 14.479 2.638 19.896 7.914 5.427 5.276 8.136 11.811 8.136 19.606 0 7.802-2.652 14.407-7.956 19.818-5.309 5.417-11.889 8.125-19.732 8.125h-230.143zM599.142 697.119c-7.859 0-14.444-2.637-19.758-7.916-5.32-5.274-7.977-11.812-7.977-19.615 0-7.793 2.657-14.403 7.977-19.82 5.315-5.412 11.899-8.115 19.758-8.115h116.070c35.799 0 66.345-12.652 91.638-37.955 25.303-25.303 37.955-55.864 37.955-91.689 0-35.827-12.652-66.391-37.955-91.692-25.293-25.302-55.839-37.952-91.638-37.952h-116.070c-7.859 0-14.444-2.638-19.758-7.915-5.32-5.269-7.977-11.804-7.977-19.605s2.657-14.407 7.977-19.818c5.315-5.419 11.899-8.128 19.758-8.128h116.019c51.215 0 94.868 18.069 130.964 54.208s54.144 79.843 54.144 131.112c0 51.282-18.048 94.915-54.144 130.903-36.096 35.999-79.749 53.996-130.964 53.996h-116.019z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["copy-link"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":854,"id":5,"name":"copy-link","prevSize":32,"code":59806},"setIdx":0,"setId":1,"iconIdx":12},{"icon":{"paths":["M460.553 655.586l185.847-119.381c9.135-5.803 13.7-13.854 13.7-24.166 0-10.308-4.565-18.389-13.7-24.243l-185.847-119.384c-9.57-6.564-19.392-7.090-29.457-1.577-10.063 5.513-15.095 14.106-15.095 25.781v238.766c0 11.678 5.032 20.271 15.095 25.783 10.065 5.513 19.887 4.988 29.457-1.579zM512.068 917.333c-56.060 0-108.754-10.641-158.082-31.915-49.329-21.278-92.238-50.155-128.727-86.626s-65.378-79.364-86.663-128.67c-21.286-49.306-31.929-101.99-31.929-158.050 0-56.064 10.638-108.758 31.915-158.086s50.151-92.238 86.624-128.727c36.474-36.49 79.364-65.378 128.671-86.663 49.306-21.286 101.991-31.929 158.051-31.929s108.757 10.638 158.084 31.915c49.331 21.277 92.237 50.151 128.73 86.624 36.489 36.474 65.374 79.364 86.66 128.671 21.286 49.306 31.932 101.991 31.932 158.051s-10.641 108.757-31.915 158.084c-21.278 49.331-50.155 92.237-86.626 128.73-36.471 36.489-79.364 65.374-128.67 86.66s-101.99 31.932-158.054 31.932z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["play-circle"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":853,"id":6,"name":"play-circle","prevSize":32,"code":59805},"setIdx":0,"setId":1,"iconIdx":13},{"icon":{"paths":["M268.011 490.335l116.705 116.751c5.364 5.364 8.176 11.591 8.437 18.676 0.26 7.080-2.422 13.6-8.046 19.549-5.624 5.564-11.913 8.28-18.865 8.148-6.952-0.127-13.24-3.009-18.864-8.631l-158.183-158.244c-6.666-6.421-9.999-13.914-9.999-22.476s3.333-16.178 9.999-22.846l158.183-158.242c5.624-5.627 11.968-8.505 19.033-8.635s13.297 2.748 18.695 8.635c5.624 5.4 8.436 11.569 8.436 18.506s-2.812 13.219-8.436 18.846l-117.096 117.139h355.503c51.078 0 94.652 18.040 130.716 54.121 36.069 36.079 54.101 79.667 54.101 130.765v99.557c0 7.495-2.506 13.77-7.519 18.827s-11.235 7.587-18.666 7.587c-7.436 0-13.731-2.531-18.885-7.587s-7.734-11.332-7.734-18.827v-99.557c0-36.464-12.888-67.594-38.668-93.379-25.776-25.79-56.891-38.683-93.345-38.683h-355.503z"],"width":975,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["reply"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":863,"id":7,"name":"reply","prevSize":32,"code":59801},"setIdx":0,"setId":1,"iconIdx":14},{"icon":{"paths":["M182.013 420.216l273.206 273.307c10.24 10.242 15.609 22.127 16.107 35.654 0.496 13.525-4.624 25.968-15.36 37.328-10.738 10.62-22.743 15.805-36.015 15.555s-25.276-5.743-36.013-16.485l-301.578-301.688c-6.236-6.24-10.846-12.936-13.829-20.086s-4.475-14.882-4.475-23.197c0-8.315 1.492-16.047 4.475-23.197s7.457-13.969 13.421-20.453l301.985-302.097c10.737-10.742 22.849-16.237 36.337-16.485 13.488-0.249 25.385 5.245 35.691 16.485 10.736 10.31 16.105 22.086 16.105 35.329s-5.369 25.237-16.105 35.978l-273.953 274.052zM570.341 470.639l222.801 222.885c10.24 10.242 15.609 22.127 16.107 35.654 0.497 13.525-4.623 25.968-15.361 37.328-10.736 10.62-22.74 15.805-36.012 15.555s-25.277-5.743-36.015-16.485l-301.576-301.688c-6.237-6.24-10.847-12.936-13.831-20.086-2.982-7.15-4.472-14.882-4.472-23.197s1.49-16.047 4.472-23.197c2.984-7.151 7.457-13.969 13.422-20.453l301.985-302.097c10.738-10.742 22.85-16.237 36.336-16.485 13.489-0.249 25.385 5.245 35.691 16.485 10.738 10.31 16.107 22.086 16.107 35.329s-5.369 25.237-16.107 35.978l-223.547 223.629h433.821c97.513 0 180.699 34.441 249.549 103.322 68.859 68.881 103.284 152.097 103.284 249.647v190.059c0 14.311-4.785 26.29-14.355 35.943s-21.448 14.485-35.635 14.485c-14.196 0-26.214-4.831-36.054-14.485s-14.764-21.632-14.764-35.943v-190.059c0-69.619-24.604-129.044-73.812-178.277-49.217-49.231-108.618-73.846-178.213-73.846h-433.821z"],"width":1396,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["reply_all"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":862,"id":8,"name":"reply_all","prevSize":32,"code":59802},"setIdx":0,"setId":1,"iconIdx":15},{"icon":{"paths":["M327.825 795.243c-17.959 0-33.333-6.393-46.121-19.188s-19.184-28.175-19.184-46.139v-475.115h-22.341c-7.481 0-13.751-2.512-18.812-7.536s-7.591-11.249-7.591-18.675c0-7.427 2.53-13.719 7.591-18.876s11.331-7.737 18.812-7.737h148.262v-12.19c0-8.799 3.112-16.312 9.338-22.54s13.736-9.342 22.532-9.342h131.234c8.797 0 16.306 3.114 22.533 9.342s9.338 13.742 9.338 22.54v12.19h148.261c7.48 0 13.751 2.512 18.812 7.537 5.061 5.023 7.592 11.249 7.592 18.676s-2.531 13.719-7.592 18.876c-5.061 5.158-11.332 7.736-18.812 7.736h-22.343v474.671c0 18.466-6.393 34.041-19.183 46.733-12.785 12.693-28.16 19.037-46.119 19.037h-316.207zM656.53 254.802h-341.205v475.115c0 3.647 1.172 6.641 3.516 8.987s5.338 3.516 8.984 3.516h316.207c3.647 0 6.641-1.17 8.982-3.516 2.345-2.345 3.516-5.339 3.516-8.987v-475.115zM431.048 669.277c7.424 0 13.713-2.531 18.869-7.592s7.734-11.337 7.734-18.817v-288.511c0-7.483-2.511-13.755-7.534-18.818s-11.245-7.594-18.669-7.594c-7.424 0-13.714 2.531-18.869 7.594s-7.733 11.335-7.733 18.818v288.511c0 7.48 2.511 13.756 7.533 18.817s11.245 7.592 18.669 7.592zM540.409 669.277c7.426 0 13.712-2.531 18.871-7.592 5.154-5.061 7.734-11.337 7.734-18.817v-288.511c0-7.483-2.511-13.755-7.534-18.818s-11.244-7.594-18.671-7.594c-7.422 0-13.712 2.531-18.866 7.594-5.159 5.062-7.739 11.335-7.739 18.818v288.511c0 7.48 2.511 13.756 7.534 18.817s11.249 7.592 18.671 7.592z"],"width":975,"attrs":[{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["delete"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"}],"properties":{"order":10,"id":9,"name":"delete","prevSize":32,"code":59803},"setIdx":0,"setId":1,"iconIdx":16},{"icon":{"paths":["M781.073 862.779l-103.97-104.009h-461.817v-70.012c0-14.336 3.711-27.648 11.132-39.936 7.421-12.293 17.797-22.318 31.128-30.081 31.715-18.388 64.862-32.875 99.441-43.447 34.579-10.576 69.913-16.647 106.003-18.208h6.445c2.213 0 4.361 0.127 6.444 0.39l-356.752-356.885 37.338-37.352 661.944 662.189-37.337 37.352zM268.091 705.946h356.208l-94.52-95.568c-7.29-0.522-14.351-0.975-21.172-1.37-6.822-0.39-13.878-0.585-21.168-0.585-35.777 0-70.811 4.613-105.104 13.834s-66.567 22.791-96.824 40.711c-5.434 3.033-9.696 6.802-12.785 11.298s-4.635 9.328-4.635 14.492v17.189zM726.938 623.036c5.988 3.803 10.83 8.426 14.531 13.873 3.696 5.442 6.090 11.654 7.183 18.632l-48.191-48.206c4.54 2.438 9.016 4.886 13.429 7.329s8.763 5.232 13.049 8.372zM557.115 460.184l-39.6-38.602c13.044-5.996 23.479-14.967 31.305-26.916 7.821-11.949 11.732-25.006 11.732-39.17 0-20.073-7.119-37.22-21.363-51.443s-31.364-21.333-51.366-21.333c-14.109 0-27.149 3.913-39.119 11.739s-20.957 18.265-26.961 31.317l-38.588-39.618c11.457-18.129 26.52-32.039 45.189-41.728s38.367-14.535 59.094-14.535c35.153 0 64.927 12.203 89.323 36.61 24.4 24.407 36.596 54.193 36.596 89.357 0 20.734-4.842 40.439-14.526 59.115-9.689 18.676-23.591 33.745-41.716 45.206z"],"width":975,"attrs":[{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["block_user"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"}],"properties":{"order":11,"id":10,"name":"block_user","prevSize":32,"code":59804},"setIdx":0,"setId":1,"iconIdx":17},{"icon":{"paths":["M798.848 547.797l-595.526 251.072c-12.854 5.146-25.065 4.036-36.634-3.319-11.57-7.36-17.355-18.039-17.355-32.043v-502.97c0-14.004 5.785-24.684 17.355-32.041s23.781-8.465 36.634-3.323l595.526 251.077c15.863 7.002 23.791 18.927 23.791 35.772 0 16.849-7.927 28.774-23.791 35.776zM213.332 725.355l505.601-213.333-505.601-213.332v157.537l231.383 55.795-231.383 55.795v157.538z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_send"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":860,"id":11,"name":"chat_send","prevSize":32,"code":59768},"setIdx":0,"setId":1,"iconIdx":18},{"icon":{"paths":["M203.375 798.861c-12.889 5.15-25.118 4.053-36.687-3.285s-17.355-18.027-17.355-32.068v-180.1l295.381-71.386-295.381-71.381v-180.102c0-14.040 5.785-24.73 17.355-32.068s23.798-8.434 36.687-3.284l595.473 251.064c15.863 7.104 23.791 19.055 23.791 35.853 0 16.794-7.927 28.693-23.791 35.695l-595.473 251.063z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_send_fill"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":859,"id":12,"name":"chat_send_fill","prevSize":32,"code":59771},"setIdx":0,"setId":1,"iconIdx":19},{"icon":{"paths":["M657.327 461.129c15.522 0 28.689-5.436 39.492-16.303s16.205-24.065 16.205-39.59c0-15.525-5.436-28.689-16.303-39.492s-24.064-16.205-39.586-16.205c-15.526 0-28.689 5.434-39.492 16.302-10.807 10.867-16.209 24.064-16.209 39.589s5.436 28.691 16.303 39.494c10.867 10.803 24.064 16.205 39.59 16.205zM366.865 461.129c15.525 0 28.689-5.436 39.492-16.303s16.206-24.065 16.206-39.59c0-15.525-5.434-28.689-16.302-39.492s-24.064-16.205-39.589-16.205c-15.525 0-28.689 5.434-39.494 16.302s-16.205 24.064-16.205 39.589c0 15.525 5.434 28.691 16.302 39.494s24.064 16.205 39.589 16.205zM512.068 917.333c-56.060 0-108.754-10.641-158.082-31.915-49.329-21.278-92.238-50.155-128.727-86.626s-65.378-79.364-86.663-128.67c-21.286-49.306-31.929-101.99-31.929-158.050 0-56.064 10.638-108.758 31.915-158.086s50.151-92.238 86.624-128.727c36.474-36.49 79.364-65.378 128.671-86.663 49.306-21.286 101.991-31.929 158.051-31.929s108.757 10.638 158.084 31.915c49.331 21.277 92.237 50.151 128.73 86.624 36.489 36.474 65.374 79.364 86.66 128.671 21.286 49.306 31.932 101.991 31.932 158.051s-10.641 108.757-31.915 158.084c-21.278 49.331-50.155 92.237-86.626 128.73-36.471 36.489-79.364 65.374-128.67 86.66s-101.99 31.932-158.054 31.932zM512 853.333c95.287 0 176-33.067 242.133-99.2s99.2-146.846 99.2-242.133c0-95.29-33.067-176.001-99.2-242.134s-146.846-99.2-242.133-99.2c-95.29 0-176.001 33.067-242.134 99.2s-99.2 146.844-99.2 242.134c0 95.287 33.067 176 99.2 242.133s146.844 99.2 242.134 99.2zM512 733.538c38.293 0 73.617-9.067 105.971-27.2s58.628-42.735 78.822-73.805c4.036-7.77 3.908-15.578-0.384-23.428-4.297-7.846-11.008-11.772-20.143-11.772h-328.533c-9.135 0-15.85 3.925-20.144 11.772-4.294 7.851-4.422 15.659-0.383 23.428 20.194 31.070 46.633 55.671 79.315 73.805 32.685 18.133 67.842 27.2 105.478 27.2z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_emoji"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":858,"id":13,"name":"chat_emoji","prevSize":32,"code":59772},"setIdx":0,"setId":1,"iconIdx":20},{"icon":{"paths":["M298.667 938.667c-70.692 0-128-57.306-128-128v-682.667c0-23.564 19.102-42.667 42.667-42.667h408.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v494.327c0 70.694-57.306 128-128 128h-426.667z","M597.333 298.667v-213.333h24.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v24.994h-213.333c-23.565 0-42.667-19.103-42.667-42.667z","M170.667 554.667h496.926c23.564 0 42.667 19.103 42.667 42.667v170.667c0 23.564-19.103 42.667-42.667 42.667h-496.926c-23.564 0-42.667-19.103-42.667-42.667v-170.667c0-23.564 19.103-42.667 42.667-42.667z","M226.979 752.235v-139.093h39.253c14.223 0 26.525 2.419 36.907 7.253 10.383 4.838 18.418 12.373 24.107 22.613 5.831 10.099 8.747 23.113 8.747 39.040 0 15.932-2.844 29.086-8.533 39.467-5.689 10.385-13.653 18.133-23.893 23.253-10.097 4.979-21.973 7.467-35.627 7.467h-40.96zM258.339 726.848h5.973c7.965 0 14.863-1.421 20.693-4.267 5.973-2.842 10.596-7.535 13.867-14.080 3.271-6.541 4.907-15.36 4.907-26.453s-1.635-19.767-4.907-26.027c-3.271-6.4-7.893-10.88-13.867-13.44-5.831-2.701-12.729-4.053-20.693-4.053h-5.973v88.32z","M418.929 754.795c-12.515 0-23.537-2.914-33.067-8.747-9.387-5.828-16.782-14.148-22.187-24.96-5.262-10.948-7.893-23.962-7.893-39.040 0-15.215 2.631-28.087 7.893-38.613 5.405-10.667 12.8-18.773 22.187-24.32 9.529-5.687 20.551-8.533 33.067-8.533s23.469 2.846 32.855 8.533c9.527 5.547 16.922 13.653 22.187 24.32 5.402 10.667 8.107 23.539 8.107 38.613 0 15.078-2.705 28.092-8.107 39.040-5.265 10.812-12.659 19.132-22.187 24.96-9.387 5.833-20.339 8.747-32.855 8.747zM418.929 727.701c9.53 0 17.069-4.122 22.615-12.373 5.547-8.247 8.32-19.341 8.32-33.28 0-13.935-2.773-24.815-8.32-32.64-5.547-7.821-13.086-11.733-22.615-11.733s-17.067 3.913-22.613 11.733c-5.547 7.825-8.32 18.705-8.32 32.64 0 13.939 2.773 25.033 8.32 33.28 5.547 8.252 13.085 12.373 22.613 12.373z","M566.255 754.795c-11.661 0-22.4-2.701-32.213-8.107-9.813-5.402-17.707-13.367-23.68-23.893-5.828-10.667-8.747-23.748-8.747-39.253 0-15.36 2.987-28.442 8.96-39.253 6.118-10.948 14.153-19.268 24.107-24.96 10.099-5.828 20.979-8.747 32.64-8.747 8.96 0 16.998 1.852 24.107 5.547 7.113 3.558 13.013 7.753 17.707 12.587l-16.64 20.053c-3.554-3.268-7.322-5.901-11.307-7.893-3.84-2.133-8.247-3.2-13.227-3.2-6.255 0-12.015 1.779-17.28 5.333-5.12 3.558-9.242 8.678-12.373 15.36-2.987 6.686-4.48 14.72-4.48 24.107 0 14.225 3.059 25.318 9.173 33.28 6.118 7.966 14.293 11.947 24.533 11.947 5.692 0 10.739-1.28 15.147-3.84 4.553-2.56 8.533-5.615 11.947-9.173l16.64 19.627c-11.661 13.653-26.667 20.48-45.013 20.48z"],"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(18, 127, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":true,"tags":["chat_attachment_doc"],"grid":0},"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(18, 127, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":15,"id":14,"name":"chat_attachment_doc","prevSize":32,"code":59773,"codes":[59773,59774,59775,59776,59777,59778]},"setIdx":0,"setId":1,"iconIdx":21},{"icon":{"paths":["M298.667 938.667c-70.692 0-128-57.306-128-128v-682.667c0-23.564 19.102-42.667 42.667-42.667h408.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v494.327c0 70.694-57.306 128-128 128h-426.667z","M597.333 298.667v-213.333h24.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v24.994h-213.333c-23.565 0-42.667-19.103-42.667-42.667z","M298.667 469.333h426.667c23.564 0 42.667 19.103 42.667 42.667v298.667c0 23.564-19.103 42.667-42.667 42.667h-426.667c-23.564 0-42.667-19.103-42.667-42.667v-298.667c0-23.564 19.103-42.667 42.667-42.667z","M608.119 755.81l43.482-43.234c16.64-16.546 43.52-16.546 60.164 0l141.568 140.757h-341.333l96.119-97.523z","M367.119 624.316l-239.119 217.378 576 54.306-278.439-270.703c-16.163-15.714-41.762-16.145-58.443-0.981z","M640 597.333c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667z"],"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(114, 205, 255)"},{"fill":"rgb(128, 183, 74)","stroke":"rgb(128, 183, 74)","strokeLinejoin":"round","strokeLinecap":"round","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(78, 147, 27)","stroke":"rgb(78, 147, 27)","strokeLinejoin":"round","strokeLinecap":"round","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(245, 215, 69)"}],"isMulticolor":true,"isMulticolor2":true,"tags":["chat_attachment_image"],"grid":0},"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(114, 205, 255)"},{"fill":"rgb(128, 183, 74)","stroke":"rgb(128, 183, 74)","strokeLinejoin":"round","strokeLinecap":"round","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(78, 147, 27)","stroke":"rgb(78, 147, 27)","strokeLinejoin":"round","strokeLinecap":"round","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(245, 215, 69)"}],"properties":{"order":16,"id":15,"name":"chat_attachment_image","prevSize":32,"code":59779,"codes":[59779,59780,59781,59782,59783,59784]},"setIdx":0,"setId":1,"iconIdx":22},{"icon":{"paths":["M298.667 938.667c-70.692 0-128-57.306-128-128v-682.667c0-23.564 19.102-42.667 42.667-42.667h408.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v494.327c0 70.694-57.306 128-128 128h-426.667z","M597.333 298.667v-213.333h24.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v24.994h-213.333c-23.565 0-42.667-19.103-42.667-42.667z","M170.667 576h496.926c11.782 0 21.333 9.551 21.333 21.333v170.667c0 11.782-9.551 21.333-21.333 21.333h-496.926c-11.782 0-21.333-9.551-21.333-21.333v-170.667c0-11.782 9.551-21.333 21.333-21.333z","M244.042 752.175v-139.093h47.787c10.24 0 19.484 1.425 27.733 4.267 8.391 2.705 15.075 7.326 20.053 13.867 4.977 6.545 7.467 15.433 7.467 26.667 0 10.812-2.489 19.699-7.467 26.667-4.978 6.972-11.591 12.16-19.84 15.573-8.249 3.273-17.28 4.907-27.093 4.907h-17.28v47.147h-31.36zM275.402 680.068h15.36c17.067 0 25.6-7.394 25.6-22.187 0-7.253-2.276-12.373-6.827-15.36s-11.093-4.48-19.627-4.48h-14.507v42.027z","M371.125 752.175v-139.093h39.253c14.223 0 26.524 2.419 36.905 7.253 10.385 4.838 18.419 12.373 24.107 22.613 5.833 10.099 8.747 23.113 8.747 39.040 0 15.932-2.842 29.086-8.533 39.467-5.687 10.385-13.653 18.133-23.893 23.253-10.095 4.979-21.972 7.467-35.625 7.467h-40.96zM402.485 726.788h5.973c7.965 0 14.863-1.421 20.692-4.267 5.973-2.842 10.598-7.535 13.867-14.080 3.273-6.541 4.907-15.36 4.907-26.453s-1.634-19.767-4.907-26.027c-3.268-6.4-7.893-10.88-13.867-13.44-5.829-2.701-12.727-4.053-20.692-4.053h-5.973v88.32z","M506.543 752.175v-139.093h87.68v26.453h-56.32v32.213h48.213v26.453h-48.213v53.973h-31.36z"],"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(255, 94, 72)","stroke":"rgb(255, 94, 72)","strokeLinejoin":"miter","strokeLinecap":"butt","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":true,"tags":["chat_attachment_pdf"],"grid":0},"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(255, 94, 72)","stroke":"rgb(255, 94, 72)","strokeLinejoin":"miter","strokeLinecap":"butt","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":17,"id":16,"name":"chat_attachment_pdf","prevSize":32,"code":59785,"codes":[59785,59786,59787,59788,59789,59790]},"setIdx":0,"setId":1,"iconIdx":23},{"icon":{"paths":["M298.667 938.667c-70.692 0-128-57.306-128-128v-682.667c0-23.564 19.102-42.667 42.667-42.667h408.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v494.327c0 70.694-57.306 128-128 128h-426.667z","M170.667 554.667h496.926c23.564 0 42.667 19.103 42.667 42.667v170.667c0 23.564-19.103 42.667-42.667 42.667h-496.926c-23.564 0-42.667-19.103-42.667-42.667v-170.667c0-23.564 19.103-42.667 42.667-42.667z","M597.333 298.667v-213.333h24.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v24.994h-213.333c-23.565 0-42.667-19.103-42.667-42.667z"],"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(168, 174, 178)"},{"fill":"rgb(0, 0, 0)","opacity":0.15}],"isMulticolor":true,"isMulticolor2":true,"tags":["chat_attachment_unknown"],"grid":0},"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(168, 174, 178)"},{"fill":"rgb(0, 0, 0)","opacity":0.15}],"properties":{"order":18,"id":17,"name":"chat_attachment_unknown","prevSize":32,"code":59791,"codes":[59791,59792,59793]},"setIdx":0,"setId":1,"iconIdx":24},{"icon":{"paths":["M414.933 233.026c44.54-53.079 100.907-82.751 169.11-89.014 68.198-6.263 128.794 12.835 181.781 57.295 52.983 44.461 82.368 100.862 88.145 169.202 5.781 68.339-13.598 129.049-58.138 182.127l-246.566 293.85c-31.646 37.713-71.637 58.778-119.979 63.202-48.34 4.425-91.367-9.186-129.079-40.832-37.713-31.642-58.588-71.654-62.626-120.030-4.038-48.371 9.765-91.418 41.409-129.131l233.381-278.131c18.564-22.124 42.121-34.546 70.665-37.265s53.892 5.217 76.049 23.807c22.153 18.59 34.534 42.222 37.141 70.896 2.611 28.676-5.457 54.182-24.205 76.527l-220.194 262.417c-5.828 6.946-13.065 10.795-21.709 11.55-8.64 0.755-16.436-1.783-23.385-7.616-6.948-5.828-10.798-13.065-11.549-21.7s1.787-16.427 7.615-23.373l220.197-262.417c7.347-8.758 10.594-18.604 9.732-29.536-0.858-10.932-5.666-20.073-14.426-27.421s-18.594-10.498-29.508-9.445c-10.918 1.052-20.049 5.958-27.396 14.715l-233.646 278.448c-19.918 24.418-28.685 51.887-26.302 82.415s15.681 55.949 39.894 76.267c24.111 20.233 51.558 28.907 82.341 26.035 30.783-2.876 56.302-16.384 76.556-40.521l246.566-293.845c33.489-39.232 48.030-84.301 43.614-135.208-4.416-50.904-26.364-92.923-65.852-126.055-38.929-32.666-83.639-46.583-134.131-41.751s-92.655 26.724-126.481 65.676l-233.38 278.132c-5.828 6.946-13.063 10.795-21.705 11.55s-16.438-1.783-23.386-7.612c-6.949-5.833-10.799-13.065-11.549-21.7s1.788-16.427 7.616-23.373l233.381-278.134z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_attachment"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":852,"id":18,"name":"chat_attachment","prevSize":32,"code":59794},"setIdx":0,"setId":1,"iconIdx":25},{"icon":{"paths":["M226.462 874.667c-21.552 0-39.795-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-571.076c0-21.552 7.467-39.795 22.4-54.729s33.176-22.4 54.729-22.4h571.076c21.551 0 39.795 7.467 54.729 22.4s22.4 33.176 22.4 54.729v571.076c0 21.551-7.467 39.795-22.4 54.729s-33.178 22.4-54.729 22.4h-571.076zM226.462 810.667h571.076c3.281 0 6.289-1.37 9.024-4.105s4.105-5.743 4.105-9.024v-571.076c0-3.282-1.37-6.292-4.105-9.027s-5.743-4.102-9.024-4.102h-571.076c-3.282 0-6.292 1.367-9.027 4.102s-4.102 5.745-4.102 9.027v571.076c0 3.281 1.367 6.289 4.102 9.024s5.745 4.105 9.027 4.105z","M405.333 597.333h-42.667c-12.089 0-22.222-4.087-30.4-12.267s-12.267-18.313-12.267-30.4v-85.333c0-12.087 4.089-22.221 12.267-30.4s18.311-12.267 30.4-12.267h64c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933c0 5.687-2.133 10.667-6.4 14.933s-9.246 6.4-14.933 6.4h-64v85.333h42.667v-21.333c0-5.687 2.133-10.667 6.4-14.933s9.245-6.4 14.933-6.4c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933v21.333c0 12.087-4.087 22.221-12.267 30.4s-18.311 12.267-30.4 12.267z","M526.933 590.933c-4.267 4.267-9.246 6.4-14.933 6.4s-10.667-2.133-14.933-6.4c-4.267-4.267-6.4-9.246-6.4-14.933v-128c0-5.687 2.133-10.667 6.4-14.933s9.246-6.4 14.933-6.4c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933v128c0 5.687-2.133 10.667-6.4 14.933z","M612.267 590.933c-4.267 4.267-9.246 6.4-14.933 6.4s-10.667-2.133-14.933-6.4c-4.267-4.267-6.4-9.246-6.4-14.933v-128c0-5.687 2.133-10.667 6.4-14.933s9.246-6.4 14.933-6.4h85.333c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933c0 5.687-2.133 10.667-6.4 14.933s-9.246 6.4-14.933 6.4h-64v21.333h42.667c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933c0 5.687-2.133 10.667-6.4 14.933s-9.246 6.4-14.933 6.4h-42.667v42.667c0 5.687-2.133 10.667-6.4 14.933z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_gif"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":851,"id":19,"name":"chat_gif","prevSize":32,"code":59795},"setIdx":0,"setId":1,"iconIdx":26},{"icon":{"paths":["M171.733 852.267c14.933 14.933 33.176 22.4 54.729 22.4h571.076c21.551 0 39.795-7.467 54.729-22.4s22.4-33.178 22.4-54.729v-571.076c0-21.552-7.467-39.795-22.4-54.729s-33.178-22.4-54.729-22.4h-571.076c-21.552 0-39.795 7.467-54.729 22.4s-22.4 33.176-22.4 54.729v571.076c0 21.551 7.467 39.795 22.4 54.729zM405.333 597.333h-42.667c-12.089 0-22.222-4.092-30.4-12.267-8.178-8.179-12.267-18.313-12.267-30.4v-85.333c0-12.092 4.089-22.225 12.267-30.4 8.178-8.179 18.311-12.268 30.4-12.268h64c5.687 0 10.667 2.135 14.933 6.401s6.4 9.242 6.4 14.933c0 5.687-2.133 10.667-6.4 14.933s-9.246 6.4-14.933 6.4h-64v85.333h42.667v-21.333c0-5.692 2.133-10.667 6.4-14.933s9.245-6.4 14.933-6.4c5.687 0 10.667 2.133 14.933 6.4s6.4 9.242 6.4 14.933v21.333c0 12.087-4.087 22.221-12.267 30.4-8.179 8.175-18.311 12.267-30.4 12.267zM526.933 590.933c-4.267 4.267-9.246 6.4-14.933 6.4s-10.667-2.133-14.933-6.4c-4.267-4.267-6.4-9.246-6.4-14.933v-128c0-5.692 2.133-10.667 6.4-14.933s9.246-6.401 14.933-6.401c5.687 0 10.667 2.135 14.933 6.401s6.4 9.242 6.4 14.933v128c0 5.687-2.133 10.667-6.4 14.933zM597.333 597.333c5.687 0 10.667-2.133 14.933-6.4s6.4-9.246 6.4-14.933v-42.667h42.667c5.687 0 10.667-2.133 14.933-6.4s6.4-9.246 6.4-14.933c0-5.692-2.133-10.667-6.4-14.933s-9.246-6.4-14.933-6.4h-42.667v-21.333h64c5.687 0 10.667-2.133 14.933-6.4s6.4-9.246 6.4-14.933c0-5.692-2.133-10.667-6.4-14.933s-9.246-6.401-14.933-6.401h-85.333c-5.687 0-10.667 2.135-14.933 6.401s-6.4 9.242-6.4 14.933v128c0 5.687 2.133 10.667 6.4 14.933s9.246 6.4 14.933 6.4z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_gif_fill"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":850,"id":20,"name":"chat_gif_fill","prevSize":32,"code":59796},"setIdx":0,"setId":1,"iconIdx":27},{"icon":{"paths":["M917.333 512c0 223.859-181.474 405.333-405.333 405.333s-405.333-181.474-405.333-405.333c0-223.859 181.474-405.333 405.333-405.333s405.333 181.474 405.333 405.333z","M458.509 437.615c-13.248 13.355-29.466 20.032-48.649 20.032-19.185 0-35.401-6.626-48.65-19.874s-19.872-29.564-19.872-48.949c0-19.386 6.624-35.703 19.872-48.951s29.566-19.873 48.951-19.873c18.95 0 35.056 6.725 48.321 20.174s19.9 29.713 19.9 48.791c0 19.078-6.626 35.295-19.874 48.65z","M714.509 501.615c-13.248 13.355-29.466 20.032-48.649 20.032s-35.401-6.626-48.649-19.874c-13.248-13.248-19.874-29.564-19.874-48.951 0-19.383 6.626-35.7 19.874-48.948s29.564-19.873 48.951-19.873c18.948 0 35.055 6.725 48.32 20.174s19.9 29.712 19.9 48.793c0 19.076-6.626 35.294-19.874 48.649z","M563.648 727.266c-35.942 9.139-72.41 8.755-109.402-1.156-36.352-9.741-67.967-27.597-94.844-53.572s-46.046-56.58-57.51-91.819c-1.891-8.546 0.254-16.060 6.433-22.528 6.18-6.473 13.681-8.525 22.505-6.161l317.336 85.030c8.823 2.364 14.293 7.893 16.41 16.589 2.116 8.691 0.218 16.269-5.692 22.724-27.55 24.785-59.294 41.749-95.236 50.893z"],"attrs":[{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(0, 0, 0)"}],"isMulticolor":true,"isMulticolor2":false,"tags":["chat_emoji_fill"],"grid":0},"attrs":[{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(0, 0, 0)"}],"properties":{"order":790,"id":21,"name":"chat_emoji_fill","prevSize":32,"code":59797,"codes":[59797,59798,59799,59800]},"setIdx":0,"setId":1,"iconIdx":28},{"icon":{"paths":["M311.795 874.667c-21.279 0-39.453-7.535-54.523-22.605s-22.606-33.246-22.606-54.524v-541.536h-10.667c-9.080 0-16.684-3.063-22.81-9.189s-9.19-13.73-9.19-22.81c0-9.080 3.063-16.684 9.19-22.81s13.729-9.19 22.81-9.19h159.999c0-10.448 3.679-19.35 11.036-26.707s16.259-11.036 26.707-11.036h180.515c10.445 0 19.349 3.679 26.705 11.036 7.36 7.357 11.038 16.259 11.038 26.707h160c9.079 0 16.683 3.063 22.805 9.19 6.127 6.126 9.195 13.729 9.195 22.81s-3.068 16.684-9.195 22.81c-6.123 6.126-13.726 9.189-22.805 9.189h-10.667v541.536c0 21.278-7.539 39.454-22.609 54.524s-33.242 22.605-54.524 22.605h-400.405zM725.333 256.002h-426.668v541.536c0 3.831 1.231 6.976 3.693 9.438s5.608 3.695 9.437 3.695h400.405c3.831 0 6.976-1.233 9.438-3.695s3.695-5.606 3.695-9.438v-541.536zM512 578.3l88.452 88.452c5.905 5.905 13.333 8.93 22.276 9.067s16.503-2.889 22.686-9.067c6.182-6.182 9.271-13.675 9.271-22.481s-3.089-16.303-9.271-22.481l-88.452-88.452 88.452-88.452c5.905-5.909 8.93-13.333 9.067-22.278 0.137-8.943-2.884-16.505-9.067-22.686s-13.675-9.272-22.481-9.272c-8.806 0-16.303 3.091-22.481 9.272l-88.452 88.45-88.454-88.45c-5.907-5.908-13.332-8.93-22.276-9.067s-16.505 2.885-22.685 9.067c-6.182 6.181-9.273 13.675-9.273 22.481 0 8.809 3.091 16.301 9.273 22.483l88.449 88.452-88.449 88.452c-5.908 5.905-8.931 13.329-9.067 22.276-0.137 8.943 2.885 16.503 9.067 22.686 6.181 6.178 13.674 9.271 22.481 9.271s16.3-3.093 22.481-9.271l88.454-88.452z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["clear-all"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":838,"id":22,"name":"clear-all","prevSize":32,"code":59759},"setIdx":0,"setId":1,"iconIdx":29},{"icon":{"paths":["M724.514 767.996h169.024c9.084 0 16.687 3.063 22.81 9.19 6.127 6.127 9.19 13.73 9.19 22.81s-3.063 16.683-9.19 22.81c-6.123 6.127-13.726 9.19-22.81 9.19h-233.024l64-64zM214.647 831.996c-5.141 0-10.037-0.93-14.687-2.79s-8.971-4.787-12.964-8.781l-63.917-63.915c-14.988-14.989-22.659-33.203-23.014-54.647s6.96-40.013 21.948-55.71l451.283-468.349c14.985-15.699 33.092-23.48 54.315-23.343 21.227 0.137 39.33 7.699 54.319 22.687l193.395 193.393c14.985 14.988 22.549 33.272 22.686 54.851s-7.292 39.863-22.276 54.852l-331.652 340.181c-3.994 3.994-8.384 6.921-13.167 8.781-4.787 1.86-9.749 2.79-14.895 2.79h-301.372zM505.271 767.996l325.5-334.029c2.185-2.189 3.281-5.333 3.281-9.437s-1.097-7.249-3.281-9.437l-192.738-192.737c-2.189-2.188-5.265-3.282-9.233-3.282-3.964 0-7.044 1.231-9.229 3.693l-452.596 467.525c-2.188 2.462-3.282 5.679-3.282 9.643 0 3.968 1.094 7.044 3.282 9.233l58.83 58.829h279.466z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["erasor"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":839,"id":23,"name":"erasor","prevSize":32,"code":59760},"setIdx":0,"setId":1,"iconIdx":30},{"icon":{"paths":["M370.707 833.643l-223.016-223.014c-6.563-6.566-11.486-13.841-14.769-21.828-3.281-7.983-4.922-16.329-4.922-25.024 0-8.149 1.641-16.218 4.922-24.205 3.283-7.987 8.206-15.262 14.769-21.824l237.129-236.884-106.256-102.975c-4.868-4.868-7.344-10.42-7.426-16.656s2.312-11.87 7.18-16.902c4.868-5.033 10.502-7.549 16.902-7.549s12.116 2.516 17.149 7.549l373.417 374.237c6.562 6.562 11.443 13.837 14.647 21.824 3.2 7.987 4.796 16.055 4.796 24.205 0 8.7-1.596 17.041-4.796 25.028-3.204 7.983-8.085 15.262-14.647 21.824l-222.195 222.195c-6.566 6.562-13.841 11.486-21.828 14.771-7.983 3.281-16.052 4.919-24.203 4.919-8.698 0-17.040-1.638-25.026-4.919-7.986-3.285-15.262-8.209-21.826-14.771zM416.738 312.781l-233.19 233.19c-3.281 3.281-5.469 6.562-6.564 9.847-1.094 3.281-1.641 6.562-1.641 9.843h482.79c0-3.281-0.546-6.562-1.643-9.843-1.092-3.285-3.281-6.566-6.562-9.847l-233.19-233.19zM824.286 853.333c-20.13 0-36.894-7.014-50.295-21.043-13.402-14.033-20.105-31.194-20.105-51.49 0-15.369 3.298-30.221 9.89-44.553s15.108-27.759 25.557-40.286l15.343-19.038c5.308-6.182 11.883-9.31 19.733-9.395 7.851-0.081 14.43 2.97 19.733 9.152l17.233 19.281c10.283 12.527 18.761 25.954 25.434 40.286s10.010 29.184 10.010 44.553c0 20.297-7.057 37.457-21.167 51.49-14.114 14.029-31.236 21.043-51.366 21.043z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["color-picker"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":840,"id":24,"name":"color-picker","prevSize":32,"code":59763},"setIdx":0,"setId":1,"iconIdx":31},{"icon":{"paths":["M576 362.667c0-117.821-95.514-213.333-213.333-213.333s-213.333 95.513-213.333 213.333c0 117.82 95.513 213.333 213.333 213.333s213.333-95.514 213.333-213.333zM640 362.667c0 153.169-124.164 277.333-277.333 277.333-153.167 0-277.333-124.164-277.333-277.333 0-153.167 124.166-277.333 277.333-277.333 153.169 0 277.333 124.166 277.333 277.333zM298.667 718.221c0-11.78 9.551-21.333 21.333-21.333h17.067c11.782 0 21.333 9.553 21.333 21.333v96.713c0 11.78 9.551 21.333 21.333 21.333h435.2c11.78 0 21.333-9.553 21.333-21.333v-435.2c0-11.782-9.553-21.333-21.333-21.333h-96.713c-11.78 0-21.333-9.551-21.333-21.333v-17.067c0-11.782 9.553-21.333 21.333-21.333h156.446c11.78 0 21.333 9.551 21.333 21.333v554.667c0 11.78-9.553 21.333-21.333 21.333h-554.667c-11.782 0-21.333-9.553-21.333-21.333v-156.446z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["shapes"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":841,"id":25,"name":"shapes","prevSize":32,"code":59764},"setIdx":0,"setId":1,"iconIdx":32},{"icon":{"paths":["M196.186 873.028c-13.565 3.008-25.244-0.384-35.036-10.176s-13.182-21.47-10.174-35.034l35.692-171.319 180.838 180.838-171.321 35.691zM367.506 837.338l-180.838-180.838 478.689-478.688c14.711-14.714 32.926-22.071 54.643-22.071s39.932 7.357 54.647 22.071l71.548 71.548c14.711 14.714 22.071 32.929 22.071 54.645s-7.36 39.931-22.071 54.645l-478.689 478.688zM710.976 222.774l-436.269 435.854 90.667 90.667 435.855-436.265c2.462-2.462 3.695-5.539 3.695-9.231s-1.233-6.77-3.695-9.232l-71.791-71.793c-2.462-2.462-5.538-3.693-9.233-3.693-3.691 0-6.767 1.231-9.229 3.693z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["pen"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":842,"id":26,"name":"pen","prevSize":32,"code":59765},"setIdx":0,"setId":1,"iconIdx":33},{"icon":{"paths":["M544.956 608.469l264.418-2.867c9.037 0.021 16.521-2.419 22.443-7.326s10.172-10.645 12.749-17.207c2.577-6.566 3.209-13.751 1.894-21.555-1.318-7.799-5.295-14.767-11.938-20.902l-463.807-417.010c-5.917-5.757-12.553-9.034-19.908-9.83s-14.399 0.274-21.129 3.21c-6.73 2.935-12.305 7.37-16.724 13.304s-6.527 13.027-6.319 21.281l-8.943 623.695c-0.023 8.845 2.35 16.418 7.12 22.716s10.44 10.739 17.014 13.321c6.574 2.577 13.735 3.311 21.484 2.189 7.749-1.118 14.664-4.992 20.745-11.618l180.902-191.398zM363.004 708.228l6.292-502.43 373.62 336.696-225.31 1.822-154.602 163.913z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["selector"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":843,"id":27,"name":"selector","prevSize":32,"code":59767},"setIdx":0,"setId":1,"iconIdx":34},{"icon":{"paths":["M716.739 875.075c-10.926 0-20.081-3.697-27.474-11.085-7.393-7.393-11.085-16.553-11.085-27.479v-121.846c0-10.993 3.948-20.101 11.853-27.325 7.905-7.219 17.49-10.829 28.759-10.829v-42.665c0-21.775 7.762-40.417 23.276-55.921 15.519-15.508 34.171-23.26 55.956-23.26 21.791 0 40.356 7.752 55.7 23.26 15.345 15.503 23.014 34.145 23.014 55.921v42.665c11.269 0 20.859 3.61 28.759 10.829 7.905 7.224 11.858 16.333 11.858 27.325v121.846c0 10.926-3.697 20.086-11.085 27.479-7.393 7.388-16.553 11.085-27.479 11.085h-162.053zM756.536 676.511h82.463v-42.665c0-12.088-3.82-22.016-11.448-29.788-7.629-7.767-17.49-11.648-29.578-11.648s-22.016 3.881-29.783 11.648c-7.772 7.772-11.653 17.7-11.653 29.788v42.665zM512.026 605.865c-32.002 0-59.202-11.197-81.602-33.597s-33.6-49.603-33.6-81.602c0-32 11.2-59.2 33.6-81.6s49.6-33.6 81.602-33.6c32 0 59.197 11.2 81.597 33.6s33.603 49.6 33.603 81.6c0 31.999-11.203 59.202-33.603 81.602s-49.597 33.597-81.597 33.597zM512.113 789.33c-90.151 0-174.392-23.89-252.723-71.67-78.332-47.78-137.381-112.225-177.147-193.336-2.791-5.315-4.829-10.726-6.114-16.223-1.286-5.497-1.929-11.309-1.929-17.436s0.643-11.938 1.929-17.436c1.285-5.497 3.323-10.906 6.114-16.224 39.766-81.109 98.813-145.553 177.144-193.335 78.329-47.781 162.542-71.671 252.639-71.671 94.249 0 180.47 25.039 258.662 75.118 78.198 50.078 137.641 117.483 178.34 202.214h-151.055c-21.284 0-41.431 2.968-60.436 8.902-19.005 5.936-36.47 14.483-52.388 25.641 0.276-2.243 0.481-4.444 0.62-6.605 0.133-2.161 0.205-4.363 0.205-6.606 0-48.318-16.922-89.39-50.765-123.213s-74.936-50.735-123.281-50.735c-48.345 0-89.406 16.921-123.184 50.762-33.778 33.843-50.667 74.937-50.667 123.282 0 48.343 16.912 89.405 50.735 123.182 33.823 33.782 74.894 50.668 123.214 50.668 15.263 0 30.003-1.915 44.227-5.745 14.218-3.825 27.459-9.216 39.711-16.164-1.26 5.197-2.094 10.353-2.504 15.468s-0.614 10.409-0.614 15.877v108.227c-13.455 2.017-26.911 3.697-40.366 5.038-13.455 1.347-26.911 2.017-40.366 2.017z"],"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["view-only"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":29,"id":28,"name":"view-only","prevSize":32,"code":59770},"setIdx":0,"setId":1,"iconIdx":35},{"icon":{"paths":["M853.333 352v-82.871c0-3.282-1.365-6.292-4.1-9.027-2.739-2.735-5.747-4.102-9.028-4.102h-82.871c-9.079 0-16.683-3.063-22.81-9.19s-9.19-13.729-9.19-22.81c0-9.080 3.063-16.684 9.19-22.81s13.73-9.19 22.81-9.19h82.871c21.551 0 39.795 7.467 54.729 22.4s22.4 33.176 22.4 54.729v82.871c0 9.080-3.063 16.684-9.19 22.81s-13.73 9.19-22.81 9.19c-9.079 0-16.683-3.063-22.81-9.19s-9.19-13.729-9.19-22.81zM106.668 352v-82.871c0-21.552 7.467-39.795 22.4-54.729s33.176-22.4 54.729-22.4h82.871c9.080 0 16.683 3.063 22.81 9.19s9.189 13.729 9.189 22.81c0 9.080-3.063 16.684-9.189 22.81s-13.73 9.19-22.81 9.19h-82.871c-3.283 0-6.292 1.367-9.027 4.102s-4.102 5.745-4.102 9.027v82.871c0 9.080-3.063 16.684-9.19 22.81s-13.729 9.19-22.809 9.19c-9.080 0-16.684-3.063-22.81-9.19s-9.19-13.729-9.19-22.81zM840.205 832h-82.871c-9.079 0-16.683-3.063-22.81-9.19s-9.19-13.73-9.19-22.81c0-9.084 3.063-16.687 9.19-22.81 6.127-6.127 13.73-9.19 22.81-9.19h82.871c3.281 0 6.289-1.37 9.028-4.105 2.735-2.735 4.1-5.743 4.1-9.024v-82.871c0-9.084 3.063-16.687 9.19-22.81 6.127-6.127 13.73-9.19 22.81-9.19s16.683 3.063 22.81 9.19c6.127 6.123 9.19 13.726 9.19 22.81v82.871c0 21.551-7.467 39.795-22.4 54.729s-33.178 22.4-54.729 22.4zM183.796 832c-21.553 0-39.796-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-82.871c0-9.084 3.063-16.687 9.19-22.81 6.127-6.127 13.73-9.19 22.81-9.19s16.683 3.063 22.809 9.19c6.127 6.123 9.19 13.726 9.19 22.81v82.871c0 3.281 1.367 6.289 4.102 9.024s5.745 4.105 9.027 4.105h82.871c9.080 0 16.683 3.063 22.81 9.19 6.126 6.123 9.189 13.726 9.189 22.81 0 9.079-3.063 16.683-9.189 22.81s-13.73 9.19-22.81 9.19h-82.871zM274.052 587.486v-150.972c0-21.28 7.535-39.454 22.605-54.525s33.245-22.605 54.522-22.605h321.64c21.278 0 39.454 7.535 54.524 22.605s22.605 33.245 22.605 54.525v150.972c0 21.278-7.535 39.454-22.605 54.524s-33.246 22.605-54.524 22.605h-321.64c-21.278 0-39.452-7.535-54.522-22.605s-22.605-33.246-22.605-54.524zM351.18 600.614h321.64c3.831 0 6.976-1.229 9.438-3.691s3.691-5.606 3.691-9.438v-150.972c0-3.831-1.229-6.976-3.691-9.438s-5.606-3.693-9.438-3.693h-321.64c-3.829 0-6.974 1.231-9.435 3.693s-3.693 5.606-3.693 9.438v150.972c0 3.831 1.231 6.976 3.693 9.438s5.606 3.691 9.435 3.691z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["fit-to-screen"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":844,"id":29,"name":"fit-to-screen","prevSize":32,"code":59769},"setIdx":0,"setId":1,"iconIdx":36},{"icon":{"paths":["M374.154 437.338h-50.872c-9.067 0-16.666-3.068-22.799-9.203-6.133-6.137-9.2-13.74-9.2-22.81s3.067-16.669 9.2-22.796c6.132-6.127 13.732-9.19 22.799-9.19h50.872v-50.872c0-9.067 3.068-16.667 9.204-22.8s13.739-9.199 22.81-9.199c9.070 0 16.669 3.066 22.794 9.199 6.127 6.133 9.19 13.734 9.19 22.8v50.872h50.871c9.067 0 16.666 3.068 22.801 9.203 6.131 6.136 9.199 13.74 9.199 22.81s-3.068 16.669-9.199 22.795c-6.135 6.127-13.734 9.19-22.801 9.19h-50.871v50.871c0 9.067-3.068 16.666-9.203 22.801-6.136 6.131-13.74 9.199-22.81 9.199-9.071 0-16.67-3.068-22.796-9.199-6.126-6.135-9.19-13.734-9.19-22.801v-50.871zM406.153 666.261c-72.925 0-134.642-25.25-185.154-75.75s-75.767-112.201-75.767-185.105c0-72.906 25.249-134.631 75.748-185.174s112.201-75.814 185.107-75.814c72.906 0 134.632 25.256 185.175 75.767s75.814 112.23 75.814 185.154c0 30.467-5.116 59.57-15.343 87.304-10.231 27.733-23.881 51.853-40.947 72.367l245.5 245.5c5.909 5.905 8.93 13.333 9.067 22.276s-2.884 16.503-9.067 22.686c-6.182 6.182-13.675 9.271-22.481 9.271s-16.299-3.089-22.481-9.271l-245.5-245.5c-21.333 17.617-45.867 31.398-73.6 41.357-27.733 9.954-56.422 14.933-86.071 14.933zM406.153 602.261c54.975 0 101.537-19.076 139.694-57.229 38.153-38.157 57.229-84.719 57.229-139.694s-19.076-101.539-57.229-139.693c-38.157-38.154-84.719-57.231-139.694-57.231s-101.539 19.077-139.693 57.231c-38.153 38.154-57.23 84.718-57.23 139.693s19.077 101.537 57.23 139.694c38.154 38.153 84.718 57.229 139.693 57.229z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["zoom-in"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":845,"id":30,"name":"zoom-in","prevSize":32,"code":59747},"setIdx":0,"setId":1,"iconIdx":37},{"icon":{"paths":["M343.115 444.753c-9.066 0-16.666-3.068-22.799-9.203s-9.2-13.739-9.2-22.809c0-9.071 3.066-16.669 9.2-22.796s13.733-9.189 22.799-9.189h139.488c9.067 0 16.666 3.068 22.801 9.203 6.131 6.136 9.199 13.74 9.199 22.81s-3.068 16.671-9.199 22.794c-6.135 6.127-13.734 9.19-22.801 9.19h-139.488zM412.859 673.677c-72.925 0-134.643-25.25-185.154-75.75-50.512-50.496-75.767-112.201-75.767-185.104 0-72.906 25.249-134.631 75.748-185.175s112.201-75.814 185.107-75.814c72.907 0 134.629 25.256 185.172 75.767 50.547 50.512 75.819 112.23 75.819 185.154 0 30.467-5.116 59.569-15.347 87.303-10.227 27.733-23.876 51.857-40.943 72.367l245.5 245.5c5.905 5.909 8.93 13.333 9.067 22.276s-2.884 16.508-9.067 22.686c-6.182 6.182-13.675 9.271-22.481 9.271s-16.303-3.089-22.481-9.271l-245.5-245.495c-21.333 17.613-45.867 31.398-73.6 41.353s-56.422 14.933-86.072 14.933zM412.859 609.677c54.976 0 101.539-19.076 139.691-57.229 38.157-38.153 57.233-84.719 57.233-139.693s-19.076-101.539-57.233-139.693c-38.153-38.154-84.715-57.231-139.691-57.231-54.975 0-101.539 19.077-139.693 57.231s-57.23 84.719-57.23 139.693c0 54.974 19.077 101.541 57.23 139.693s84.718 57.229 139.693 57.229z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["zoom-out"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":846,"id":31,"name":"zoom-out","prevSize":32,"code":59748},"setIdx":0,"setId":1,"iconIdx":38},{"icon":{"paths":["M339.55 787.14c-9.080 0-16.683-3.063-22.81-9.19s-9.189-13.73-9.189-22.81c0-9.079 3.063-16.683 9.189-22.81s13.73-9.186 22.81-9.186h274.218c44.523 0 82.842-14.703 114.953-44.105s48.162-65.711 48.162-108.924c0-43.213-16.051-79.454-48.162-108.719s-70.43-43.896-114.953-43.896h-299.574l96.247 96.248c6.181 6.178 9.271 13.675 9.271 22.481s-3.090 16.299-9.271 22.481c-6.181 6.182-13.743 9.203-22.687 9.067s-16.369-3.157-22.277-9.067l-146.214-146.215c-3.993-3.993-6.81-8.205-8.451-12.636s-2.462-9.216-2.462-14.359c0-5.142 0.82-9.929 2.462-14.359s4.458-8.642 8.451-12.636l146.214-146.214c6.182-6.181 13.676-9.271 22.482-9.271s16.3 3.090 22.481 9.271c6.181 6.181 9.203 13.743 9.067 22.687s-3.159 16.368-9.067 22.276l-96.247 96.247h299.574c62.413 0 115.866 20.827 160.367 62.481 44.497 41.654 66.748 93.033 66.748 154.132 0 61.103-22.251 112.546-66.748 154.338-44.501 41.792-97.954 62.686-160.367 62.686h-274.218z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["undo"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":847,"id":32,"name":"undo","prevSize":32,"code":59749},"setIdx":0,"setId":1,"iconIdx":39},{"icon":{"paths":["M710.071 419.689h-299.57c-44.526 0-82.844 14.633-114.953 43.898s-48.164 65.506-48.164 108.719c0 43.213 16.055 79.522 48.164 108.924s70.427 44.1 114.953 44.1h274.218c9.079 0 16.683 3.063 22.81 9.19s9.186 13.73 9.186 22.81c0 9.079-3.059 16.683-9.186 22.81s-13.73 9.19-22.81 9.19h-274.218c-62.413 0-115.869-20.894-160.368-62.686s-66.748-93.239-66.748-154.338c0-61.103 22.249-112.478 66.748-154.133 44.499-41.654 97.955-62.481 160.368-62.481h299.57l-96.247-96.247c-5.905-5.908-8.93-13.333-9.067-22.276s2.889-16.506 9.067-22.687c6.182-6.181 13.675-9.271 22.481-9.271s16.303 3.090 22.485 9.271l146.214 146.214c3.994 3.994 6.81 8.206 8.448 12.636 1.643 4.431 2.462 9.217 2.462 14.359s-0.819 9.928-2.462 14.359c-1.638 4.431-4.454 8.643-8.448 12.636l-146.214 146.217c-5.909 5.905-13.333 8.93-22.276 9.067s-16.508-2.889-22.69-9.067c-6.178-6.182-9.271-13.675-9.271-22.485 0-8.806 3.093-16.299 9.271-22.481l96.247-96.245z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["redo"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":848,"id":33,"name":"redo","prevSize":32,"code":59750},"setIdx":0,"setId":1,"iconIdx":40},{"icon":{"paths":["M512 704c-53.278 0-98.597-18.684-135.958-56.043s-56.041-82.679-56.041-135.957c0-53.278 18.68-98.598 56.041-135.959s82.68-56.041 135.958-56.041c53.278 0 98.598 18.68 135.957 56.041 37.363 37.361 56.043 82.682 56.043 135.959s-18.679 98.598-56.043 135.957c-37.359 37.359-82.679 56.043-135.957 56.043zM85.333 544c-9.067 0-16.666-3.068-22.799-9.203-6.134-6.14-9.201-13.743-9.201-22.81 0-9.071 3.066-16.67 9.2-22.797 6.132-6.127 13.732-9.19 22.799-9.19h96.001c9.066 0 16.666 3.068 22.799 9.203s9.201 13.739 9.201 22.81c0 9.071-3.066 16.67-9.2 22.797s-13.733 9.19-22.799 9.19h-96.001zM842.667 544c-9.067 0-16.666-3.068-22.801-9.203-6.131-6.14-9.199-13.743-9.199-22.81 0-9.071 3.068-16.67 9.199-22.797 6.135-6.127 13.734-9.19 22.801-9.19h96c9.067 0 16.666 3.068 22.801 9.203 6.131 6.135 9.199 13.739 9.199 22.81s-3.068 16.67-9.199 22.797c-6.135 6.127-13.734 9.19-22.801 9.19h-96zM511.987 213.332c-9.071 0-16.67-3.067-22.797-9.2s-9.19-13.733-9.19-22.799v-96.001c0-9.067 3.068-16.667 9.203-22.8s13.739-9.199 22.81-9.199c9.071 0 16.67 3.066 22.797 9.199s9.19 13.734 9.19 22.8v96.001c0 9.066-3.068 16.666-9.203 22.799s-13.739 9.2-22.81 9.2zM511.987 970.667c-9.071 0-16.67-3.068-22.797-9.203-6.127-6.131-9.19-13.73-9.19-22.797v-96c0-9.067 3.068-16.666 9.203-22.801 6.135-6.131 13.739-9.199 22.81-9.199s16.67 3.068 22.797 9.199c6.127 6.135 9.19 13.734 9.19 22.801v96c0 9.067-3.068 16.666-9.203 22.797-6.135 6.135-13.739 9.203-22.81 9.203zM256.247 300.387l-53.662-52.185c-6.345-5.908-9.409-13.333-9.19-22.276s3.324-16.724 9.315-23.343c6.535-6.619 14.206-9.929 23.013-9.929s16.301 3.31 22.482 9.929l52.595 53.252c6.181 6.619 9.271 14.113 9.271 22.482s-3.022 15.863-9.067 22.481c-6.044 6.618-13.36 9.749-21.948 9.394s-16.191-3.625-22.81-9.806zM775.795 821.414l-52.595-53.252c-6.182-6.618-9.271-14.221-9.271-22.81s3.089-15.974 9.271-22.153c5.769-6.618 12.983-9.749 21.641-9.395s16.294 3.622 22.912 9.805l53.662 52.186c6.345 5.905 9.408 13.333 9.19 22.276s-3.324 16.725-9.314 23.343c-6.537 6.618-14.208 9.929-23.014 9.929s-16.299-3.311-22.481-9.929zM723.2 301.004c-6.618-6.044-9.749-13.36-9.395-21.948 0.358-8.588 3.627-16.192 9.805-22.81l52.186-53.662c5.909-6.345 13.333-9.409 22.276-9.19s16.725 3.324 23.343 9.315c6.618 6.535 9.929 14.206 9.929 23.012s-3.311 16.301-9.929 22.482l-53.252 52.595c-6.618 6.181-14.114 9.271-22.481 9.271s-15.863-3.023-22.481-9.067zM202.586 821.504c-6.619-6.677-9.929-14.421-9.929-23.228s3.31-16.303 9.929-22.481l53.251-52.595c6.619-6.182 14.222-9.271 22.809-9.271 8.588 0 15.973 3.089 22.154 9.271 6.345 5.769 9.34 12.983 8.985 21.641s-3.488 16.294-9.396 22.912l-52.184 53.662c-6.181 6.618-13.675 9.818-22.482 9.6s-16.519-3.388-23.137-9.51z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["light"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":849,"id":34,"name":"light","prevSize":32,"code":59751},"setIdx":0,"setId":1,"iconIdx":41},{"icon":{"paths":["M513.229 874.667c-101.085 0-186.801-35.17-257.146-105.515s-105.518-156.062-105.518-257.148c0-84.186 26.051-159.014 78.153-224.491s121.503-107.679 208.202-126.606c8.973-2.243 16.875-2.024 23.714 0.656 6.835 2.681 12.39 6.674 16.657 11.98s6.78 11.747 7.548 19.323c0.764 7.576-1.011 15.139-5.333 22.687-8.806 16.027-15.343 32.711-19.61 50.051s-6.4 35.473-6.4 54.399c0 70.018 24.452 129.478 73.353 178.382 48.905 48.9 108.365 73.353 178.381 73.353 20.954 0 40.973-2.654 60.062-7.957 19.093-5.308 35.802-11.46 50.133-18.462 7.002-3.063 13.841-4.143 20.514-3.243 6.673 0.905 12.39 3.132 17.148 6.69 5.197 3.554 9.067 8.41 11.61 14.562s2.829 13.389 0.862 21.705c-15.155 83.908-55.974 153.161-122.462 207.748-66.487 54.592-143.108 81.886-229.867 81.886z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["dark"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":837,"id":35,"name":"dark","prevSize":32,"code":59752},"setIdx":0,"setId":1,"iconIdx":42},{"icon":{"paths":["M833.293 190.706c12.497 12.497 12.497 32.758 0 45.255l-597.332 597.332c-12.497 12.497-32.758 12.497-45.255 0s-12.497-32.755 0-45.252l597.335-597.335c12.497-12.497 32.755-12.497 45.252 0z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["line"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":836,"id":36,"name":"line","prevSize":32,"code":59753},"setIdx":0,"setId":1,"iconIdx":43},{"icon":{"paths":["M138.667 181.333c0-23.564 19.103-42.667 42.667-42.667h661.333c23.565 0 42.667 19.103 42.667 42.667v661.333c0 23.565-19.102 42.667-42.667 42.667h-661.333c-23.564 0-42.667-19.102-42.667-42.667v-661.333zM202.667 202.667v618.667h618.667v-618.667h-618.667z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["square"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":835,"id":37,"name":"square","prevSize":32,"code":59754},"setIdx":0,"setId":1,"iconIdx":44},{"icon":{"paths":["M138.667 512c0-206.186 167.147-373.333 373.333-373.333s373.333 167.147 373.333 373.333c0 206.187-167.147 373.333-373.333 373.333s-373.333-167.147-373.333-373.333zM512 202.667c-170.84 0-309.333 138.493-309.333 309.333 0 170.842 138.493 309.333 309.333 309.333 170.842 0 309.333-138.492 309.333-309.333 0-170.84-138.492-309.333-309.333-309.333z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["circle"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":834,"id":38,"name":"circle","prevSize":32,"code":59755},"setIdx":0,"setId":1,"iconIdx":45},{"icon":{"paths":["M453.751 308.968l356.915-95.635-95.633 356.915-108.015-108.015-371.058 371.059c-12.497 12.497-32.758 12.497-45.255 0s-12.497-32.755 0-45.252l371.060-371.060-108.015-108.012z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":833,"id":39,"name":"arrow","prevSize":32,"code":59756},"setIdx":0,"setId":1,"iconIdx":46},{"icon":{"paths":["M1024 512c0 282.772-229.233 512-512 512-282.768 0-511.999-229.233-511.999-512 0-282.754 229.245-511.998 511.999-511.998 282.762 0 512 229.238 512 511.998z"],"attrs":[{"fill":"rgb(234, 196, 67)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["gradient"],"grid":0},"attrs":[{"fill":"rgb(234, 196, 67)"}],"properties":{"order":41,"id":40,"name":"gradient","prevSize":32,"code":59757},"setIdx":0,"setId":1,"iconIdx":47},{"icon":{"paths":["M191.999 896c-17.722 0-32.819-6.238-45.292-18.709s-18.707-27.571-18.707-45.291h63.999v64zM128 746.667v-85.333h63.999v85.333h-63.999zM128 576v-85.333h63.999v85.333h-63.999zM128 405.332v-85.333h63.999v85.333h-63.999zM128 234.666c0-17.722 6.236-32.82 18.707-45.292s27.569-18.707 45.292-18.707v63.999h-63.999zM277.332 896v-64h85.333v64h-85.333zM277.332 234.666v-63.999h85.333v63.999h-85.333zM448 234.666v-63.999h85.333v63.999h-85.333zM618.667 234.666v-63.999h85.333v63.999h-85.333zM789.333 405.332v-85.333h64v85.333h-64zM789.333 234.666v-63.999c17.719 0 32.819 6.236 45.291 18.707s18.709 27.569 18.709 45.292h-64z","M512 554.667l113.779 341.333 56.887-170.667 170.667-56.887-341.333-113.779z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["area-selection"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":832,"id":41,"name":"area-selection","prevSize":32,"code":59758},"setIdx":0,"setId":1,"iconIdx":48},{"icon":{"paths":["M384.047 917.739c-32.579 0-60.287-11.405-83.124-34.21s-34.256-50.496-34.256-83.076c0-32.576 11.403-60.288 34.208-83.123 22.805-22.839 50.497-34.257 83.076-34.257s60.286 11.405 83.125 34.21c22.835 22.801 34.253 50.496 34.253 83.076 0 32.576-11.401 60.284-34.206 83.123-22.805 22.835-50.497 34.257-83.076 34.257zM570.419 769.472c-4.813-32-17.203-60.608-37.167-85.824-19.968-25.22-44.608-43.435-73.929-54.647l125.786-125.623h-322.709c-20.020 0-36.759-6.976-50.216-20.928s-20.184-31.074-20.184-51.366c0-12.684 3.255-24.359 9.764-35.025s14.988-19.501 25.436-26.502l493.781-301.374c10.338-6.181 21.252-7.726 32.738-4.636 11.49 3.090 20.322 9.737 26.505 19.938 6.178 10.202 7.795 20.937 4.838 32.205-2.953 11.268-9.242 19.993-18.871 26.174l-402.296 252.801h397.705c19.759 0 36.433 6.793 50.018 20.378 13.585 13.588 20.382 30.257 20.382 50.021 0 11.499-1.327 22.839-3.981 34.018-2.654 11.183-8.055 20.847-16.205 28.996l-241.395 241.395z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["highlight"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":831,"id":42,"name":"highlight","prevSize":32,"code":59761},"setIdx":0,"setId":1,"iconIdx":49},{"icon":{"paths":["M480.004 800.661v-160.661c0-9.079 3.063-16.683 9.186-22.81 6.127-6.123 13.73-9.186 22.81-9.186 9.084 0 16.687 3.063 22.81 9.186 6.127 6.127 9.19 13.73 9.19 22.81v160.004l65.152-65.809c6.345-6.345 13.99-9.583 22.933-9.719s16.725 3.102 23.343 9.719c6.618 6.622 9.929 14.332 9.929 23.138s-3.311 16.521-9.929 23.138l-116.433 116.433c-3.994 3.994-8.205 6.81-12.634 8.448-4.433 1.643-9.216 2.462-14.362 2.462-5.141 0-9.924-0.819-14.357-2.462-4.429-1.638-8.64-4.454-12.634-8.448l-117.088-117.090c-6.345-6.345-9.477-13.875-9.396-22.601s3.433-16.397 10.051-23.019c6.618-6.618 14.222-9.924 22.81-9.924s16.191 3.307 22.81 9.924l65.808 66.466zM224 544l65.807 65.152c6.345 6.345 9.586 13.99 9.723 22.933s-3.104 16.725-9.723 23.343c-6.619 6.618-14.332 9.929-23.138 9.929s-16.519-3.311-23.138-9.929l-116.43-116.433c-3.994-3.994-6.811-8.205-8.451-12.634-1.641-4.433-2.462-9.216-2.462-14.362 0-5.141 0.821-9.924 2.462-14.357 1.641-4.429 4.458-8.64 8.451-12.634l116.43-116.432c6.345-6.345 13.88-9.586 22.605-9.722s16.396 3.104 23.014 9.722c6.619 6.618 9.929 14.222 9.929 22.81s-3.309 16.191-9.929 22.81l-65.806 65.808h160.658c9.080 0 16.684 3.063 22.81 9.186 6.126 6.127 9.189 13.73 9.189 22.81 0 9.084-3.063 16.687-9.189 22.81-6.127 6.127-13.73 9.19-22.81 9.19h-160.002zM800.661 544h-160.661c-9.079 0-16.683-3.063-22.81-9.19-6.123-6.123-9.186-13.726-9.186-22.81 0-9.079 3.063-16.683 9.186-22.81 6.127-6.123 13.73-9.186 22.81-9.186h160.004l-65.809-65.152c-6.345-6.345-9.583-13.988-9.719-22.932s3.102-16.724 9.719-23.343c6.622-6.619 14.332-9.929 23.138-9.929s16.521 3.31 23.138 9.929l116.433 116.432c3.994 3.994 6.81 8.205 8.448 12.634 1.643 4.433 2.462 9.216 2.462 14.357 0 5.146-0.819 9.929-2.462 14.362-1.638 4.429-4.454 8.64-8.448 12.634l-117.090 117.086c-6.345 6.349-13.769 9.481-22.276 9.395-8.503-0.081-16.064-3.43-22.686-10.048-6.618-6.618-9.929-14.221-9.929-22.81s3.311-16.192 9.929-22.81l65.809-65.809zM480.004 223.344l-66.464 66.463c-6.345 6.345-13.77 9.586-22.276 9.722s-16.069-3.104-22.687-9.722c-6.619-6.619-9.929-14.223-9.929-22.81s3.31-16.191 9.929-22.81l116.432-117.087c3.994-3.994 8.205-6.811 12.634-8.451 4.433-1.641 9.216-2.462 14.357-2.462 5.146 0 9.929 0.821 14.362 2.462 4.429 1.641 8.64 4.458 12.634 8.451l117.086 117.087c6.349 6.345 9.587 13.77 9.724 22.276s-3.102 16.068-9.724 22.686c-6.618 6.619-14.221 9.929-22.81 9.929-8.585 0-16.188-3.309-22.81-9.929l-66.462-65.806v160.658c0 9.080-3.063 16.684-9.19 22.81-6.123 6.127-13.726 9.19-22.81 9.19-9.079 0-16.683-3.063-22.81-9.19-6.123-6.126-9.186-13.729-9.186-22.81v-160.658z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["move"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":830,"id":43,"name":"move","prevSize":32,"code":59762},"setIdx":0,"setId":1,"iconIdx":50},{"icon":{"paths":["M466.871 282.665h-186.666c-12.535 0-23.191-4.392-31.966-13.176s-13.162-19.45-13.162-31.999c0-12.548 4.387-23.267 13.162-32.156s19.43-13.333 31.966-13.333h463.999c12.535 0 23.189 4.392 31.966 13.177 8.772 8.784 13.163 19.45 13.163 31.999s-4.39 23.267-13.163 32.156c-8.777 8.889-19.43 13.333-31.966 13.333h-186.667v504.207c0 12.535-4.395 23.189-13.18 31.966-8.781 8.772-19.447 13.163-31.996 13.163s-23.266-4.429-32.158-13.282c-8.887-8.858-13.333-19.61-13.333-32.256v-503.797z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["text"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":829,"id":44,"name":"text","prevSize":32,"code":59766},"setIdx":0,"setId":1,"iconIdx":51},{"icon":{"paths":["M209.154 846.438c-14.809 5.198-27.832 2.179-39.070-9.060-11.237-11.235-14.257-24.257-9.058-39.068l113.977-318.46c9.19-25.784 27.084-41.591 53.681-47.419s49.427 0.786 68.489 19.849l158.010 158.011c19.063 19.063 25.678 41.89 19.849 68.489-5.829 26.595-21.635 44.491-47.419 53.678l-318.459 113.979zM576.057 508.285c-5.091-5.095-7.639-10.818-7.639-17.17s2.548-12.079 7.639-17.17l207.479-207.48c20.533-20.532 45.371-30.799 74.514-30.799 29.147 0 53.985 10.266 74.514 30.799l3.703 3.703c4.411 4.411 6.685 9.793 6.816 16.146 0.131 6.355-2.351 12.078-7.447 17.172-4.829 5.094-10.42 7.641-16.777 7.641-6.353 0-12.075-2.547-17.17-7.641l-2.679-2.678c-11.133-11.133-24.654-16.699-40.567-16.699-15.909 0-29.434 5.566-40.567 16.699l-208.499 208.502c-4.411 4.411-9.794 6.615-16.151 6.615-6.353 0-12.075-2.544-17.17-7.639zM426.398 358.625c-5.095-5.094-7.639-10.818-7.639-17.172s2.544-12.078 7.639-17.171l7.406-7.405c12.182-12.184 18.272-26.94 18.272-44.27s-6.091-32.086-18.272-44.268l-9.691-9.689c-4.411-4.411-6.615-9.793-6.615-16.148 0-6.353 2.548-12.077 7.639-17.171 5.095-5.093 10.818-7.64 17.17-7.64 6.357 0 12.079 2.547 17.175 7.64l8.663 8.665c21.844 21.845 32.768 48.049 32.768 78.611s-10.924 56.766-32.768 78.612l-8.43 8.43c-4.411 4.411-9.794 6.616-16.146 6.616s-12.079-2.546-17.17-7.64zM501.228 433.455c-5.095-5.095-7.639-10.818-7.639-17.17s2.544-12.079 7.639-17.172l125.559-125.558c11.133-11.133 16.699-24.655 16.699-40.567s-5.566-29.434-16.699-40.567l-44.663-44.662c-4.411-4.411-6.615-9.794-6.615-16.148s2.544-12.077 7.639-17.171c5.095-5.093 10.818-7.64 17.17-7.64s12.079 2.547 17.175 7.64l43.635 43.638c20.795 20.795 31.195 45.765 31.195 74.91s-10.4 54.114-31.195 74.91l-126.579 126.582c-4.411 4.411-9.798 6.615-16.151 6.615s-12.075-2.548-17.17-7.639zM650.887 583.115c-5.091-5.091-7.639-10.818-7.639-17.17s2.548-12.079 7.639-17.17l31.822-31.822c22.11-22.11 48.787-33.165 80.032-33.165s57.922 11.055 80.028 33.165l32.846 32.846c4.411 4.411 6.619 9.794 6.619 16.146s-2.548 12.079-7.643 17.17c-5.091 5.095-10.818 7.639-17.17 7.639s-12.079-2.544-17.17-7.639l-31.822-31.822c-12.448-12.444-27.677-18.67-45.687-18.67-18.014 0-33.243 6.226-45.687 18.67l-32.846 32.846c-4.411 4.411-9.794 6.615-16.151 6.615-6.353 0-12.075-2.544-17.17-7.639z"],"attrs":[{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["celebration"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"}],"properties":{"order":46,"id":45,"name":"celebration","prevSize":32,"code":59746},"setIdx":0,"setId":1,"iconIdx":52},{"icon":{"paths":["M319.992 797.88c-19.217 0-35.403-6.63-48.559-19.891s-19.733-29.496-19.733-48.701v-36.756c0-7.875 2.633-14.464 7.899-19.773s11.802-7.962 19.61-7.962c7.808 0 14.419 2.652 19.834 7.962s8.123 11.899 8.123 19.773v36.756c0 3.282 1.368 6.292 4.102 9.027 2.736 2.734 5.745 4.101 9.028 4.101h434.541c3.282 0 6.292-1.367 9.027-4.101s4.101-5.745 4.101-9.027v-36.756c0-7.875 2.637-14.464 7.9-19.773s11.802-7.962 19.61-7.962c7.808 0 14.418 2.652 19.835 7.962s8.12 11.899 8.12 19.773v36.756c0 19.205-6.636 35.441-19.907 48.701-13.266 13.261-29.512 19.891-48.727 19.891h-434.803zM509.833 281.123l-87.385 87.385c-5.634 5.634-12.102 8.588-19.405 8.862s-14.077-2.544-20.325-8.451c-5.841-5.908-8.693-12.513-8.556-19.814s3.159-13.907 9.065-19.815l130.543-130.542c3.569-3.282 7.316-5.744 11.249-7.385s8.187-2.462 12.759-2.462c4.572 0 8.827 0.821 12.759 2.462s7.542 4.103 10.824 7.385l130.54 130.542c5.637 5.634 8.591 11.993 8.863 19.076s-2.509 13.579-8.351 19.487c-6.246 5.907-12.954 8.793-20.122 8.656-7.163-0.136-13.701-3.159-19.61-9.066l-87.383-86.318v347.736c0 7.869-2.632 14.459-7.9 19.768-5.263 5.309-11.802 7.962-19.61 7.962s-14.418-2.652-19.835-7.962c-5.412-5.309-8.121-11.899-8.121-19.768v-347.736z"],"width":1075,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["upload-new"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":828,"id":46,"name":"upload-new","prevSize":32,"code":59745},"setIdx":0,"setId":1,"iconIdx":53},{"icon":{"paths":["M539.525 714.65c7.112 0 13.332-2.493 18.652-7.475 5.34-4.982 8.013-11.382 8.013-19.2v-147.2h148.219c7.096 0 13.138-2.493 18.12-7.475 4.982-4.966 7.47-11.366 7.47-19.2 0-7.816-2.488-14.216-7.47-19.199-4.982-4.966-11.382-7.45-19.195-7.45h-147.144v-152.55c0-7.1-2.673-13.142-8.013-18.125-5.32-4.984-11.889-7.475-19.702-7.475-7.117 0-13.343 2.491-18.683 7.475-5.325 4.983-7.984 11.383-7.984 19.2v151.475h-151.419c-7.097 0-13.136 2.483-18.118 7.45-4.982 4.983-7.473 11.383-7.473 19.199 0 7.834 2.491 14.234 7.473 19.2 4.981 4.982 11.379 7.475 19.193 7.475h150.344v148.275c0 7.101 2.659 13.143 7.984 18.125 5.34 4.982 11.919 7.475 19.732 7.475zM537.4 917.299c-56.863 0-109.998-10.486-159.404-31.462s-92.237-49.597-128.49-85.862c-36.253-36.265-64.863-79.114-85.831-128.538s-31.451-102.579-31.451-159.462c0-56.883 10.484-109.858 31.451-158.925s49.577-91.913 85.831-128.538c36.253-36.625 79.083-65.425 128.49-86.4s102.541-31.462 159.404-31.462c56.863 0 109.814 10.487 158.863 31.462s91.878 49.775 128.492 86.4c36.613 36.625 65.403 79.471 86.369 128.538s31.447 102.042 31.447 158.925c0 56.883-10.481 110.039-31.447 159.462s-49.756 92.273-86.369 128.538c-36.613 36.265-79.442 64.886-128.492 85.862s-102.001 31.462-158.863 31.462z"],"width":1075,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["add"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":827,"id":47,"name":"add","prevSize":32,"code":59741},"setIdx":0,"setId":1,"iconIdx":54},{"icon":{"paths":["M609.9 138.325c4.265 4.266 9.595 6.4 15.99 6.4 5.699 0 10.68-2.134 14.945-6.4s6.4-9.242 6.4-14.925c0-6.4-2.135-11.734-6.4-16s-9.247-6.4-14.945-6.4c-6.395 0-11.725 2.133-15.99 6.4s-6.4 9.6-6.4 16c0 5.683 2.135 10.658 6.4 14.925z","M138.622 612.296c4.265 4.265 9.239 6.4 14.919 6.4 6.398 0 11.729-2.135 15.994-6.4s6.397-9.6 6.397-16c0-6.4-2.132-11.556-6.397-15.462-4.265-3.927-9.596-5.888-15.994-5.888-5.681 0-10.654 1.961-14.919 5.888-4.265 3.907-6.397 9.062-6.397 15.462s2.132 11.735 6.397 16z","M138.622 443.207c4.265 3.926 9.239 5.888 14.919 5.888 6.398 0 11.729-1.962 15.994-5.888 4.265-3.908 6.397-9.062 6.397-15.462s-2.132-11.733-6.397-16c-4.265-4.266-9.596-6.4-15.994-6.4-5.681 0-10.654 2.134-14.919 6.4s-6.397 9.6-6.397 16c0 6.4 2.132 11.554 6.397 15.462z","M254.317 795.208c7.456 7.475 16.515 11.213 27.177 11.213s19.722-3.738 27.177-11.213c7.473-7.46 11.209-16.522 11.209-27.187s-3.736-19.727-11.209-27.187c-7.455-7.475-16.515-11.213-27.177-11.213s-19.722 3.738-27.177 11.213c-7.472 7.46-11.208 16.522-11.208 27.187s3.736 19.727 11.208 27.187z","M254.317 623.483c7.456 7.475 16.515 11.213 27.177 11.213s19.722-3.738 27.177-11.213c7.473-7.46 11.209-16.522 11.209-27.187s-3.736-19.738-11.209-27.213c-7.455-7.46-16.515-11.187-27.177-11.187s-19.722 3.727-27.177 11.187c-7.472 7.475-11.208 16.548-11.208 27.213s3.736 19.727 11.208 27.187z","M254.317 454.958c7.456 7.458 16.515 11.187 27.177 11.187s19.722-3.729 27.177-11.187c7.473-7.475 11.209-16.546 11.209-27.213s-3.736-19.729-11.209-27.187c-7.455-7.475-16.515-11.213-27.177-11.213s-19.722 3.738-27.177 11.213c-7.472 7.458-11.208 16.521-11.208 27.187s3.736 19.738 11.208 27.213z","M254.317 283.207c7.456 7.475 16.515 11.213 27.177 11.213s19.722-3.738 27.177-11.213c7.473-7.458 11.209-16.52 11.209-27.187s-3.736-19.729-11.209-27.187c-7.455-7.475-16.515-11.213-27.177-11.213s-19.722 3.738-27.177 11.213c-7.472 7.458-11.208 16.521-11.208 27.187s3.736 19.729 11.208 27.187z","M425.978 283.207c7.455 7.475 16.515 11.213 27.177 11.213s19.73-3.738 27.203-11.213c7.456-7.458 11.183-16.52 11.183-27.187s-3.727-19.729-11.183-27.187c-7.472-7.475-16.54-11.213-27.203-11.213s-19.722 3.738-27.177 11.213c-7.473 7.458-11.209 16.521-11.209 27.187s3.736 19.729 11.209 27.187z","M437.161 424.545c4.265 4.267 9.596 6.4 15.994 6.4 5.698 0 10.68-2.133 14.945-6.4s6.398-9.6 6.398-16c0-5.683-2.133-10.658-6.398-14.925s-9.247-6.4-14.945-6.4c-6.397 0-11.729 2.134-15.994 6.4s-6.398 9.242-6.398 14.925c0 6.4 2.132 11.734 6.398 16z","M594.442 283.207c7.47 7.475 16.538 11.213 27.203 11.213 10.66 0 19.722-3.738 27.177-11.213 7.47-7.458 11.208-16.52 11.208-27.187s-3.738-19.729-11.208-27.187c-7.455-7.475-16.517-11.213-27.177-11.213-10.665 0-19.732 3.738-27.203 11.213-7.455 7.458-11.187 16.521-11.187 27.187s3.732 19.729 11.187 27.187z","M766.126 795.208c7.455 7.475 16.517 11.213 27.177 11.213 10.665 0 19.722-3.738 27.177-11.213 7.475-7.46 11.208-16.522 11.208-27.187s-3.732-19.727-11.208-27.187c-7.455-7.475-16.512-11.213-27.177-11.213-10.66 0-19.722 3.738-27.177 11.213-7.47 7.46-11.208 16.522-11.208 27.187s3.738 19.727 11.208 27.187z","M766.126 623.483c7.455 7.475 16.517 11.213 27.177 11.213 10.665 0 19.722-3.738 27.177-11.213 7.475-7.46 11.208-16.522 11.208-27.187s-3.732-19.738-11.208-27.213c-7.455-7.46-16.512-11.187-27.177-11.187-10.66 0-19.722 3.727-27.177 11.187-7.47 7.475-11.208 16.548-11.208 27.213s3.738 19.727 11.208 27.187z","M766.126 454.958c7.455 7.458 16.517 11.187 27.177 11.187 10.665 0 19.722-3.729 27.177-11.187 7.475-7.475 11.208-16.546 11.208-27.213s-3.732-19.729-11.208-27.187c-7.455-7.475-16.512-11.213-27.177-11.213-10.66 0-19.722 3.738-27.177 11.213-7.47 7.458-11.208 16.521-11.208 27.187s3.738 19.738 11.208 27.213z","M766.126 283.207c7.455 7.475 16.517 11.213 27.177 11.213 10.665 0 19.722-3.738 27.177-11.213 7.475-7.458 11.208-16.52 11.208-27.187s-3.732-19.729-11.208-27.187c-7.455-7.475-16.512-11.213-27.177-11.213-10.66 0-19.722 3.738-27.177 11.213-7.47 7.458-11.208 16.521-11.208 27.187s3.738 19.729 11.208 27.187z","M905.262 612.296c4.265 4.265 9.595 6.4 15.995 6.4 5.683 0 10.655-2.135 14.92-6.4s6.395-9.6 6.395-16c0-6.4-2.13-11.556-6.395-15.462-4.265-3.927-9.236-5.888-14.92-5.888-6.4 0-11.73 1.961-15.995 5.888-4.265 3.907-6.395 9.062-6.395 15.462s2.13 11.735 6.395 16z","M905.262 443.207c4.265 3.926 9.595 5.888 15.995 5.888 5.683 0 10.655-1.962 14.92-5.888 4.265-3.908 6.395-9.062 6.395-15.462s-2.13-11.733-6.395-16c-4.265-4.266-9.236-6.4-14.92-6.4-6.4 0-11.73 2.134-15.995 6.4s-6.395 9.6-6.395 16c0 6.4 2.13 11.554 6.395 15.462z","M632.289 430.945c-6.4 0-11.73-2.134-15.995-6.4s-6.395-9.6-6.395-16c0-5.683 2.13-10.658 6.395-14.925s9.595-6.4 15.995-6.4c5.699 0 10.68 2.133 14.945 6.4s6.395 9.242 6.395 14.925c0 6.4-2.13 11.733-6.395 16s-9.247 6.4-14.945 6.4z","M445.683 150.42c-6.397 0-11.729-2.134-15.994-6.4s-6.398-9.242-6.398-14.925c0-6.4 2.132-11.734 6.398-16s9.596-6.4 15.994-6.4c5.698 0 10.68 2.134 14.945 6.4s6.397 9.6 6.397 16c0 5.683-2.132 10.658-6.397 14.925s-9.247 6.4-14.945 6.4z","M650.737 589.117c0 63.944-51.82 115.779-115.738 115.779s-115.734-51.835-115.734-115.779c0-63.944 51.816-115.78 115.734-115.78s115.738 51.836 115.738 115.78z","M353.156 870.707v-41.231c0-50.591 37.581-91.607 83.94-91.607h195.858c46.362 0 83.942 41.016 83.942 91.607v41.231c-46.536 53.591-112.374 65.644-185.37 65.644-69.409 0-132.349-16.599-178.372-65.644z"],"width":1075,"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["blur"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":826,"id":48,"name":"blur","prevSize":32,"code":59742},"setIdx":0,"setId":1,"iconIdx":55},{"icon":{"paths":["M537.462 900.301c-53.297 0-103.59-10.122-150.882-30.362-47.257-20.275-88.492-48.010-123.704-83.2-35.178-35.226-62.901-76.477-83.169-123.75-20.233-47.309-30.35-97.623-30.35-150.938 0-54.034 10.117-104.518 30.35-151.451 20.268-46.934 47.991-88.013 83.169-123.238 35.213-35.191 76.447-62.925 123.704-83.2 47.291-20.241 97.585-30.362 150.882-30.362 54.011 0 104.479 10.121 151.393 30.362 46.915 20.275 87.982 48.009 123.192 83.2 35.18 35.226 62.899 76.305 83.169 123.238 20.234 46.934 30.351 97.417 30.351 151.451 0 53.315-10.117 103.629-30.351 150.938-20.27 47.273-47.99 88.525-83.169 123.75-35.21 35.19-76.278 62.925-123.192 83.2-46.915 20.239-97.382 30.362-151.393 30.362zM537.462 844.851c92.401 0 170.947-32.358 235.638-97.075s97.039-143.293 97.039-235.725c0-40.552-6.927-79.122-20.782-115.714-13.85-36.625-33.234-69.53-58.138-98.714l-468.104 468.277c29.173 24.914 62.066 44.303 98.677 58.163 36.577 13.855 75.133 20.787 115.669 20.787zM283.706 726.477l468.099-468.277c-29.169-24.918-62.065-44.305-98.673-58.163-36.577-13.858-75.136-20.787-115.671-20.787-92.399 0-170.945 32.358-235.637 97.075s-97.039 143.292-97.039 235.726c0 40.55 6.926 79.119 20.78 115.712 13.853 36.623 33.233 69.53 58.142 98.714z"],"width":1075,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["remove"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":814,"id":49,"name":"remove","prevSize":32,"code":59743},"setIdx":0,"setId":1,"iconIdx":56},{"icon":{"paths":["M170.667 395.1v-72.533l194.133-193.067h71.467l-265.6 265.6zM170.667 204.167v-62.933l12.8-11.733h61.867l-74.667 74.667zM686.933 266.033c-6.4-6.4-12.983-12.445-19.755-18.133-6.741-5.689-13.312-10.667-19.712-14.933l103.467-103.467h72.533l-136.533 136.533zM236.8 643.635l106.667-106.667c4.267 6.4 8.533 12.442 12.8 18.133 4.267 5.687 8.889 11.021 13.867 16l-30.933 30.933c-16.355 4.267-33.593 10.138-51.712 17.621-18.147 7.45-35.043 15.445-50.688 23.979zM749.867 395.1v-1.067c-0.713-9.956-2.133-19.911-4.267-29.867s-5.333-19.2-9.6-27.733l160-160v72.533l-146.133 146.133zM478.933 208.433l80-78.933h71.467l-74.667 74.667c-4.979-0.711-9.6-1.423-13.867-2.133s-8.887-1.067-13.867-1.067c-7.821 0.711-16 1.593-24.533 2.645-8.533 1.081-16.713 2.688-24.533 4.821zM170.667 588.169v-71.467l148.267-148.268c-2.133 8.533-3.726 16.881-4.779 25.045-1.081 8.192-1.621 16.199-1.621 24.021 0 4.977 0.185 9.602 0.555 13.868 0.341 4.267 0.867 8.533 1.579 12.8l-144 144zM856.533 673.502c-4.979-6.4-10.667-12.262-17.067-17.579-6.4-5.35-13.513-10.513-21.333-15.488l77.867-77.867v72.533l-39.467 38.4zM752 584.969c-1.421-3.558-3.2-6.942-5.333-10.155-2.133-3.187-4.267-6.204-6.4-9.045l-9.045-11.221c-3.213-3.9-6.601-7.27-10.155-10.112l174.933-176.002v73.602l-144 142.933zM533.333 572.169c-41.246 0-76.446-14.579-105.6-43.733-29.155-29.158-43.733-64.358-43.733-105.602 0-41.245 14.578-76.445 43.733-105.6 29.154-29.156 64.354-43.733 105.6-43.733s76.446 14.577 105.6 43.733c29.154 29.155 43.733 64.355 43.733 105.6 0 41.243-14.579 76.443-43.733 105.602-29.154 29.154-64.354 43.733-105.6 43.733zM533.333 508.169c23.467 0 43.563-8.363 60.288-25.088 16.695-16.7 25.045-36.779 25.045-60.247 0-23.467-8.35-43.563-25.045-60.288-16.725-16.697-36.821-25.045-60.288-25.045s-43.55 8.348-60.245 25.045c-16.725 16.725-25.088 36.821-25.088 60.288 0 23.468 8.363 43.547 25.088 60.247 16.695 16.725 36.779 25.088 60.245 25.088zM256 870.835c-23.564 0-42.667-19.106-42.667-42.667v-25.6c0-22.046 5.689-41.788 17.067-59.221 11.378-17.408 26.667-30.737 45.867-39.979 34.845-17.779 74.141-32.542 117.888-44.288 43.721-11.721 90.112-17.579 139.179-17.579s95.475 5.858 139.221 17.579c43.721 11.746 82.999 26.509 117.845 44.288 19.2 9.242 34.487 22.754 45.867 40.533 11.379 17.775 17.067 37.333 17.067 58.667v25.6c0 23.561-19.102 42.667-42.667 42.667h-554.667zM278.4 806.835h509.867c-0.713-12.092-3.029-21.692-6.955-28.8-3.895-7.113-10.825-13.158-20.779-18.133-26.313-12.8-58.483-25.062-96.512-36.779-38.059-11.75-81.621-17.621-130.688-17.621s-92.617 5.871-130.645 17.621c-38.059 11.716-70.243 23.979-96.555 36.779-9.245 4.975-16 11.021-20.267 18.133-4.267 7.108-6.755 16.708-7.467 28.8z","M810.667 832h-554.667c0-85.333 89.6-149.333 277.333-149.333s277.333 64 277.333 149.333z","M640 426.667c0 70.692-47.756 128-106.667 128s-106.667-57.308-106.667-128c0-70.692 47.756-128 106.667-128s106.667 57.308 106.667 128z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["vb"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":815,"id":50,"name":"vb","prevSize":32,"code":59744},"setIdx":0,"setId":1,"iconIdx":57},{"icon":{"paths":["M226.462 832c-21.552 0-39.795-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-485.743c0-21.552 7.467-39.795 22.4-54.729s33.176-22.4 54.729-22.4h571.076c21.551 0 39.795 7.467 54.729 22.4s22.4 33.176 22.4 54.729v485.743c0 21.551-7.467 39.795-22.4 54.729s-33.178 22.4-54.729 22.4h-571.076zM315.077 633.434h111.589c12.361 0 22.836-4.292 31.425-12.881s12.885-19.063 12.885-31.424v-13.129c0-5.308-1.724-9.681-5.171-13.129s-7.821-5.171-13.129-5.171h-14.276c-5.308 0-9.681 1.724-13.129 5.171-3.446 3.447-5.169 7.821-5.169 13.129 0 1.638-0.684 3.145-2.051 4.514-1.367 1.365-2.872 2.048-4.513 2.048h-85.333c-1.641 0-3.145-0.683-4.513-2.048-1.367-1.37-2.051-2.876-2.051-4.514v-128c0-1.643 0.684-3.145 2.051-4.514 1.367-1.365 2.871-2.052 4.513-2.052h85.333c1.641 0 3.146 0.687 4.513 2.052 1.367 1.37 2.051 2.871 2.051 4.514 0 5.303 1.723 9.681 5.169 13.129 3.447 3.443 7.821 5.167 13.129 5.167h14.276c5.308 0 9.681-1.724 13.129-5.167 3.447-3.447 5.171-7.825 5.171-13.129v-13.129c0-12.363-4.297-22.838-12.885-31.426s-19.063-12.882-31.425-12.882h-111.589c-12.363 0-22.838 4.294-31.426 12.882s-12.882 19.063-12.882 31.426v154.257c0 12.361 4.294 22.835 12.882 31.424s19.063 12.881 31.426 12.881zM708.919 390.563h-111.586c-12.365 0-22.839 4.294-31.428 12.882s-12.881 19.063-12.881 31.426v154.257c0 12.361 4.292 22.835 12.881 31.424s19.063 12.881 31.428 12.881h111.586c12.365 0 22.839-4.292 31.428-12.881s12.881-19.063 12.881-31.424v-13.129c0-5.308-1.724-9.681-5.167-13.129-3.447-3.447-7.825-5.171-13.129-5.171h-14.276c-5.308 0-9.685 1.724-13.129 5.171-3.447 3.447-5.171 7.821-5.171 13.129 0 1.638-0.683 3.145-2.052 4.514-1.365 1.365-2.871 2.048-4.51 2.048h-85.333c-1.643 0-3.149-0.683-4.514-2.048-1.37-1.37-2.052-2.876-2.052-4.514v-128c0-1.643 0.683-3.145 2.052-4.514 1.365-1.365 2.871-2.052 4.514-2.052h85.333c1.638 0 3.145 0.687 4.51 2.052 1.37 1.37 2.052 2.871 2.052 4.514 0 5.303 1.724 9.681 5.171 13.129 3.443 3.443 7.821 5.167 13.129 5.167h14.276c5.303 0 9.681-1.724 13.129-5.167 3.443-3.447 5.167-7.825 5.167-13.129v-13.129c0-12.363-4.292-22.838-12.881-31.426s-19.063-12.882-31.428-12.882z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["captions"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":816,"id":51,"name":"captions","prevSize":32,"code":59713},"setIdx":0,"setId":1,"iconIdx":58},{"icon":{"paths":["M917.824 963.443l-145.724-145.719h-514.953c-21.552 0-39.795-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-485.742c0-4.431 0.615-8.479 1.846-12.145s2.941-7.030 5.129-10.093l-101.661-101.661 45.618-45.621 832.492 832.491-45.619 45.619zM905.353 738.953l-133.909-133.909c4.156-3.554 7.275-8.013 9.353-13.372 2.078-5.363 3.119-10.97 3.119-16.819v-29.538h-50.871v18.048h-3.281l-95.181-95.177v-34.462c0-1.643 0.683-3.145 2.052-4.514 1.365-1.37 2.871-2.052 4.51-2.052h85.333c1.643 0 3.149 0.683 4.514 2.052 1.37 1.37 2.052 2.871 2.052 4.514v17.229h50.871v-30.357c0-12.363-4.292-22.838-12.881-31.426s-19.063-12.882-31.428-12.882h-111.586c-11.268 0-20.992 3.679-29.171 11.036-8.175 7.357-12.949 16.533-14.315 27.529v3.282l-240.411-240.41h484.102c21.551 0 39.795 7.467 54.729 22.4s22.4 33.176 22.4 54.729v484.1zM345.763 619.157h111.59c12.361 0 22.835-4.292 31.424-12.881s12.885-19.063 12.885-31.424v-7.881l-21.662-21.658h-29.214v16.41c0 1.638-0.683 3.145-2.048 4.51-1.37 1.37-2.871 2.052-4.514 2.052h-85.333c-1.641 0-3.145-0.683-4.513-2.052-1.368-1.365-2.051-2.871-2.051-4.51v-132.595c0-1.643 0.684-3.146 2.051-4.514s2.871-2.051 4.513-2.051h18.052l-41.025-41.025h-6.564c-7.987 2.188-14.633 6.92-19.939 14.195s-7.959 15.563-7.959 24.862v154.257c0 12.361 4.294 22.835 12.882 31.424s19.063 12.881 31.426 12.881z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["captions-off"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":817,"id":52,"name":"captions-off","prevSize":32,"code":59736},"setIdx":0,"setId":1,"iconIdx":59},{"icon":{"paths":["M511.986 899.594c-5.106 0-10.23-1.178-15.371-3.528-5.142-2.355-9.71-5.53-13.703-9.523l-231.385-231.383c-6.619-6.548-9.928-14.561-9.928-24.028 0-9.472 3.299-17.377 9.898-23.726 6.424-6.344 14.229-9.513 23.416-9.513s16.954 3.169 23.3 9.513l179.694 178.632v-647.304c0-9.067 3.423-17.022 10.27-23.867s14.806-10.267 23.878-10.267c9.068 0 17.024 3.422 23.859 10.267 6.84 6.844 10.255 14.8 10.255 23.867v647.304l179.287-178.632c6.62-6.615 14.52-9.923 23.711-9.923 9.185 0 16.988 3.308 23.414 9.923 6.6 6.62 9.897 14.602 9.897 23.946s-3.308 17.28-9.928 23.808l-231.383 231.383c-3.994 3.994-8.581 7.168-13.757 9.523-5.176 2.35-10.322 3.528-15.425 3.528z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["view-last"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":818,"id":53,"name":"view-last","prevSize":32,"code":59740},"setIdx":0,"setId":1,"iconIdx":60},{"icon":{"paths":["M512 650.172c-5.141 0-9.929-0.819-14.357-2.458-4.433-1.643-8.644-4.459-12.638-8.452l-132.675-132.676c-5.907-5.909-8.929-13.333-9.065-22.276s2.884-16.503 9.065-22.686c6.181-6.182 13.784-9.382 22.81-9.6s16.629 2.761 22.811 8.943l82.050 82.052v-326.403c0-9.080 3.063-16.684 9.19-22.81s13.73-9.189 22.81-9.189c9.079 0 16.683 3.063 22.81 9.189s9.19 13.729 9.19 22.81v326.403l82.052-82.052c5.905-5.909 13.44-8.819 22.605-8.738 9.161 0.081 16.832 3.213 23.014 9.395 6.178 6.182 9.271 13.675 9.271 22.481s-3.093 16.299-9.271 22.481l-132.676 132.676c-3.994 3.994-8.205 6.81-12.638 8.452-4.429 1.638-9.216 2.458-14.357 2.458zM269.13 832c-21.552 0-39.795-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-82.871c0-9.079 3.063-16.683 9.19-22.81s13.729-9.19 22.81-9.19c9.080 0 16.684 3.063 22.81 9.19s9.19 13.73 9.19 22.81v82.871c0 3.281 1.367 6.289 4.102 9.028 2.735 2.735 5.745 4.1 9.027 4.1h485.742c3.281 0 6.289-1.365 9.028-4.1 2.735-2.739 4.1-5.747 4.1-9.028v-82.871c0-9.079 3.063-16.683 9.19-22.81s13.73-9.19 22.81-9.19c9.079 0 16.683 3.063 22.81 9.19s9.19 13.73 9.19 22.81v82.871c0 21.551-7.467 39.795-22.4 54.729s-33.178 22.4-54.729 22.4h-485.742z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["download"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":819,"id":54,"name":"download","prevSize":32,"code":59737},"setIdx":0,"setId":1,"iconIdx":61},{"icon":{"paths":["M390.564 672h242.869c10.927 0 20.087-3.699 27.477-11.089 7.394-7.39 11.089-16.55 11.089-27.477v-242.87c0-10.927-3.695-20.085-11.089-27.476-7.39-7.391-16.55-11.087-27.477-11.087h-242.869c-10.927 0-20.085 3.696-27.476 11.087s-11.087 16.55-11.087 27.476v242.87c0 10.927 3.696 20.087 11.087 27.477s16.55 11.089 27.476 11.089zM512.073 917.333c-56.064 0-108.757-10.641-158.086-31.915-49.329-21.278-92.238-50.155-128.727-86.626s-65.378-79.364-86.663-128.67c-21.286-49.306-31.929-101.99-31.929-158.050 0-56.064 10.639-108.758 31.915-158.086s50.151-92.238 86.624-128.727c36.474-36.49 79.364-65.378 128.671-86.663s101.99-31.929 158.050-31.929c56.064 0 108.757 10.638 158.084 31.915 49.331 21.277 92.241 50.151 128.73 86.624s65.378 79.364 86.66 128.671c21.286 49.306 31.932 101.991 31.932 158.051s-10.641 108.757-31.915 158.084c-21.278 49.331-50.15 92.237-86.626 128.73-36.471 36.489-79.364 65.374-128.67 86.66s-101.99 31.932-158.050 31.932z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["transcript-stop"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":820,"id":55,"name":"transcript-stop","prevSize":32,"code":59738},"setIdx":0,"setId":1,"iconIdx":62},{"icon":{"paths":["M287.922 413.537c20.512 0 38.097-7.156 52.756-21.467s21.989-32.044 21.989-53.199v-178.872c0-21.156-7.312-38.889-21.937-53.2-14.626-14.31-32.207-21.466-52.744-21.466-21.159 0-38.891 7.155-53.195 21.466s-21.457 32.044-21.457 53.2v178.872c0 21.155 7.148 38.889 21.444 53.199s32.009 21.467 53.142 21.467zM776.205 906.667c21.21 0 39.369-7.556 54.473-22.66s22.656-33.259 22.656-54.468v-656.41c0-21.21-7.552-39.367-22.656-54.471s-33.263-22.657-54.473-22.657h-285.538c-9.067 0-16.666 3.068-22.797 9.204-6.135 6.136-9.199 13.739-9.199 22.81s3.063 16.669 9.199 22.796c6.131 6.126 13.73 9.19 22.797 9.19h285.538c3.827 0 6.976 1.231 9.438 3.693s3.691 5.607 3.691 9.437v656.41c0 3.827-1.229 6.972-3.691 9.434s-5.611 3.695-9.438 3.695h-443.074c-3.829 0-6.975-1.233-9.437-3.695s-3.693-5.606-3.693-9.434v-45.952c0-9.067-3.068-16.666-9.204-22.797-6.135-6.135-13.739-9.199-22.81-9.199s-16.669 3.063-22.795 9.199c-6.127 6.131-9.19 13.73-9.19 22.797v45.952c0 21.21 7.552 39.364 22.657 54.468s33.261 22.66 54.471 22.66h443.074zM450.462 751.59h208.41c9.766 0 17.95-3.302 24.555-9.907 6.605-6.609 9.907-14.793 9.907-24.555 0-9.766-3.302-17.95-9.907-24.555s-14.788-9.907-24.555-9.907h-208.41c-9.762 0-17.946 3.302-24.553 9.907-6.605 6.605-9.907 14.788-9.907 24.555 0 9.762 3.302 17.946 9.907 24.555 6.607 6.605 14.79 9.907 24.553 9.907zM533.333 623.59h128c9.067 0 16.666-3.072 22.801-9.207 6.131-6.135 9.199-13.739 9.199-22.81s-3.068-16.666-9.199-22.793c-6.135-6.127-13.734-9.19-22.801-9.19h-128c-9.067 0-16.666 3.068-22.797 9.203-6.135 6.135-9.199 13.739-9.199 22.81s3.063 16.67 9.199 22.797c6.131 6.127 13.73 9.19 22.797 9.19zM288.151 490.667c-38.222 0-71.665-10.94-100.331-32.819-28.666-21.884-47.156-50.026-55.471-84.433-2.462-9.354-6.884-17.449-13.267-24.287s-14.095-10.256-23.137-10.256c-9.042 0-16.627 3.637-22.753 10.912s-8.178 15.59-6.154 24.943c8.041 46.933 29.497 86.564 64.369 118.893s76.403 51.802 124.594 58.419v77.295c0 9.067 3.068 16.666 9.204 22.797 6.136 6.135 13.74 9.203 22.81 9.203s16.67-3.068 22.796-9.203c6.126-6.131 9.189-13.73 9.189-22.797v-77.295c47.48-6.618 88.766-26.091 123.856-58.419 35.093-32.329 56.794-71.686 65.109-118.071 2.022-9.627 0.038-18.147-5.952-25.558-5.986-7.412-13.589-11.118-22.81-11.118-9.216 0-16.994 3.419-23.343 10.256-6.345 6.837-10.748 14.933-13.21 24.287-8.311 34.407-26.952 62.549-55.917 84.433-28.964 21.879-62.159 32.819-99.584 32.819z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["transcript"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":821,"id":56,"name":"transcript","prevSize":32,"code":59739},"setIdx":0,"setId":1,"iconIdx":63},{"icon":{"paths":["M826.701 870.349l-254.935-254.93c-21.33 15.642-44.723 27.909-70.177 36.797-25.452 8.888-52.536 13.332-81.251 13.332-71.136 0-131.593-24.888-181.37-74.665s-74.667-110.223-74.667-181.334c0-71.111 24.889-131.555 74.667-181.333s110.222-74.667 181.333-74.667c71.111 0 131.554 24.889 181.336 74.667 49.777 49.778 74.665 110.235 74.665 181.37 0 28.716-4.444 55.799-13.332 81.251s-21.156 48.845-36.803 70.18l254.935 254.93-54.4 54.4zM420.3 588.749c49.778 0 92.089-17.423 126.936-52.265 34.842-34.845 52.265-77.156 52.265-126.934s-17.423-92.089-52.265-126.933c-34.847-34.845-77.158-52.267-126.936-52.267s-92.089 17.422-126.933 52.267c-34.845 34.844-52.267 77.155-52.267 126.933s17.422 92.089 52.267 126.934c34.844 34.842 77.155 52.265 126.933 52.265z"],"width":1075,"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["search"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":58,"id":57,"name":"search","prevSize":32,"code":59717},"setIdx":0,"setId":1,"iconIdx":64},{"icon":{"paths":["M564.029 905.856c-15.754 0-27.080-5.125-33.971-15.375s-7.629-22.769-2.217-37.555l145.316-383.48c3.994-10.57 11.566-19.424 22.728-26.562 11.162-7.139 22.446-10.708 33.864-10.708 10.778 0 21.514 3.589 32.21 10.766 10.691 7.177 18.012 16.032 21.965 26.566l146.191 383.967c5.484 14.792 4.618 27.218-2.596 37.284-7.219 10.066-18.749 15.099-34.596 15.099-6.968 0-13.665-2.355-20.081-7.055-6.42-4.705-11.008-10.337-13.763-16.891l-33.659-97.408h-192.061l-33.060 96.937c-2.519 6.595-7.276 12.314-14.259 17.157-6.989 4.838-14.326 7.26-22.011 7.26zM658.386 715.581h140.227l-68.925-193.725h-2.381l-68.92 193.725zM320.665 372.935c10.536 19.097 21.731 36.712 33.584 52.846 11.853 16.133 25.766 33.252 41.738 51.359 29.812-31.671 54.013-64.027 72.602-97.066s34.128-68.184 46.616-105.435h-396.265c-11.146 0-20.489-3.774-28.030-11.323-7.54-7.549-11.31-16.903-11.31-28.062s3.774-20.512 11.322-28.061c7.549-7.549 16.903-11.323 28.062-11.323h236.306v-39.385c0-11.159 3.774-20.512 11.323-28.061 7.549-7.549 16.903-11.324 28.062-11.324s20.512 3.774 28.061 11.324c7.549 7.548 11.324 16.902 11.324 28.061v39.385h236.307c11.156 0 20.511 3.774 28.058 11.323 7.552 7.549 11.325 16.902 11.325 28.061s-3.773 20.513-11.325 28.062c-7.547 7.548-16.901 11.323-28.058 11.323h-76.349c-13.153 46.386-31.406 91.549-54.753 135.491-23.347 43.941-52.207 85.259-86.579 123.953l93.128 96.328-29.537 80.901-120.534-120.53-169.107 169.103c-7.548 7.552-16.82 11.325-27.815 11.325s-20.266-4.132-27.814-12.39c-8.261-7.547-12.39-16.819-12.39-27.817 0-10.993 4.13-20.618 12.39-28.882l170.172-170.665c-17.832-20.063-34.174-40.37-49.025-60.922s-28.13-42.142-39.836-64.766c-7.275-13.793-7.385-25.968-0.328-36.525s18.789-15.836 35.2-15.836c6.171 0 12.506 1.933 19.003 5.797s11.322 8.442 14.473 13.73z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["lang-select"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":822,"id":58,"name":"lang-select","prevSize":32,"code":59714},"setIdx":0,"setId":1,"iconIdx":65},{"icon":{"paths":["M511.982 917.315c-55.576 0-108.020-10.655-157.332-31.959-49.313-21.304-92.321-50.309-129.026-87.014s-65.709-79.713-87.014-129.024c-21.306-49.316-31.96-101.76-31.96-157.335 0-56.013 10.653-108.567 31.96-157.661 21.305-49.094 50.31-91.993 87.014-128.697s79.713-65.709 129.026-87.014c49.312-21.306 101.756-31.96 157.332-31.96 56.015 0 108.567 10.653 157.663 31.96 49.091 21.305 91.991 50.31 128.696 87.014s65.71 79.604 87.014 128.697c21.304 49.094 31.959 101.647 31.959 157.661 0 55.575-10.655 108.019-31.959 157.335-21.304 49.311-50.309 92.319-87.014 129.024s-79.606 65.71-128.696 87.014c-49.096 21.304-101.647 31.959-157.663 31.959zM511.982 852.413c21.773-28.882 40.097-58.010 54.976-87.388 14.879-29.373 26.993-61.481 36.347-96.328h-182.646c9.901 35.942 22.154 68.598 36.76 97.971 14.605 29.373 32.793 57.958 54.564 85.745zM429.358 840.678c-16.355-23.465-31.043-50.145-44.062-80.041s-23.139-60.539-30.36-91.94h-144.74c22.537 44.308 52.76 81.536 90.668 111.677 37.908 30.136 80.74 50.243 128.494 60.303zM594.606 840.678c47.754-10.061 90.583-30.167 128.497-60.303 37.903-30.141 68.127-67.369 90.665-111.677h-144.742c-8.586 31.672-19.389 62.459-32.41 92.349-13.020 29.896-27.023 56.438-42.010 79.631zM183.367 604.703h158.606c-2.68-15.867-4.622-31.411-5.825-46.648-1.203-15.232-1.805-30.592-1.805-46.072s0.602-30.837 1.805-46.072c1.203-15.234 3.145-30.783 5.825-46.646h-158.606c-4.102 14.496-7.247 29.566-9.436 45.211s-3.282 31.48-3.282 47.508c0 16.028 1.094 31.864 3.282 47.511 2.188 15.642 5.334 30.71 9.436 45.21zM405.971 604.703h212.023c2.678-15.867 4.623-31.278 5.827-46.239s1.802-30.454 1.802-46.482c0-16.028-0.599-31.522-1.802-46.482-1.203-14.961-3.149-30.373-5.827-46.237h-212.023c-2.68 15.863-4.622 31.276-5.826 46.237-1.203 14.96-1.805 30.454-1.805 46.482s0.602 31.521 1.805 46.482c1.204 14.961 3.146 30.372 5.826 46.239zM681.994 604.703h158.602c4.106-14.5 7.25-29.568 9.436-45.21 2.191-15.647 3.282-31.483 3.282-47.511s-1.091-31.864-3.282-47.508c-2.186-15.645-5.33-30.715-9.436-45.211h-158.602c2.678 15.863 4.618 31.412 5.821 46.646 1.203 15.235 1.807 30.593 1.807 46.072s-0.604 30.84-1.807 46.072c-1.203 15.237-3.144 30.781-5.821 46.648zM669.025 355.266h144.742c-22.81-44.855-52.828-82.080-90.051-111.674-37.228-29.593-80.261-49.832-129.111-60.718 16.353 24.834 30.904 51.993 43.653 81.477 12.744 29.484 22.999 59.789 30.766 90.914zM420.659 355.266h182.646c-9.902-35.665-22.359-68.527-37.376-98.586-15.012-30.058-32.998-58.434-53.947-85.129-20.951 26.695-38.934 55.071-53.948 85.129-15.016 30.058-27.474 62.921-37.375 98.586zM210.196 355.266h144.74c7.768-31.126 18.024-61.43 30.77-90.914s27.296-56.643 43.651-81.477c-49.121 10.886-92.226 31.194-129.314 60.923s-67.037 66.886-89.848 111.468z"],"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["globe"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":60,"id":59,"name":"globe","prevSize":32,"code":59716},"setIdx":0,"setId":1,"iconIdx":66},{"icon":{"paths":["M484.27 661.171v-428.639c0-7.858 2.637-14.445 7.912-19.76s11.811-7.972 19.61-7.972c7.796 0 14.406 2.657 19.818 7.972 5.417 5.316 8.125 11.902 8.125 19.76v429.048l103.388-103.798c5.837-6.18 12.483-9.272 19.937-9.272s14.019 3.062 19.692 9.185c5.903 5.729 8.858 12.237 8.858 19.517 0 7.286-2.954 13.88-8.858 19.789l-146.954 146.954c-6.743 7.004-14.612 10.501-23.603 10.501-8.989 0-16.986-3.497-23.987-10.501l-147.105-147.103c-5.716-5.74-8.351-12.278-7.904-19.615 0.447-7.337 3.488-13.819 9.122-19.456 5.907-6.18 12.512-9.201 19.814-9.068 7.303 0.138 13.907 3.159 19.814 9.068l102.32 103.388z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["down-arrow"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":823,"id":60,"name":"down-arrow","prevSize":32,"code":59711},"setIdx":0,"setId":1,"iconIdx":67},{"icon":{"paths":["M192 853.315c-17.066 0-32-6.4-44.8-19.2s-19.2-27.73-19.2-44.8v-554.664c0-17.067 6.4-32 19.2-44.8s27.734-19.2 44.8-19.2h640c17.065 0 32 6.4 44.8 19.2s19.2 27.733 19.2 44.8v554.664c0 17.070-6.4 32-19.2 44.8s-27.735 19.2-44.8 19.2h-640zM192 789.315h640v-554.664h-640v554.664zM288 638.915h151.466c9.067 0 16.667-3.067 22.8-9.196 6.133-6.134 9.2-13.737 9.2-22.804v-44.8h-53.333v23.47h-108.8v-147.202h108.8v23.467h53.333v-44.8c0-9.067-3.066-16.667-9.2-22.8s-13.733-9.2-22.8-9.2h-151.466c-9.066 0-16.667 3.066-22.8 9.2s-9.2 13.733-9.2 22.8v189.864c0 9.068 3.067 16.671 9.2 22.804 6.133 6.129 13.733 9.196 22.8 9.196zM585.6 638.915h151.465c8.535 0 16-3.2 22.4-9.6s9.6-13.865 9.6-22.4v-44.8h-53.33v23.47h-108.8v-147.202h108.8v23.467h53.33v-44.8c0-8.534-3.2-16-9.6-22.4s-13.865-9.6-22.4-9.6h-151.465c-8.535 0-16 3.2-22.4 9.6s-9.6 13.866-9.6 22.4v189.864c0 8.535 3.2 16 9.6 22.4s13.865 9.6 22.4 9.6z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["caption-mode"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":824,"id":61,"name":"caption-mode","prevSize":32,"code":59708},"setIdx":0,"setId":1,"iconIdx":68},{"icon":{"paths":["M714.634 424.534c-20.623 0-38.216-7.282-52.782-21.846-14.592-14.592-21.888-32.199-21.888-52.822v-179.2c0-21.333 7.296-39.111 21.888-53.333 14.566-14.222 32.159-21.334 52.782-21.334 21.33 0 39.112 7.111 53.33 21.334 14.223 14.222 21.335 32 21.335 53.333v179.2c0 20.622-7.112 38.23-21.335 52.822-14.218 14.563-32 21.846-53.33 21.846zM226.1 917.335c-21.334 0-39.467-7.47-54.4-22.4-14.934-14.935-22.4-33.070-22.4-54.4v-657.069c0-21.333 7.466-39.466 22.4-54.4s33.066-22.4 54.4-22.4h317.864v64h-317.864c-3.556 0-6.571 1.238-9.046 3.712-2.503 2.503-3.754 5.533-3.754 9.088v657.069c0 3.553 1.251 6.584 3.754 9.088 2.474 2.473 5.49 3.712 9.046 3.712h443.734c3.553 0 6.584-1.239 9.088-3.712 2.473-2.504 3.712-5.535 3.712-9.088v-77.87h64v77.87c0 21.33-7.47 39.465-22.4 54.4-14.935 14.93-33.070 22.4-54.4 22.4h-443.734zM309.3 762.665v-69.33h277.334v69.33h-277.334zM309.3 634.665v-64h192v64h-192zM746.634 672h-64v-108.8c-53.335-7.823-98.673-31.647-136.023-71.466-37.318-39.822-55.978-87.111-55.978-141.867h64.001c0 42.667 15.826 78.578 47.488 107.734 31.631 29.155 69.135 43.734 112.512 43.734 44.088 0 81.777-14.578 113.065-43.734s46.935-65.066 46.935-107.734h64c0 54.756-18.488 102.045-55.47 141.867-36.977 39.82-82.488 63.643-136.53 71.466v108.8z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["stt"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":825,"id":62,"name":"stt","prevSize":32,"code":59709},"setIdx":0,"setId":1,"iconIdx":69},{"icon":{"paths":["M309.35 714.629h277.33v-63.995h-277.33v63.995zM309.35 543.964h405.33v-63.997h-405.33v63.997zM309.35 373.298h405.33v-63.997h-405.33v63.997zM226.479 874.629c-21.553 0-39.795-7.465-54.729-22.4-14.933-14.93-22.4-33.172-22.4-54.728v-571.073c0-21.552 7.467-39.795 22.4-54.728s33.176-22.4 54.729-22.4h571.074c21.55 0 39.798 7.466 54.728 22.4 14.935 14.933 22.4 33.176 22.4 54.728v571.073c0 21.555-7.465 39.798-22.4 54.728-14.93 14.935-33.178 22.4-54.728 22.4h-571.074zM226.479 810.634h571.074c3.282 0 6.292-1.367 9.027-4.106 2.734-2.734 4.101-5.745 4.101-9.027v-571.073c0-3.282-1.367-6.291-4.101-9.027s-5.745-4.103-9.027-4.103h-571.074c-3.282 0-6.291 1.368-9.027 4.103s-4.103 5.745-4.103 9.027v571.073c0 3.282 1.368 6.292 4.103 9.027 2.736 2.739 5.745 4.106 9.027 4.106z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["transcript-mode"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":811,"id":63,"name":"transcript-mode","prevSize":32,"code":59710},"setIdx":0,"setId":1,"iconIdx":70},{"icon":{"paths":["M916.178 808.381l-724.878-724.881-36.204 36.204 724.878 724.881 36.204-36.204zM520.996 342.147l-181.969-181.97c13.932-4.385 28.922-6.577 44.973-6.577 40.558 0 74.35 13.999 101.378 41.995 27.062 27.997 40.589 63.001 40.589 105.013 0 14.702-1.659 28.549-4.972 41.539zM624.307 445.457l159.237 159.235c17.167-16.927 25.748-37.212 25.748-60.861v-201.139c0-24.131-8.934-44.612-26.808-61.444-17.869-16.832-39.613-25.248-65.234-25.248s-47.508 8.416-65.664 25.248c-18.186 16.832-27.279 37.314-27.279 61.444v102.765zM242.032 300.609c0-3.318 0.084-6.593 0.253-9.823l156.257 156.257c-4.746 0.417-9.594 0.625-14.542 0.625-40.558 0-74.35-13.999-101.378-41.995-27.060-28.030-40.59-63.052-40.59-105.063zM551.286 599.788c9.718 26.045 26.312 48.922 49.777 68.628 14.618 12.252 30.423 21.775 47.416 28.564l97.377 97.377v49.096c0 7.301-2.826 13.614-8.489 18.949-5.663 5.33-12.37 7.997-20.116 7.997s-14.454-2.668-20.116-7.997c-5.663-5.335-8.489-11.648-8.489-18.949v-80.794c-53.617-5.601-99.036-26.081-136.264-61.445-37.228-35.333-59.118-77.696-65.666-127.089-1.201-8.402 1.029-15.549 6.691-21.448 1.809-1.889 3.816-3.471 6.019-4.751l51.86 51.86zM840.335 661.484l40.73 40.73 1.055-0.998c37.228-35.333 58.819-77.696 64.763-127.089 1.203-8.402-1.029-15.549-6.687-21.448-5.663-5.898-13.256-8.847-22.779-8.847-7.148 0-13.107 2.115-17.884 6.339-4.772 4.204-7.762 9.953-8.965 17.254-5.453 36.541-22.2 67.896-50.232 94.060zM142.698 768c-18.949 0-34.669-6.472-47.16-19.41-12.492-12.974-18.738-28.902-18.738-47.79v-29.394c0-21.007 5.564-40.095 16.694-57.272 11.161-17.142 25.875-30.264 44.143-39.373 39.195-18.888 79.753-33.423 121.673-43.607 41.888-10.153 83.451-15.227 124.69-15.227 3.533 0 7.071 0.036 10.612 0.113 24.093 0.507 40.588 23.060 40.588 47.16 0 83.743 40.209 158.095 102.374 204.8h-394.876z"],"attrs":[{"fill":"rgb(250, 250, 250)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["demote-filled"],"grid":0},"attrs":[{"fill":"rgb(250, 250, 250)"}],"properties":{"order":812,"id":64,"name":"demote-filled","prevSize":32,"code":59666},"setIdx":0,"setId":1,"iconIdx":71},{"icon":{"paths":["M242.255 290.755c-0.17 3.241-0.255 6.525-0.255 9.854 0 42.012 13.53 77.033 40.59 105.063 27.027 27.997 60.82 41.995 101.378 41.995 4.958 0 9.815-0.209 14.571-0.628l-156.284-156.285z","M783.544 604.692l-159.237-159.235v-102.765c0-24.131 9.093-44.612 27.279-61.444 18.156-16.832 40.044-25.248 65.664-25.248s47.365 8.416 65.234 25.248c17.874 16.832 26.808 37.314 26.808 61.444v201.139c0 23.649-8.581 43.935-25.748 60.861z","M551.286 599.788c9.718 26.045 26.312 48.922 49.777 68.628 14.618 12.252 30.423 21.775 47.416 28.564l97.377 97.377v49.096c0 7.301-2.826 13.614-8.489 18.949-5.663 5.33-12.37 7.997-20.116 7.997s-14.454-2.668-20.116-7.997c-5.663-5.335-8.489-11.648-8.489-18.949v-80.794c-53.617-5.601-99.036-26.081-136.264-61.445-37.228-35.333-59.118-77.696-65.666-127.089-1.201-8.402 1.029-15.549 6.691-21.448 1.809-1.889 3.816-3.471 6.019-4.751l51.86 51.86z","M881.065 702.213l-40.73-40.73c28.032-26.163 44.78-57.518 50.232-94.060 1.203-7.301 4.193-13.051 8.965-17.254 4.777-4.224 10.737-6.339 17.884-6.339 9.523 0 17.116 2.949 22.779 8.847 5.658 5.898 7.89 13.046 6.687 21.448-5.944 49.393-27.535 91.756-64.763 127.089-0.348 0.338-0.701 0.666-1.055 0.998z","M520.97 342.121l-181.951-181.952c13.925-4.38 28.908-6.569 44.949-6.569 40.558 0 74.35 13.999 101.378 41.995 27.059 27.997 40.591 63.001 40.591 105.013 0 14.692-1.654 28.53-4.966 41.513z","M472.767 721.577c12.172 15.985 26.406 30.285 42.299 42.496h-370.662c-18.74 0-34.701-6.641-47.882-19.917-13.148-13.307-19.722-29.071-19.722-47.288v-29.389c0-21.007 5.709-40.1 17.126-57.272 11.45-17.142 26.196-30.264 44.237-39.373 40.909-18.888 82.867-33.423 125.873-43.607 42.973-10.153 85.613-15.227 127.92-15.227 2.838 0 5.677 0.020 8.519 0.067 18.754 0.302 31.166 18.514 28.382 37.064-2.504 16.676-16.772 30.259-33.638 30.136l-3.262-0.015c-37.447 0-75.41 4.89-113.889 14.674-38.513 9.81-75.094 22.415-109.745 37.811-7.623 3.492-13.698 8.233-18.225 14.213-4.493 5.949-6.74 13.128-6.74 21.53v29.389h283.354c18.351 0 34.94 10.112 46.058 24.709z","M191.3 83.5l724.877 724.877-36.204 36.204-724.877-724.877 36.204-36.204z"],"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["demote-outlined"],"grid":0},"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"properties":{"order":813,"id":65,"name":"demote-outlined","prevSize":32,"code":59682},"setIdx":0,"setId":1,"iconIdx":72},{"icon":{"paths":["M524.8 204.8c35.346 0 64 28.654 64 64v486.4c0 35.346-28.654 64-64 64s-64-28.654-64-64v-486.4c0-35.346 28.654-64 64-64z","M780.8 358.4c35.346 0 64 28.654 64 64v179.2c0 35.346-28.654 64-64 64s-64-28.654-64-64v-179.2c0-35.346 28.654-64 64-64z","M268.8 358.4c35.346 0 64 28.654 64 64v179.2c0 35.346-28.654 64-64 64s-64-28.654-64-64v-179.2c0-35.346 28.654-64 64-64z"],"attrs":[{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["active-speaker"],"grid":0},"attrs":[{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"}],"properties":{"order":67,"id":66,"name":"active-speaker","prevSize":32,"code":59651},"setIdx":0,"setId":1,"iconIdx":73},{"icon":{"paths":["M491.52 705.536c9.556 0 17.408-3.072 23.552-9.216s9.216-13.996 9.216-23.552c0-9.556-3.072-17.408-9.216-23.552s-13.996-9.216-23.552-9.216c-9.556 0-17.408 3.072-23.552 9.216s-9.216 13.996-9.216 23.552c0 9.556 3.072 17.408 9.216 23.552s13.996 9.216 23.552 9.216zM460.8 556.032h61.44v-245.76h-61.44v245.76zM491.52 901.12c-53.932 0-104.612-10.24-152.044-30.72-47.459-20.48-88.596-48.128-123.412-82.944s-62.464-75.952-82.944-123.412c-20.48-47.432-30.72-98.111-30.72-152.044s10.24-104.625 30.72-152.084c20.48-47.432 48.128-88.556 82.944-123.372s75.953-62.464 123.412-82.944c47.432-20.48 98.111-30.72 152.044-30.72s104.624 10.24 152.084 30.72c47.432 20.48 88.556 48.128 123.372 82.944s62.464 75.94 82.944 123.372c20.48 47.459 30.72 98.152 30.72 152.084s-10.24 104.612-30.72 152.044c-20.48 47.46-48.128 88.596-82.944 123.412s-75.94 62.464-123.372 82.944c-47.46 20.48-98.152 30.72-152.084 30.72z"],"width":983,"attrs":[{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["alert"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"}],"properties":{"order":68,"id":67,"name":"alert","prevSize":32,"code":59684},"setIdx":0,"setId":1,"iconIdx":74},{"icon":{"paths":["M512 625.050q-7.475 0-14.95-2.662t-13.824-10.138l-188.826-187.699q-9.626-10.701-9.062-25.088 0.512-14.387 10.138-24.013 11.725-10.65 24.525-10.138 12.8 0.563 23.45 10.138l168.55 169.626 169.574-168.55q9.626-10.65 22.426-10.65t24.525 10.65q10.701 10.65 10.701 24.525t-10.701 23.501l-187.75 187.699q-6.349 7.475-13.824 10.138t-14.95 2.662z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-down"],"grid":0},"attrs":[{}],"properties":{"order":69,"id":68,"name":"arrow-down","prevSize":32,"code":59648},"setIdx":0,"setId":1,"iconIdx":75},{"icon":{"paths":["M295.475 616.55q-10.701-9.626-10.701-23.501t10.701-24.525l187.75-186.675q6.349-7.475 13.824-10.65t14.95-3.174 14.95 3.174 13.824 10.65l188.826 187.75q9.626 10.65 10.138 23.45t-10.138 23.501q-10.65 10.65-24.013 10.65-13.312 0-24.013-10.65l-169.574-167.475-169.574 168.499q-9.626 10.701-22.938 10.701-13.363 0-24.013-11.725z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-up"],"grid":0},"attrs":[{}],"properties":{"order":70,"id":69,"name":"arrow-up","prevSize":32,"code":59649},"setIdx":0,"setId":1,"iconIdx":76},{"icon":{"paths":["M512 837.333l-325.333-325.333 325.333-325.333 45.867 45.867-248.533 247.467h528v64h-528l248.533 247.467-45.867 45.867z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["back-btn"],"defaultCode":59650,"grid":0},"attrs":[{}],"properties":{"order":71,"id":70,"name":"back-btn","prevSize":32,"code":59650},"setIdx":0,"setId":1,"iconIdx":77},{"icon":{"paths":["M106.6 187.751c0-22.767 7.834-41.967 23.501-57.6 15.633-15.667 34.833-23.501 57.6-23.501h648.498c22.769 0 41.969 7.834 57.6 23.501 15.667 15.633 23.501 34.833 23.501 57.6v477.849c0 22.769-7.834 41.969-23.501 57.6-15.631 15.631-34.831 23.45-57.6 23.45h-578.098l-82.125 81.101c-12.834 12.8-27.597 15.821-44.288 9.062-16.726-6.758-25.088-19.031-25.088-36.813v-612.249zM266.6 386.151h490.699v-68.301h-490.699v68.301zM266.6 535.45h322.149v-68.249h-322.149v68.249z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":72,"id":71,"name":"chat-filled","prevSize":32,"code":59652},"setIdx":0,"setId":1,"iconIdx":78},{"icon":{"paths":["M298.667 586.667h257.067q12.8 0 21.888-9.088 9.045-9.045 9.045-22.912t-9.045-22.955q-9.088-9.045-22.955-9.045h-257.067q-12.8 0-21.845 9.045-9.088 9.088-9.088 22.955t9.088 22.912q9.045 9.088 22.912 9.088zM298.667 458.667h427.733q12.8 0 21.888-9.088 9.045-9.045 9.045-22.912t-9.045-22.955q-9.088-9.045-22.955-9.045h-427.733q-12.8 0-21.845 9.045-9.088 9.088-9.088 22.955t9.088 22.912q9.045 9.088 22.912 9.088zM298.667 330.667h427.733q12.8 0 21.888-9.088 9.045-9.045 9.045-22.912t-9.045-22.912q-9.088-9.088-22.955-9.088h-427.733q-12.8 0-21.845 9.088-9.088 9.045-9.088 22.912t9.088 22.912q9.045 9.088 22.912 9.088zM106.667 804.267v-620.8q0-32 22.4-54.4t54.4-22.4h657.067q32 0 54.4 22.4t22.4 54.4v486.4q0 32-22.4 54.4t-54.4 22.4h-582.4l-86.4 85.333q-18.133 18.133-41.6 8.021-23.467-10.155-23.467-35.755z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat-nav"],"grid":0},"attrs":[{}],"properties":{"order":73,"id":72,"name":"chat-nav","prevSize":32,"code":59655},"setIdx":0,"setId":1,"iconIdx":79},{"icon":{"paths":["M106.6 800v-612.249c0-22.767 7.834-41.967 23.501-57.6 15.633-15.667 34.833-23.501 57.6-23.501h648.498c22.769 0 41.969 7.834 57.6 23.501 15.667 15.633 23.501 34.833 23.501 57.6v477.849c0 22.769-7.834 41.969-23.501 57.6-15.631 15.631-34.831 23.45-57.6 23.45h-578.098l-82.125 81.101c-12.834 12.8-27.597 15.821-44.288 9.062-16.726-6.758-25.088-19.031-25.088-36.813zM174.901 732.774l54.374-54.374h606.923c3.584 0 6.605-1.244 9.062-3.738 2.493-2.493 3.738-5.514 3.738-9.062v-477.849c0-3.584-1.244-6.605-3.738-9.062-2.458-2.492-5.478-3.738-9.062-3.738h-648.498c-3.584 0-6.605 1.246-9.062 3.738-2.492 2.458-3.738 5.478-3.738 9.062v545.023zM174.901 187.751v-12.8 12.8z","M588.749 535.45h-322.149v-68.249h322.149v68.249z","M757.299 386.151h-490.699v-68.301h490.699v68.301z"],"attrs":[{"fill":"rgb(197, 197, 197)"},{"fill":"rgb(197, 197, 197)"},{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"},{"fill":"rgb(197, 197, 197)"},{"fill":"rgb(197, 197, 197)"}],"properties":{"order":74,"id":73,"name":"chat-outlined","prevSize":32,"code":59664},"setIdx":0,"setId":1,"iconIdx":80},{"icon":{"paths":["M456.6 650.65l239.976-238.9-39.475-39.475-200.501 199.476-90.675-89.601-39.475 39.476 130.15 129.024zM512.051 900.25c-53.318 0-103.63-10.122-150.939-30.362-47.274-20.275-88.525-48.010-123.75-83.2-35.191-35.226-62.925-76.477-83.2-123.75-20.241-47.309-30.362-97.623-30.362-150.938 0-54.033 10.121-104.516 30.362-151.45 20.275-46.933 48.009-88.013 83.2-123.238 35.226-35.191 76.476-62.925 123.75-83.2 47.309-20.241 97.622-30.362 150.939-30.362 54.031 0 104.515 10.121 151.45 30.362 46.93 20.275 88.013 48.009 123.238 83.2 35.19 35.226 62.925 76.305 83.2 123.238 20.239 46.934 30.362 97.417 30.362 151.45 0 53.315-10.122 103.629-30.362 150.938-20.275 47.273-48.010 88.525-83.2 123.75-35.226 35.19-76.308 62.925-123.238 83.2-46.935 20.239-97.418 30.362-151.45 30.362z"],"attrs":[{"fill":"rgb(41, 193, 87)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["check"],"defaultCode":59653,"grid":0},"attrs":[{"fill":"rgb(41, 193, 87)"}],"properties":{"order":75,"id":74,"name":"check","prevSize":32,"code":59653},"setIdx":0,"setId":1,"iconIdx":81},{"icon":{"paths":["M213.333 938.667c-23.467 0-43.563-8.35-60.288-25.045-16.697-16.725-25.045-36.821-25.045-60.288v-597.333h85.333v597.333h469.333v85.333h-469.333zM384 768c-23.467 0-43.549-8.35-60.245-25.045-16.725-16.725-25.088-36.821-25.088-60.288v-512c0-23.467 8.363-43.563 25.088-60.288 16.697-16.697 36.779-25.045 60.245-25.045h384c23.467 0 43.563 8.349 60.288 25.045 16.695 16.725 25.045 36.821 25.045 60.288v512c0 23.467-8.35 43.563-25.045 60.288-16.725 16.695-36.821 25.045-60.288 25.045h-384z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["clipboard"],"defaultCode":59654,"grid":0},"attrs":[{}],"properties":{"order":76,"id":75,"name":"clipboard","prevSize":32,"code":59654},"setIdx":0,"setId":1,"iconIdx":82},{"icon":{"paths":["M512 556.8l-216.533 216.533q-8.533 8.533-21.845 9.045-13.355 0.555-22.955-9.045t-9.6-22.4 9.6-22.4l216.533-216.533-216.533-216.533q-8.533-8.533-9.045-21.888-0.555-13.312 9.045-22.912t22.4-9.6 22.4 9.6l216.533 216.533 216.533-216.533q8.533-8.533 21.888-9.088 13.312-0.512 22.912 9.088t9.6 22.4-9.6 22.4l-216.533 216.533 216.533 216.533q8.533 8.533 9.045 21.845 0.555 13.355-9.045 22.955t-22.4 9.6-22.4-9.6z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["close"],"grid":0},"attrs":[{}],"properties":{"order":77,"id":76,"name":"close","prevSize":32,"code":59656},"setIdx":0,"setId":1,"iconIdx":83},{"icon":{"paths":["M42.667 560.213v-53.76h362.246l-130.885-126.72 39.662-38.4 198.31 192-198.31 192-39.662-38.4 130.885-126.72h-362.246zM512 533.333l198.31-192 39.663 38.4-130.884 126.72h362.244v53.76h-362.244l130.884 126.72-39.663 38.4-198.31-192z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["collapse"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":810,"id":77,"name":"collapse","prevSize":32,"code":59667},"setIdx":0,"setId":1,"iconIdx":84},{"icon":{"paths":["M323.091 562.97c27.378-20.864 56.734-37.257 88.067-49.182 31.334-11.921 64.947-17.882 100.843-17.882s69.508 5.961 100.843 17.882c31.334 11.925 60.689 28.318 88.068 49.182l168.832-165.428c-52.932-40.538-109.060-72.431-168.375-95.681-59.319-23.249-122.445-34.874-189.367-34.874s-130.046 11.625-189.365 34.874c-59.319 23.249-115.444 55.143-168.375 95.681l168.832 165.428zM512 810.667c-3.652 0-6.997-0.597-10.039-1.788-3.042-1.195-6.084-3.281-9.126-6.259l-399.72-391.665c-5.475-5.365-8.061-11.625-7.757-18.778s3.194-13.115 8.67-17.885c57.798-50.076 122.137-89.421 193.016-118.036s145.866-42.922 224.957-42.922c79.091 0 154.078 14.307 224.956 42.922 70.882 28.615 135.219 67.96 193.015 118.036 5.478 4.769 8.367 10.731 8.67 17.885 0.307 7.153-2.278 13.413-7.757 18.778l-399.718 391.665c-3.042 2.978-6.084 5.065-9.126 6.259-3.042 1.19-6.387 1.788-10.039 1.788z"],"attrs":[{"fill":"rgb(255, 171, 0)","opacity":0.6}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-bad"],"defaultCode":59657,"grid":0},"attrs":[{"fill":"rgb(255, 171, 0)","opacity":0.6}],"properties":{"order":79,"id":78,"name":"connection-bad","prevSize":32,"code":59657},"setIdx":0,"setId":1,"iconIdx":85},{"icon":{"paths":["M512 810.667c-4.561 0-8.951-0.649-13.175-1.941-4.25-1.293-8-3.878-11.26-7.757l-392.905-388.847c-6.516-7.111-9.617-15.192-9.305-24.242 0.339-9.050 3.766-16.808 10.282-23.273 58.643-49.131 123.475-86.626 194.498-112.485s144.98-38.788 221.865-38.788c76.885 0 150.844 12.929 221.867 38.788s135.855 63.354 194.496 112.485c6.515 6.465 9.946 14.223 10.283 23.273 0.311 9.050-2.79 17.131-9.306 24.242l-392.905 388.847c-3.26 3.878-6.997 6.464-11.221 7.757-4.25 1.293-8.653 1.941-13.214 1.941z"],"attrs":[{"fill":"rgb(41, 193, 87)","opacity":0.6}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-good"],"defaultCode":59658,"grid":0},"attrs":[{"fill":"rgb(41, 193, 87)","opacity":0.6}],"properties":{"order":80,"id":79,"name":"connection-good","prevSize":32,"code":59658},"setIdx":0,"setId":1,"iconIdx":86},{"icon":{"paths":["M490.667 298.667c58.91 0 106.667-47.756 106.667-106.667s-47.757-106.667-106.667-106.667c-58.91 0-106.667 47.756-106.667 106.667s47.757 106.667 106.667 106.667zM341.333 277.333c0 58.91-47.756 106.667-106.667 106.667s-106.667-47.756-106.667-106.667c0-58.91 47.756-106.667 106.667-106.667s106.667 47.756 106.667 106.667zM725.333 341.333c23.565 0 42.667-19.103 42.667-42.667s-19.102-42.667-42.667-42.667c-23.565 0-42.667 19.103-42.667 42.667s19.102 42.667 42.667 42.667zM832 597.333c35.345 0 64-28.655 64-64s-28.655-64-64-64c-35.345 0-64 28.655-64 64s28.655 64 64 64zM810.667 789.333c0 35.345-28.655 64-64 64s-64-28.655-64-64c0-35.345 28.655-64 64-64s64 28.655 64 64zM490.667 938.667c35.345 0 64-28.655 64-64s-28.655-64-64-64c-35.345 0-64 28.655-64 64s28.655 64 64 64zM341.333 768c0 47.13-38.205 85.333-85.333 85.333s-85.333-38.204-85.333-85.333c0-47.13 38.205-85.333 85.333-85.333s85.333 38.204 85.333 85.333zM170.667 640c47.128 0 85.333-38.204 85.333-85.333s-38.205-85.333-85.333-85.333c-47.128 0-85.333 38.204-85.333 85.333s38.205 85.333 85.333 85.333z"],"attrs":[{"fill":"rgb(179, 179, 179)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-loading"],"defaultCode":59659,"grid":0},"attrs":[{"fill":"rgb(179, 179, 179)"}],"properties":{"order":81,"id":80,"name":"connection-loading","prevSize":32,"code":59659},"setIdx":0,"setId":1,"iconIdx":87},{"icon":{"paths":["M511.735 810.667c-4.565 0-8.96-0.649-13.184-1.941-4.254-1.293-8.009-3.878-11.268-7.757l-392.17-387.878c-6.52-7.111-9.78-15.515-9.78-25.212s3.26-17.455 9.78-23.273c58.027-49.777 122.731-87.441 194.11-112.989 71.406-25.522 145.576-38.284 222.512-38.284 76.932 0 151.104 12.929 222.511 38.788 71.377 25.859 136.081 63.354 194.108 112.485 6.519 6.465 9.95 14.223 10.287 23.273 0.316 9.050-2.462 16.808-8.329 23.273l-6.848 5.818c-16.951-18.101-37.645-32.646-62.084-43.636-24.461-10.99-49.732-16.485-75.81-16.485-52.16 0-96.495 18.101-133.005 54.303-36.514 36.2-54.767 80.164-54.767 131.88 0 28.442 5.709 54.455 17.131 78.042 11.396 23.607 26.223 43.17 44.48 58.684l-123.226 121.212c-3.26 3.878-7.002 6.464-11.226 7.757-4.254 1.293-8.661 1.941-13.222 1.941zM785.57 690.423c-7.172 0-13.039-2.415-17.604-7.253-4.565-4.86-6.195-10.202-4.89-16.017 1.301-16.162 5.385-29.739 12.245-40.73 6.831-10.991 18.722-25.212 35.674-42.667 13.692-13.577 22.822-24.243 27.383-32 4.565-7.757 6.848-16.482 6.848-26.18 0-12.928-4.723-24.41-14.161-34.445-9.468-10.005-23.33-15.010-41.583-15.010-11.085 0-21.359 2.419-30.827 7.253-9.442 4.86-17.421 11.494-23.94 19.9-4.565 5.815-9.783 9.694-15.65 11.635-5.867 1.937-11.409 1.617-16.627-0.969-6.519-2.59-10.756-6.788-12.71-12.608-1.958-5.82-1.63-11.315 0.977-16.486 10.432-15.514 24.124-28.288 41.075-38.319 16.951-10.010 36.186-15.014 57.702-15.014 29.99 0 54.438 8.883 73.348 26.65 18.906 17.792 28.361 40.585 28.361 68.382 0 14.869-3.26 27.955-9.779 39.253-6.519 11.328-18.91 26.364-37.163 45.111-11.738 10.991-19.887 20.365-24.452 28.122-4.561 7.757-7.497 17.131-8.802 28.122-1.301 6.464-4.237 11.959-8.802 16.482-4.561 4.527-10.103 6.788-16.623 6.788zM784.593 797.090c-9.131 0-16.785-3.23-22.963-9.698-6.208-6.464-9.314-14.221-9.314-23.27s3.106-16.806 9.314-23.275c6.178-6.464 13.833-9.694 22.963-9.694 9.126 0 16.951 3.23 23.471 9.694 6.519 6.468 9.779 14.225 9.779 23.275s-3.26 16.806-9.779 23.27c-6.519 6.468-14.345 9.698-23.471 9.698z"],"attrs":[{"fill":"rgb(179, 179, 179)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-unpublished"],"defaultCode":59660,"grid":0},"attrs":[{"fill":"rgb(179, 179, 179)"}],"properties":{"order":82,"id":81,"name":"connection-unpublished","prevSize":32,"code":59660},"setIdx":0,"setId":1,"iconIdx":88},{"icon":{"paths":["M512 801.126c-4.561 0-8.951-0.64-13.175-1.911-4.25-1.271-8-3.814-11.26-7.633l-392.905-382.636c-6.516-6.997-9.617-15.102-9.305-24.313 0.339-9.236 3.766-16.718 10.282-22.443 57.991-48.346 122.329-85.242 193.012-110.688 70.71-25.446 145.16-38.168 223.351-38.168s152.627 12.723 223.313 38.168c70.707 25.446 135.057 62.342 193.050 110.688 6.515 5.725 9.946 13.206 10.283 22.443 0.311 9.211-2.79 17.316-9.306 24.313l-92.851 90.651h-254.118c-19.546 0-36.16 6.677-49.843 20.036s-20.527 29.581-20.527 48.666v232.828zM616.58 802.078c-5.214-5.090-7.821-11.601-7.821-19.541 0-7.966 2.607-14.81 7.821-20.535l61.572-60.113-61.572-60.117c-5.214-5.086-7.821-11.601-7.821-19.541 0-7.966 2.607-14.81 7.821-20.535 5.862-5.086 12.873-7.633 21.030-7.633 8.132 0 14.805 2.547 20.019 7.633l61.572 60.117 61.577-60.117c5.867-5.086 12.877-7.633 21.035-7.633 8.132 0 14.801 2.547 20.015 7.633 5.867 5.726 8.798 12.57 8.798 20.535 0 7.94-2.931 14.455-8.798 19.541l-61.572 60.117 61.572 60.113c5.867 5.726 8.798 12.57 8.798 20.535 0 7.94-2.931 14.451-8.798 19.541-5.214 5.726-11.883 8.589-20.015 8.589-8.158 0-15.168-2.863-21.035-8.589l-61.577-60.113-61.572 60.113c-5.214 5.726-11.887 8.589-20.019 8.589-8.158 0-15.168-2.863-21.030-8.589z"],"attrs":[{"fill":"rgb(179, 179, 179)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-unsupported"],"defaultCode":59661,"grid":0},"attrs":[{"fill":"rgb(179, 179, 179)"}],"properties":{"order":83,"id":82,"name":"connection-unsupported","prevSize":32,"code":59661},"setIdx":0,"setId":1,"iconIdx":89},{"icon":{"paths":["M387.886 626.458c17.644-13.709 36.96-24.439 57.951-32.192 20.988-7.748 43.042-11.622 66.163-11.622s45.175 3.874 66.163 11.622c20.992 7.753 40.307 18.483 57.95 32.192l233.63-228.917c-52.932-40.538-109.060-72.431-168.375-95.68-59.319-23.249-122.445-34.874-189.367-34.874s-130.046 11.625-189.365 34.874c-59.319 23.249-115.444 55.143-168.375 95.68l233.626 228.917zM512 810.667c-3.652 0-6.997-0.597-10.039-1.788-3.042-1.195-6.084-3.281-9.126-6.259l-399.72-391.665c-5.475-5.365-8.061-11.625-7.757-18.778s3.194-13.115 8.67-17.885c57.798-50.076 122.137-89.421 193.016-118.036s145.866-42.922 224.957-42.922c79.091 0 154.078 14.307 224.956 42.922 70.882 28.615 135.219 67.96 193.015 118.036 5.478 4.769 8.367 10.731 8.67 17.885 0.307 7.153-2.278 13.413-7.757 18.778l-399.718 391.665c-3.042 2.978-6.084 5.065-9.126 6.259-3.042 1.19-6.387 1.788-10.039 1.788z"],"attrs":[{"fill":"rgb(255, 65, 77)","opacity":0.6}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-very-bad"],"defaultCode":59662,"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)","opacity":0.6}],"properties":{"order":84,"id":83,"name":"connection-very-bad","prevSize":32,"code":59662},"setIdx":0,"setId":1,"iconIdx":90},{"icon":{"paths":["M814.249 946.069c-70.816 102.289-222.025 102.289-292.846 0l-461.498-666.614c-81.771-118.113 2.766-279.455 146.422-279.455l922.998 0c143.658 0 228.196 161.343 146.423 279.456l-461.499 666.613z"],"width":1336,"attrs":[{"fill":"rgb(85, 85, 85)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["downside-triangle"],"defaultCode":59702,"grid":0},"attrs":[{"fill":"rgb(85, 85, 85)"}],"properties":{"order":85,"id":84,"name":"downside-triangle","prevSize":32,"code":59702},"setIdx":0,"setId":1,"iconIdx":91},{"icon":{"paths":["M533.325 341.333c83.913 0 166.571 16.896 247.979 50.688 81.438 33.763 153.801 84.425 217.088 151.979 8.533 8.533 12.8 18.487 12.8 29.867s-4.267 21.333-12.8 29.867l-98.133 96c-7.821 7.821-16.883 12.087-27.179 12.8-10.325 0.713-19.755-2.133-28.288-8.533l-123.733-93.867c-5.687-4.267-9.954-9.246-12.8-14.933s-4.267-12.087-4.267-19.2v-121.6c-27.021-8.533-54.754-15.287-83.2-20.267s-57.6-7.467-87.467-7.467c-29.867 0-59.021 2.487-87.467 7.467-28.444 4.979-56.177 11.733-83.2 20.267v121.6c0 7.113-1.422 13.513-4.267 19.2s-7.111 10.667-12.8 14.933l-123.733 93.867c-8.533 6.4-17.948 9.246-28.245 8.533-10.325-0.713-19.399-4.979-27.221-12.8l-98.133-96c-8.533-8.533-12.8-18.487-12.8-29.867s4.267-21.333 12.8-29.867c62.578-67.554 134.756-118.215 216.533-151.979 81.778-33.792 164.621-50.688 248.533-50.688z"],"width":1067,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["end-call"],"defaultCode":59663,"grid":0},"attrs":[{}],"properties":{"order":86,"id":85,"name":"end-call","prevSize":32,"code":59663},"setIdx":0,"setId":1,"iconIdx":92},{"icon":{"paths":["M42.667 499.823l201.155-201.156 40.231 40.231-132.762 132.761h721.474l-132.762-132.761 40.23-40.231 201.156 201.156-201.156 201.152-40.23-40.23 132.762-132.762h-721.474l132.762 132.762-40.231 40.23-201.155-201.152z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["expand"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":809,"id":86,"name":"expand","prevSize":32,"code":59683},"setIdx":0,"setId":1,"iconIdx":93},{"icon":{"paths":["M170.667 85.333c-47.128 0-85.333 38.205-85.333 85.333v213.333c0 47.13 38.205 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.128-38.204-85.333-85.333-85.333h-213.333z","M170.667 554.667c-47.128 0-85.333 38.204-85.333 85.333v213.333c0 47.13 38.205 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.13-38.204-85.333-85.333-85.333h-213.333z","M554.667 170.667c0-47.128 38.204-85.333 85.333-85.333h213.333c47.13 0 85.333 38.205 85.333 85.333v213.333c0 47.13-38.204 85.333-85.333 85.333h-213.333c-47.13 0-85.333-38.204-85.333-85.333v-213.333z","M640 554.667c-47.13 0-85.333 38.204-85.333 85.333v213.333c0 47.13 38.204 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.13-38.204-85.333-85.333-85.333h-213.333z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["grid"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":808,"id":87,"name":"grid","prevSize":32,"code":59719},"setIdx":0,"setId":1,"iconIdx":94},{"icon":{"paths":["M484.299 706.15h55.502v-245.351h-55.502v245.351zM512.051 396.799c8.53 0 15.647-3.021 21.35-9.062 5.663-6.042 8.499-13.329 8.499-21.862 0-7.816-2.836-14.746-8.499-20.787-5.704-6.075-12.82-9.114-21.35-9.114-8.535 0-15.652 2.85-21.352 8.55-5.666 5.701-8.499 12.817-8.499 21.35s2.833 15.821 8.499 21.862c5.7 6.042 12.817 9.062 21.352 9.062zM512.051 900.25c-53.318 0-103.63-10.122-150.939-30.362-47.274-20.275-88.525-48.010-123.75-83.2-35.191-35.226-62.925-76.477-83.2-123.75-20.241-47.309-30.362-97.623-30.362-150.938 0-54.033 10.121-104.516 30.362-151.45 20.275-46.933 48.009-88.013 83.2-123.238 35.226-35.191 76.476-62.925 123.75-83.2 47.309-20.241 97.622-30.362 150.939-30.362 54.031 0 104.515 10.121 151.45 30.362 46.93 20.275 88.013 48.009 123.238 83.2 35.19 35.226 62.925 76.476 83.2 123.75 20.239 47.309 30.362 97.622 30.362 150.938 0 53.315-10.122 103.629-30.362 150.938-20.275 47.273-48.010 88.525-83.2 123.75-35.226 35.19-76.477 62.925-123.75 83.2-47.309 20.239-97.623 30.362-150.938 30.362zM512.051 844.8c92.431 0 171.008-32.358 235.725-97.075s97.075-143.293 97.075-235.725c0-92.433-32.358-171.008-97.075-235.725s-143.293-97.075-235.725-97.075c-92.434 0-171.010 32.358-235.726 97.075s-97.075 143.292-97.075 235.725c0 92.432 32.358 171.009 97.075 235.725s143.292 97.075 235.726 97.075z"],"attrs":[{"opacity":0.5}],"isMulticolor":false,"isMulticolor2":false,"tags":["info"],"defaultCode":59665,"grid":0},"attrs":[{"opacity":0.5}],"properties":{"order":89,"id":88,"name":"info","prevSize":32,"code":59665},"setIdx":0,"setId":1,"iconIdx":95},{"icon":{"paths":["M739.554 512c-15.642 0-28.442 12.8-28.442 28.446v170.667h-398.224v-398.224h170.665c15.646 0 28.446-12.8 28.446-28.444s-12.8-28.445-28.446-28.445h-170.665c-31.289 0-56.889 25.6-56.889 56.889v398.224c0 31.287 25.6 56.887 56.889 56.887h398.224c31.287 0 56.887-25.6 56.887-56.887v-170.667c0-15.646-12.8-28.446-28.446-28.446z","M753.792 256h-150.473c-12.8 0-19.055 15.36-9.954 24.178l55.181 55.182-221.867 221.867c-2.632 2.633-4.721 5.76-6.146 9.199-1.425 3.443-2.159 7.13-2.159 10.854s0.734 7.411 2.159 10.854c1.425 3.439 3.514 6.566 6.146 9.199 2.637 2.633 5.76 4.723 9.203 6.148 3.439 1.425 7.13 2.159 10.85 2.159 3.725 0 7.415-0.734 10.854-2.159s6.566-3.516 9.199-6.148l221.585-221.582 55.181 55.182c9.101 8.819 24.461 2.56 24.461-10.24v-150.471c0-7.964-6.255-14.222-14.221-14.222z"],"attrs":[{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["link-share"],"defaultCode":59707,"grid":0},"attrs":[{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"}],"properties":{"order":90,"id":89,"name":"link-share","prevSize":32,"code":59707},"setIdx":0,"setId":1,"iconIdx":96},{"icon":{"paths":["M256 85.495c23.564 0 42.667 19.103 42.667 42.667v128c0 23.564-19.103 42.667-42.667 42.667h-128c-23.564 0-42.667-19.102-42.667-42.667v-128c0-23.564 19.103-42.667 42.667-42.667h128zM640 128.161c0-23.564-19.102-42.667-42.667-42.667h-170.667c-23.564 0-42.667 19.103-42.667 42.667v128c0 23.564 19.103 42.667 42.667 42.667h170.667c23.565 0 42.667-19.102 42.667-42.667v-128zM938.667 128.161c0-23.564-19.102-42.667-42.667-42.667h-128c-23.565 0-42.667 19.103-42.667 42.667v128c0 23.564 19.102 42.667 42.667 42.667h128c23.565 0 42.667-19.102 42.667-42.667v-128zM128 384.161c-23.564 0-42.667 19.103-42.667 42.668v469.333c0 23.565 19.103 42.667 42.667 42.667h768c23.565 0 42.667-19.102 42.667-42.667v-469.333c0-23.565-19.102-42.668-42.667-42.668h-768z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["list-view"],"defaultCode":59668,"grid":0},"attrs":[{}],"properties":{"order":91,"id":90,"name":"list-view","prevSize":32,"code":59668},"setIdx":0,"setId":1,"iconIdx":97},{"icon":{"paths":["M800.676 751.514c12.416-33.055 18.621-69.868 18.621-110.423v-381.694c0-14.127-4.413-25.524-13.245-34.191-8.858-8.698-20.485-13.047-34.872-13.047-14.392 0-26.015 4.349-34.877 13.047-8.827 8.667-13.24 20.064-13.24 34.191v212.083c0 4.472-1.48 8.158-4.434 11.058-2.918 2.899-6.676 4.349-11.264 4.349s-8.182-1.45-10.793-4.349c-2.637-2.899-3.958-6.585-3.958-11.058v-324.833c0-13.51-4.572-24.752-13.711-33.728-9.175-9.006-20.628-13.51-34.36-13.51-14.418 0-26.045 4.504-34.872 13.51-8.832 8.976-13.245 20.218-13.245 33.728v324.833c0 4.472-1.475 8.158-4.429 11.058s-6.723 4.349-11.31 4.349c-4.557 0-8.31-1.45-11.264-4.349s-4.434-6.585-4.434-11.058v-374.013c0-14.127-4.255-25.369-12.769-33.728s-19.968-12.538-34.355-12.538c-14.392 0-26.017 4.179-34.878 12.538-8.828 8.359-13.243 19.601-13.243 33.728v329.177l330.934 324.871zM439.296 396.755l-95.247-93.503v-123.849c0-14.127 4.257-25.523 12.772-34.19 8.515-8.698 19.967-13.047 34.357-13.047 14.422 0 26.047 4.349 34.875 13.047 8.829 8.667 13.243 20.064 13.243 34.19v217.353zM533.555 921.6c-64.128 0-121.076-17.193-170.844-51.584-49.737-34.36-86.717-84.316-110.942-149.857l-93.268-239.056c-5.876-14.126-6.363-26.664-1.461-37.614 4.901-10.918 13.573-16.378 26.015-16.378 18.977 0 36.321 6.585 52.030 19.756s26.188 26.834 31.435 40.991l44.16 107.937c1.32 2.591 7.211 6.124 17.673 10.598h15.694v-105.644l-292.849-292.848 54.306-54.306 814.635 814.639-54.303 54.303-154.163-154.158c-48.251 35.482-107.628 53.222-178.12 53.222z"],"attrs":[{"fill":"rgb(9, 157, 253)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["lower-hand"],"grid":0},"attrs":[{"fill":"rgb(9, 157, 253)"}],"properties":{"order":92,"id":91,"name":"lower-hand","prevSize":32,"code":59669},"setIdx":0,"setId":1,"iconIdx":98},{"icon":{"paths":["M735.991 604.804l-48-48c7.113-12.087 12.629-25.6 16.555-40.533 3.895-14.933 5.845-30.579 5.845-46.933h64c0 26.313-3.371 50.658-10.112 73.045-6.771 22.413-16.201 43.221-28.288 62.421zM598.391 466.138l-58.667-58.667v-194.133c0-12.089-4.083-22.229-12.245-30.421-8.192-8.164-18.334-12.245-30.421-12.245s-22.217 4.081-30.379 12.245c-8.192 8.192-12.288 18.332-12.288 30.421v107.733l-64-64v-43.733c0-29.867 10.311-55.111 30.933-75.733 20.621-20.623 45.867-30.933 75.733-30.933s55.113 10.311 75.733 30.933c20.621 20.622 30.933 45.867 30.933 75.733v228.267c0 4.979-0.525 9.417-1.579 13.312-1.079 3.925-2.334 7.667-3.755 11.221zM465.058 885.338v-140.8c-70.4-7.821-128.881-37.858-175.445-90.112-46.592-52.279-69.888-113.975-69.888-185.088h64c0 59.021 20.622 109.325 61.866 150.912 41.245 41.613 91.733 62.421 151.467 62.421 27.021 0 52.621-4.979 76.8-14.933s45.513-23.467 64-40.533l45.867 45.867c-20.621 19.2-44.087 35.2-70.4 48s-54.4 20.621-84.267 23.467v140.8h-64zM847.991 926.938l-776.533-776.533 44.8-44.8 776.533 775.467-44.8 45.867z","M857.617 935.467l-787.2-786.135 45.867-44.8 786.134 786.135-44.8 44.8z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-off"],"defaultCode":59670,"grid":0},"attrs":[{},{}],"properties":{"order":93,"id":92,"name":"mic-off","prevSize":32,"code":59670},"setIdx":0,"setId":1,"iconIdx":99},{"icon":{"paths":["M741.325 609.075l-52.275-51.2c4.982-7.818 9.078-16.538 12.288-26.163 3.21-9.59 5.514-19.712 6.912-30.361 1.434-9.25 5.002-16.538 10.701-21.862 5.668-5.359 12.764-8.038 21.299-8.038 10.685 0 19.574 3.738 26.675 11.213 7.132 7.475 9.984 16.538 8.55 27.187-2.867 18.498-6.963 36.111-12.288 52.838-5.325 16.691-12.611 32.154-21.862 46.387zM605.85 474.676l-215.45-216.525v-40.55c0-30.584 10.838-56.713 32.512-78.387 21.709-21.709 47.855-32.563 78.438-32.563 30.582 0 56.525 10.854 77.824 32.563 21.366 21.674 32.051 47.803 32.051 78.387v225.075c0 5.666-0.358 11.537-1.075 17.613-0.717 6.042-2.15 10.837-4.301 14.387zM817.050 907.725l-717.824-716.799c-6.417-6.383-9.626-14.2-9.626-23.45s3.209-17.425 9.626-24.525c7.1-7.134 15.274-10.701 24.525-10.701s17.066 3.567 23.45 10.701l717.875 716.799c6.385 7.101 9.574 15.089 9.574 23.962 0 8.909-3.19 16.911-9.574 24.013-7.101 6.415-15.273 9.626-24.525 9.626s-17.085-3.21-23.501-9.626zM467.2 851.2v-102.4c-64-7.101-118.221-33.060-162.662-77.875-44.442-44.785-70.57-98.473-78.387-161.075-1.434-10.65 1.399-19.712 8.499-27.187 7.134-7.475 16.026-11.213 26.675-11.213 7.817 0 14.575 2.679 20.275 8.038 5.7 5.325 9.609 12.612 11.725 21.862 7.134 51.199 30.242 94.039 69.325 128.511 39.117 34.473 85.35 51.712 138.701 51.712 25.6 0 49.766-4.608 72.499-13.824 22.769-9.252 43.044-21.709 60.826-37.376l50.125 50.176c-19.901 18.468-42.481 33.572-67.738 45.312s-52.465 19.389-81.613 22.938v102.4c0 9.252-3.379 17.254-10.138 24.013s-14.746 10.138-23.962 10.138c-9.25 0-17.254-3.379-24.013-10.138s-10.138-14.761-10.138-24.013z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-off-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":94,"id":93,"name":"mic-off-filled","prevSize":32,"code":59720},"setIdx":0,"setId":1,"iconIdx":100},{"icon":{"paths":["M741.325 609.075l-52.275-51.2c4.265-7.818 8.192-16.538 11.776-26.163 3.548-9.59 6.026-19.712 7.424-30.361 1.434-9.25 5.002-16.538 10.701-21.862 5.668-5.359 12.764-8.038 21.299-8.038 10.685 0 19.574 3.738 26.675 11.213 7.132 7.475 9.984 16.538 8.55 27.187-2.867 18.498-6.963 36.111-12.288 52.838-5.325 16.691-12.611 32.154-21.862 46.387zM605.85 474.676l-62.925-62.925v-194.15c0-12.083-4.081-22.221-12.237-30.413-8.192-8.158-17.971-12.237-29.338-12.237-12.083 0-22.221 4.079-30.413 12.237-8.192 8.192-12.288 18.33-12.288 30.413v108.8l-68.25-68.25v-40.55c0-30.584 10.838-56.713 32.512-78.387 21.709-21.709 47.855-32.563 78.438-32.563 30.582 0 56.525 10.854 77.824 32.563 21.366 21.674 32.051 47.803 32.051 78.387v225.075c0 5.666-0.358 11.537-1.075 17.613-0.717 6.042-2.15 10.837-4.301 14.387zM817.050 907.725l-717.824-716.799c-6.417-6.383-9.626-14.2-9.626-23.45s3.209-17.425 9.626-24.525c7.1-7.134 15.274-10.701 24.525-10.701s17.066 3.567 23.45 10.701l717.875 716.799c6.385 7.101 9.574 15.089 9.574 23.962 0 8.909-3.19 16.911-9.574 24.013-7.101 6.415-15.273 9.626-24.525 9.626s-17.085-3.21-23.501-9.626zM467.2 851.2v-103.475c-64-7.101-118.221-32.87-162.662-77.312s-70.57-97.961-78.387-160.563c-1.434-10.65 1.229-19.712 7.987-27.187s15.821-11.213 27.187-11.213c7.817 0 14.575 2.679 20.275 8.038 5.7 5.325 9.609 12.612 11.725 21.862 6.417 50.483 29.354 93.148 68.813 127.999 39.458 34.816 85.862 52.224 139.213 52.224 25.6 0 49.956-4.439 73.062-13.312s43.197-21.504 60.262-37.888l50.125 50.176c-19.901 18.468-42.481 33.572-67.738 45.312s-52.465 19.031-81.613 21.862v103.475c0 9.252-3.379 17.254-10.138 24.013s-14.746 10.138-23.962 10.138c-9.25 0-17.254-3.379-24.013-10.138s-10.138-14.761-10.138-24.013z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-off-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":95,"id":94,"name":"mic-off-outlined","prevSize":32,"code":59721},"setIdx":0,"setId":1,"iconIdx":101},{"icon":{"paths":["M512 599.671c-32.367 0-59.725-11.298-82.074-33.899-22.351-22.601-33.526-50.266-33.526-82.995v-280.549c0-32.731 11.175-60.396 33.526-82.996 22.349-22.6 49.707-33.9 82.074-33.9s59.725 11.3 82.074 33.9c22.353 22.6 33.527 50.265 33.527 82.996v280.549c0 32.73-11.174 60.395-33.527 82.995-22.349 22.601-49.707 33.899-82.074 33.899zM512 938.667c-10.018 0-18.295-3.302-24.832-9.911-6.566-6.639-9.847-15.027-9.847-25.156v-119.232c-70.131-7.795-129.657-36.442-178.578-85.943-48.952-49.468-77.282-108.881-84.989-178.24-1.541-10.133 1.156-18.893 8.092-26.278 6.936-7.42 15.799-11.127 26.588-11.127 8.477 0 15.983 3.115 22.519 9.348 6.566 6.238 10.62 14.029 12.161 23.381 7.706 56.887 33.138 104.614 76.295 143.172 43.157 38.592 94.023 57.886 152.591 57.886s109.436-19.294 152.593-57.886c43.153-38.558 68.587-86.285 76.292-143.172 1.545-9.353 5.581-17.143 12.117-23.381 6.566-6.234 14.089-9.348 22.562-9.348 10.79 0 19.652 3.708 26.59 11.127 6.938 7.386 9.634 16.145 8.090 26.278-7.706 69.359-36.019 128.772-84.941 178.24-48.951 49.502-108.493 78.148-178.624 85.943v119.232c0 10.129-3.268 18.517-9.801 25.156-6.566 6.609-14.861 9.911-24.879 9.911z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-on"],"defaultCode":59671,"grid":0},"attrs":[{}],"properties":{"order":96,"id":95,"name":"mic-on","prevSize":32,"code":59671},"setIdx":0,"setId":1,"iconIdx":102},{"icon":{"paths":["M513.075 582.4c-30.582 0-56.712-10.839-78.386-32.512-21.709-21.709-32.563-47.855-32.563-78.438v-254.925c0-30.584 10.854-56.542 32.563-77.875 21.674-21.334 47.804-32 78.386-32 30.587 0 56.545 10.666 77.875 32 21.335 21.333 32 47.291 32 77.875v254.925c0 30.583-10.665 56.729-32 78.438-21.33 21.673-47.288 32.512-77.875 32.512zM513.075 885.35c-9.249 0-17.253-3.379-24.012-10.138s-10.138-14.761-10.138-24.013v-102.4c-64-7.101-118.221-33.060-162.662-77.875-44.442-44.785-70.57-98.473-78.387-161.075-1.434-10.65 1.229-19.712 7.987-27.187s15.821-11.213 27.187-11.213c7.817 0 14.763 2.679 20.838 8.038 6.042 5.325 9.762 12.612 11.162 21.862 7.134 50.483 30.242 93.148 69.325 127.999 39.117 34.816 85.35 52.224 138.7 52.224 52.603 0 98.458-17.408 137.574-52.224 39.117-34.852 62.228-77.517 69.325-127.999 1.434-9.25 5.002-16.538 10.701-21.862 5.704-5.359 12.82-8.038 21.35-8.038 11.366 0 20.429 3.738 27.187 11.213s9.421 16.538 7.987 27.187c-7.096 62.602-32.87 116.29-77.312 161.075-44.472 44.815-98.693 70.774-162.662 77.875v102.4c0 9.252-3.379 17.254-10.138 24.013s-14.761 10.138-24.013 10.138z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-on-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":97,"id":96,"name":"mic-on-filled","prevSize":32,"code":59722},"setIdx":0,"setId":1,"iconIdx":103},{"icon":{"paths":["M513.075 582.4c-30.582 0-56.712-10.839-78.386-32.512-21.709-21.709-32.563-47.855-32.563-78.438v-254.925c0-30.584 10.854-56.542 32.563-77.875 21.674-21.334 47.804-32 78.386-32 30.587 0 56.545 10.666 77.875 32 21.335 21.333 32 47.291 32 77.875v254.925c0 30.583-10.665 56.729-32 78.438-21.33 21.673-47.288 32.512-77.875 32.512zM513.075 885.35c-9.249 0-17.253-3.379-24.012-10.138s-10.138-14.761-10.138-24.013v-103.475c-64-7.101-118.221-32.87-162.662-77.312s-70.57-97.961-78.387-160.563c-1.434-10.65 1.229-19.712 7.987-27.187s15.821-11.213 27.187-11.213c7.817 0 14.763 2.679 20.838 8.038 6.042 5.325 9.762 12.612 11.162 21.862 7.134 50.483 30.242 93.148 69.325 127.999 39.117 34.816 85.35 52.224 138.7 52.224 52.603 0 98.458-17.408 137.574-52.224 39.117-34.852 62.228-77.517 69.325-127.999 1.434-9.25 5.002-16.538 10.701-21.862 5.704-5.359 12.82-8.038 21.35-8.038 11.366 0 20.429 3.738 27.187 11.213s9.421 16.538 7.987 27.187c-7.096 62.602-32.87 116.121-77.312 160.563-44.472 44.442-98.693 70.211-162.662 77.312v103.475c0 9.252-3.379 17.254-10.138 24.013s-14.761 10.138-24.013 10.138zM513.075 514.15c12.083 0 22.036-4.095 29.85-12.287 7.818-8.192 11.725-18.33 11.725-30.413v-254.925c0-12.083-3.907-22.033-11.725-29.85-7.813-7.817-17.766-11.725-29.85-11.725-12.082 0-22.22 3.908-30.412 11.725-8.158 7.816-12.237 17.766-12.237 29.85v254.925c0 12.083 4.079 22.221 12.237 30.413 8.192 8.192 18.33 12.287 30.412 12.287z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-on-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":98,"id":97,"name":"mic-on-outlined","prevSize":32,"code":59723},"setIdx":0,"setId":1,"iconIdx":104},{"icon":{"paths":["M441.617 226.089c0-19.549 6.852-36.159 20.553-49.828 13.67-13.7 30.276-20.55 49.826-20.55s36.16 6.85 49.83 20.55c13.7 13.669 20.548 30.278 20.548 49.828s-6.848 36.175-20.548 49.874c-13.67 13.669-30.281 20.503-49.83 20.503s-36.156-6.834-49.826-20.503c-13.7-13.7-20.553-30.325-20.553-49.874zM441.617 497.045c0-19.55 6.852-36.16 20.553-49.83 13.67-13.7 30.276-20.549 49.826-20.549s36.16 6.848 49.83 20.549c13.7 13.67 20.548 30.281 20.548 49.83s-6.848 36.156-20.548 49.826c-13.67 13.7-30.281 20.553-49.83 20.553s-36.156-6.852-49.826-20.553c-13.7-13.67-20.553-30.276-20.553-49.826zM441.617 768c0-19.55 6.852-36.16 20.553-49.826 13.67-13.7 30.276-20.553 49.826-20.553s36.16 6.852 49.83 20.553c13.7 13.666 20.548 30.276 20.548 49.826s-6.848 36.173-20.548 49.873c-13.67 13.67-30.281 20.506-49.83 20.506s-36.156-6.835-49.826-20.506c-13.7-13.7-20.553-30.323-20.553-49.873z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["more-menu"],"defaultCode":59672,"grid":0},"attrs":[{}],"properties":{"order":99,"id":98,"name":"more-menu","prevSize":32,"code":59672},"setIdx":0,"setId":1,"iconIdx":105},{"icon":{"paths":["M857.604 928.495l-763.508-786.65c-5.842-6.028-8.763-13.563-8.763-22.605s3.286-16.954 9.859-23.735c6.572-6.781 14.43-10.172 23.573-10.172 9.114 0 16.957 3.391 23.529 10.172l762.413 786.649c5.841 6.029 8.764 13.564 8.764 22.605s-3.285 16.951-9.86 23.735c-6.571 6.78-14.238 10.172-23.002 10.172s-16.431-3.392-23.006-10.172zM784.213 628.979l-461.173-474.701h381.207c22.639 0 41.626 7.912 56.96 23.735 15.339 15.823 23.006 35.037 23.006 57.642v206.836l119.398-123.198c6.575-6.781 14.067-8.484 22.481-5.108 8.384 3.406 12.574 10.006 12.574 19.802v318.728c0 9.796-4.19 16.38-12.574 19.759-8.414 3.405-15.906 1.715-22.481-5.065l-119.398-123.196v84.766zM200.352 155.408l582.764 601.289c-0.73 20.343-9.126 38.050-25.195 53.12s-34.325 22.605-54.771 22.605h-498.417c-21.908 0-40.531-7.91-55.866-23.735-15.336-15.821-23.004-35.034-23.004-57.643v-515.389c0-20.344 7.303-38.805 21.908-55.382s32.133-24.866 52.58-24.866z","M298.667 1024c-40.818 0-79.395-7.842-115.733-23.522s-68.071-37.082-95.2-64.213c-27.129-27.127-48.533-58.859-64.213-95.198s-23.52-75.166-23.52-116.48c0-40.819 7.84-79.394 23.52-115.733s37.085-67.947 64.213-94.827c27.129-26.88 58.862-48.162 95.2-63.842s75.165-23.518 116.48-23.518c40.818 0 79.395 7.838 115.733 23.518 36.339 15.68 67.947 36.962 94.827 63.842s48.158 58.487 63.838 94.827c15.68 36.339 23.522 75.166 23.522 116.48 0 40.819-7.842 79.394-23.522 115.733s-36.958 68.070-63.838 95.198c-26.88 27.132-58.487 48.533-94.827 64.213-36.338 15.68-75.165 23.522-116.48 23.522z","M256 533.333v256h85.333v-256h-85.333z","M298.667 938.667c23.564 0 42.667-19.102 42.667-42.667s-19.103-42.667-42.667-42.667c-23.564 0-42.667 19.102-42.667 42.667s19.103 42.667 42.667 42.667z"],"attrs":[{"fill":"rgb(143, 143, 143)"},{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":false,"tags":["no-cam"],"grid":0},"attrs":[{"fill":"rgb(143, 143, 143)"},{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":100,"id":99,"name":"no-cam","prevSize":32,"code":59673,"codes":[59673,59674,59675,59676]},"setIdx":0,"setId":1,"iconIdx":106},{"icon":{"paths":["M742.306 612.459l-51.61-50.795c5.35-8.277 9.937-17.681 13.764-28.22 3.823-10.534 6.498-21.444 8.026-32.734 1.532-9.028 5.551-16.555 12.066-22.575 6.485-6.016 13.931-9.028 22.345-9.028 10.705 0 19.499 3.584 26.381 10.748 6.878 7.134 9.557 15.59 8.026 25.374-3.059 20.318-7.829 39.309-14.315 56.977-6.511 17.698-14.741 34.449-24.683 50.253zM594.351 465.719l-223.656-221.233v-46.279c0-31.605 11.087-58.319 33.262-80.141s49.321-32.734 81.432-32.734c32.115 0 59.26 10.911 81.434 32.734 22.178 21.822 33.263 48.536 33.263 80.141v241.553c0 5.265-0.563 9.963-1.698 14.084-1.161 4.156-2.509 8.115-4.036 11.874zM838.652 929.638l-786.81-774.323c-6.117-6.020-9.175-13.74-9.175-23.162 0-9.391 3.058-17.473 9.175-24.245 6.882-6.772 15.11-10.159 24.683-10.159 9.542 0 17.373 3.386 23.49 10.159l786.808 773.193c6.118 6.771 9.178 14.869 9.178 24.29 0 9.391-3.059 17.472-9.178 24.247-6.878 6.020-15.091 9.028-24.636 9.028-9.57 0-17.417-3.008-23.535-9.028zM450.982 875.456v-115.132c-69.582-7.526-128.658-35.187-177.228-82.987-48.539-47.765-76.632-105.139-84.278-172.109-1.529-9.783 1.147-18.24 8.029-25.374 6.882-7.164 15.675-10.748 26.38-10.748 8.411 0 15.675 3.012 21.792 9.028 6.117 6.020 10.323 13.547 12.617 22.575 7.646 54.933 32.879 101.018 75.699 138.253 42.819 37.261 93.284 55.893 151.396 55.893 29.056 0 56.585-5.265 82.581-15.804 25.997-10.534 48.939-24.832 68.817-42.889l49.318 48.533c-22.174 20.318-47.407 37.052-75.695 50.206-28.292 13.184-58.496 21.658-90.611 25.421v115.132c0 9.783-3.243 17.882-9.728 24.29-6.515 6.383-14.741 9.574-24.683 9.574-9.937 0-18.15-3.191-24.636-9.574-6.515-6.409-9.771-14.507-9.771-24.29z","M298.667 1024c-40.818 0-79.395-7.842-115.733-23.522s-68.071-37.082-95.2-64.213c-27.129-27.127-48.533-58.859-64.213-95.198s-23.52-75.166-23.52-116.48c0-40.819 7.84-79.394 23.52-115.733s37.085-67.947 64.213-94.827c27.129-26.88 58.862-48.162 95.2-63.842s75.165-23.518 116.48-23.518c40.818 0 79.395 7.838 115.733 23.518 36.339 15.68 67.947 36.962 94.827 63.842s48.158 58.487 63.838 94.827c15.68 36.339 23.522 75.166 23.522 116.48 0 40.819-7.842 79.394-23.522 115.733s-36.958 68.070-63.838 95.198c-26.88 27.132-58.487 48.533-94.827 64.213-36.338 15.68-75.165 23.522-116.48 23.522z","M256 533.333v256h85.333v-256h-85.333z","M298.667 938.667c23.564 0 42.667-19.102 42.667-42.667s-19.103-42.667-42.667-42.667c-23.564 0-42.667 19.102-42.667 42.667s19.103 42.667 42.667 42.667z"],"attrs":[{"fill":"rgb(143, 143, 143)"},{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":false,"tags":["no-mic"],"defaultCode":59677,"grid":0},"attrs":[{"fill":"rgb(143, 143, 143)"},{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":101,"id":100,"name":"no-mic","prevSize":32,"codes":[59677,59678,59679,59680],"code":59677},"setIdx":0,"setId":1,"iconIdx":107},{"icon":{"paths":["M-4.25 772.271v-71.467c0-32.713 16.185-58.85 48.555-78.421 32.341-19.541 74.467-29.312 126.379-29.312 7.822 0 15.118 0.354 21.888 1.067l20.779 2.133c-10.667 14.933-18.489 31.117-23.467 48.555-4.978 17.408-7.467 35.712-7.467 54.912v72.533h-186.667zM251.75 772.271v-72.533c0-47.646 24.007-86.217 72.021-115.712 47.986-29.525 110.733-44.288 188.246-44.288 78.221 0 141.154 14.763 188.8 44.288 47.646 29.495 71.467 68.066 71.467 115.712v72.533h-520.534zM841.617 772.271v-72.533c0-19.2-2.658-37.504-7.979-54.912-5.346-17.438-13.001-33.621-22.955-48.555l20.821-2.133c6.741-0.713 14.025-1.067 21.845-1.067 51.913 0 94.050 9.771 126.421 29.312 32.341 19.571 48.512 45.709 48.512 78.421v71.467h-186.667zM170.683 551.471c-24.178 0-44.8-8.533-61.867-25.6s-25.6-37.687-25.6-61.867c0-24.179 8.533-44.985 25.6-62.421 17.067-17.408 37.689-26.112 61.867-26.112s44.8 8.704 61.867 26.112c17.067 17.436 25.6 38.242 25.6 62.421s-8.533 44.8-25.6 61.867c-17.067 17.067-37.689 25.6-61.867 25.6zM853.35 551.471c-24.179 0-44.8-8.533-61.867-25.6s-25.6-37.687-25.6-61.867c0-24.179 8.533-44.985 25.6-62.421 17.067-17.408 37.687-26.112 61.867-26.112s44.8 8.704 61.867 26.112c17.067 17.436 25.6 38.242 25.6 62.421s-8.533 44.8-25.6 61.867c-17.067 17.067-37.687 25.6-61.867 25.6zM512.017 507.738c-36.979 0-68.267-12.8-93.867-38.4s-38.4-56.533-38.4-92.8c0-36.978 12.8-68.267 38.4-93.867s56.888-38.4 93.867-38.4c36.979 0 68.267 12.8 93.867 38.4s38.4 56.889 38.4 93.867c0 36.267-12.8 67.2-38.4 92.8s-56.887 38.4-93.867 38.4z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["participants"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":807,"id":101,"name":"participants","prevSize":32,"code":59686},"setIdx":0,"setId":1,"iconIdx":108},{"icon":{"paths":["M809.574 374.374l-153.6-154.624 49.101-48.026q20.275-20.275 48.538-20.275t48.538 20.275l56.525 56.525q19.2 20.275 19.2 49.613t-20.275 48.538zM196.25 874.65q-15.974 0-28.262-12.237-12.237-12.288-12.237-28.262v-97.075q0-7.475 2.662-14.387 2.662-6.963 9.062-13.363l441.6-441.6 153.6 153.6-441.6 441.6q-6.4 6.4-13.363 9.062-6.912 2.662-14.387 2.662z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["pencil-filled"],"grid":0},"attrs":[{}],"properties":{"order":103,"id":102,"name":"pencil-filled","prevSize":32,"code":59712},"setIdx":0,"setId":1,"iconIdx":109},{"icon":{"paths":["M224 806.4h56.525l385.075-385.075-55.45-56.525-386.15 386.15zM809.574 374.374l-153.6-154.624 49.101-48.026q20.275-20.275 48.538-20.275t48.538 20.275l56.525 56.525q19.2 20.275 19.2 49.613t-20.275 48.538zM196.25 874.65q-15.974 0-28.262-12.237-12.237-12.288-12.237-28.262v-97.075q0-7.475 2.662-14.387 2.662-6.963 9.062-13.363l441.6-441.6 153.6 153.6-441.6 441.6q-6.4 6.4-13.363 9.062-6.912 2.662-14.387 2.662zM637.85 393.626l-27.699-28.826 55.45 56.525z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["pencil-outlined"],"grid":0},"attrs":[{}],"properties":{"order":104,"id":103,"name":"pencil-outlined","prevSize":32,"code":59735},"setIdx":0,"setId":1,"iconIdx":110},{"icon":{"paths":["M136.6 789.35v-73.626c0-16.348 3.908-30.566 11.725-42.65s18.842-22.39 33.075-30.925c34.816-19.932 71.441-35.942 109.875-48.026 38.366-12.083 82.090-18.125 131.174-18.125s92.807 6.042 131.176 18.125c38.431 12.083 75.059 28.093 109.875 48.026 14.234 8.535 25.257 18.842 33.075 30.925 7.813 12.083 11.725 26.301 11.725 42.65v73.626h-571.701zM785.101 789.35v-70.4c0-26.317-5.156-50.499-15.462-72.55-10.312-22.052-23.639-40.192-39.987-54.426 19.896 5.699 39.629 12.646 59.187 20.838 19.558 8.156 38.943 17.935 58.163 29.338 12.083 7.101 21.862 17.577 29.338 31.437 7.439 13.86 11.162 28.979 11.162 45.363v70.4h-102.4zM422.45 499.201c-36.966 0-68.25-12.971-93.85-38.912-25.6-25.976-38.4-57.088-38.4-93.338 0-37.001 12.8-68.301 38.4-93.901s56.883-38.4 93.85-38.4c36.966 0 68.25 12.8 93.851 38.4 25.6 25.6 38.4 56.9 38.4 93.901 0 36.25-12.8 67.362-38.4 93.338-25.602 25.941-56.885 38.912-93.851 38.912zM722.176 366.951c0 36.25-12.8 67.362-38.4 93.338-25.6 25.941-56.883 38.912-93.85 38.912-2.15 0-3.4 0.17-3.738 0.512-0.343 0.375-1.946 0.205-4.813-0.512 15.631-18.501 27.904-38.776 36.813-60.826 8.873-22.016 13.312-45.824 13.312-71.424 0-26.317-4.628-50.33-13.875-72.038-9.216-21.675-21.299-41.762-36.25-60.262h8.55c36.966 0 68.25 12.8 93.85 38.4s38.4 56.9 38.4 93.901z"],"attrs":[{"fill":"rgb(119, 119, 119)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["people"],"grid":0},"attrs":[{"fill":"rgb(119, 119, 119)"}],"properties":{"order":800,"id":104,"name":"people","prevSize":32,"code":59718},"setIdx":0,"setId":1,"iconIdx":111},{"icon":{"paths":["M487.619 499.756c-39.281 0-72.804-14.061-100.571-42.181-27.767-28.092-41.651-61.44-41.651-100.043 0-39.28 13.884-72.804 41.651-100.571s61.291-41.65 100.571-41.65c39.283 0 72.806 13.883 100.571 41.65s41.652 61.291 41.652 100.571c0 38.603-13.887 71.951-41.652 100.043-27.765 28.12-61.289 42.181-100.571 42.181zM182.857 808.58v-90.414c0-19.641 5.418-37.927 16.254-54.857s25.397-30.135 43.683-39.619c39.957-19.641 80.43-34.377 121.417-44.208 40.96-9.811 82.096-14.711 123.408-14.711s82.461 4.901 123.451 14.711c40.96 9.83 81.418 24.566 121.373 44.208 18.286 9.484 32.846 22.689 43.686 39.619 10.835 16.93 16.252 35.216 16.252 54.857v90.414h-609.524z"],"width":975,"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["person"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":106,"id":105,"name":"person","prevSize":32,"code":59681},"setIdx":0,"setId":1,"iconIdx":112},{"icon":{"paths":["M572.252 651.402l0.036 132.762-39.209 39.209-140.291-140.293-156.908 156.908-39.209-0.036-0.036-39.209 156.908-156.908-140.29-140.289 39.208-39.208 132.76 0.036 205.166-205.167-36.204-36.204 39.209-39.208 259.441 259.437-39.209 39.209-36.204-36.204-205.169 205.167z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["pin-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":107,"id":106,"name":"pin-filled","prevSize":32,"code":59724},"setIdx":0,"setId":1,"iconIdx":113},{"icon":{"paths":["M572.252 651.402l0.036 132.762-39.209 39.209-140.291-140.293-156.908 156.908-39.209-0.036-0.036-39.209 156.908-156.908-140.29-140.289 39.208-39.208 132.76 0.036 205.166-205.167-36.204-36.204 39.209-39.208 259.441 259.437-39.209 39.209-36.204-36.204-205.169 205.167zM308.289 520.166l208.171 208.169v-99.558l221.752-221.75-108.616-108.612-221.747 221.751h-99.56z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["pin-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":108,"id":107,"name":"pin-outlined","prevSize":32,"code":59725},"setIdx":0,"setId":1,"iconIdx":114},{"icon":{"paths":["M170.667 85.333c-47.128 0-85.333 38.205-85.333 85.333v213.333c0 47.13 38.205 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.128-38.204-85.333-85.333-85.333h-213.333z","M170.667 554.667c-47.128 0-85.333 38.204-85.333 85.333v213.333c0 47.13 38.205 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.13-38.204-85.333-85.333-85.333h-213.333z","M554.667 170.667c0-47.128 38.204-85.333 85.333-85.333h213.333c47.13 0 85.333 38.205 85.333 85.333v682.667c0 47.13-38.204 85.333-85.333 85.333h-213.333c-47.13 0-85.333-38.204-85.333-85.333v-682.667z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["pinned"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":804,"id":108,"name":"pinned","prevSize":32,"code":59726},"setIdx":0,"setId":1,"iconIdx":115},{"icon":{"paths":["M1024 512c0 282.77-229.23 512-512 512s-512-229.23-512-512c0-282.77 229.23-512 512-512s512 229.23 512 512z","M1024 512c0 282.77-229.23 512-512 512s-512-229.23-512-512c0-282.77 229.23-512 512-512s512 229.23 512 512z","M237.52 772.135v-37.445c0-68.645 55.647-124.293 124.292-124.293h290.016c68.645 0 124.292 55.648 124.292 124.293v26.929c-68.901 72.72-166.393 118.078-274.479 118.078-102.777 0-195.974-41.012-264.121-107.563z","M672.547 392.888c0 91.526-74.197 165.723-165.723 165.723s-165.723-74.197-165.723-165.723c0-91.526 74.197-165.723 165.723-165.723s165.723 74.197 165.723 165.723z"],"attrs":[{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":false,"tags":["profile"],"defaultCode":59703,"grid":0},"attrs":[{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":110,"id":109,"name":"profile","prevSize":32,"codes":[59703,59704,59705,59706],"code":59703},"setIdx":0,"setId":1,"iconIdx":116},{"icon":{"paths":["M282.622 405.672c27.028 27.997 60.82 41.995 101.378 41.995s74.35-13.999 101.378-41.995c27.062-28.030 40.589-63.052 40.589-105.063s-13.527-77.017-40.589-105.013c-27.028-27.997-60.82-41.995-101.378-41.995s-74.35 13.999-101.378 41.995c-27.060 27.997-40.59 63.001-40.59 105.013s13.53 77.033 40.59 105.063z","M95.538 748.59c12.492 12.938 28.212 19.41 47.16 19.41h394.876c-62.165-46.705-102.374-121.057-102.374-204.8 0-24.1-16.496-46.653-40.588-47.16-3.541-0.077-7.078-0.113-10.612-0.113-41.239 0-82.803 5.074-124.69 15.227-41.921 10.184-82.478 24.719-121.673 43.607-18.267 9.108-32.982 22.231-44.143 39.373-11.129 17.178-16.694 36.265-16.694 57.272v29.394c0 18.888 6.246 34.816 18.738 47.79z","M717.251 631.368c-25.62 0-47.508-8.55-65.664-25.651-18.186-17.126-27.279-37.755-27.279-61.885v-201.139c0-24.131 9.093-44.612 27.279-61.444 18.156-16.832 40.044-25.248 65.664-25.248s47.365 8.416 65.234 25.248c17.874 16.832 26.808 37.314 26.808 61.444v201.139c0 24.131-8.934 44.759-26.808 61.885-17.869 17.101-39.613 25.651-65.234 25.651zM717.251 870.4c-7.747 0-14.454-2.668-20.116-7.997-5.663-5.335-8.494-11.648-8.494-18.949v-80.794c-53.612-5.601-99.031-26.081-136.259-61.445-37.228-35.333-59.118-77.696-65.666-127.089-1.201-8.402 1.029-15.549 6.691-21.448s13.253-8.847 22.777-8.847c6.543 0 12.365 2.115 17.454 6.339 5.059 4.204 8.177 9.953 9.349 17.254 5.975 39.828 25.334 73.492 58.076 100.992 32.768 27.469 71.496 41.206 116.188 41.206 44.063 0 82.478-13.737 115.246-41.206 32.768-27.5 52.127-61.164 58.071-100.992 1.203-7.301 4.193-13.051 8.965-17.254 4.777-4.224 10.737-6.339 17.884-6.339 9.523 0 17.116 2.949 22.779 8.847 5.658 5.898 7.89 13.046 6.687 21.448-5.944 49.393-27.535 91.756-64.763 127.089-37.258 35.364-82.678 55.844-136.264 61.445v80.794c0 7.301-2.826 13.614-8.489 18.949-5.663 5.33-12.37 7.997-20.116 7.997z"],"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["promote-filled"],"grid":0},"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"properties":{"order":805,"id":110,"name":"promote-filled","prevSize":32,"code":59727},"setIdx":0,"setId":1,"iconIdx":117},{"icon":{"paths":["M287.952 405.117c27.727 28.366 62.395 42.55 104.003 42.55s76.276-14.183 104.003-42.55c27.761-28.333 41.641-63.17 41.641-104.509 0-42.012-13.88-77.017-41.641-105.013-27.727-27.997-62.395-41.995-104.003-41.995s-76.276 13.999-104.003 41.995c-27.761 27.997-41.641 63.001-41.641 105.013 0 41.339 13.88 76.176 41.641 104.509zM448.626 357.879c-14.879 15.057-33.769 22.585-56.67 22.585s-41.791-7.528-56.67-22.585c-14.912-15.057-22.368-34.147-22.368-57.271 0-23.090 7.456-42.163 22.368-57.221 14.879-15.057 33.769-22.585 56.67-22.585s41.791 7.528 56.67 22.585c14.912 15.057 22.368 34.13 22.368 57.221 0 23.123-7.456 42.213-22.368 57.271z","M515.067 768c-15.893-12.211-30.127-26.511-42.299-42.496-11.118-14.597-27.707-24.704-46.058-24.704h-283.354v-29.394c0-8.402 2.247-15.58 6.74-21.524 4.527-5.985 10.602-10.726 18.225-14.218 34.651-15.396 71.233-27.996 109.745-37.811 38.479-9.779 76.442-14.669 113.889-14.669 1.087 0 2.174 0 3.262 0.010 16.866 0.123 31.134-13.455 33.638-30.136 2.784-18.55-9.628-36.762-28.382-37.059-2.842-0.046-5.682-0.072-8.519-0.072-42.307 0-84.947 5.074-127.92 15.227-43.006 10.184-84.963 24.719-125.873 43.607-18.041 9.108-32.787 22.231-44.237 39.373-11.417 17.178-17.126 36.265-17.126 57.272v29.394c0 18.212 6.574 33.976 19.722 47.288 13.181 13.276 29.142 19.912 47.882 19.912h370.662z","M717.251 631.368c-25.62 0-47.508-8.55-65.664-25.651-18.186-17.126-27.279-37.755-27.279-61.885v-201.139c0-24.131 9.093-44.612 27.279-61.444 18.156-16.832 40.044-25.248 65.664-25.248s47.365 8.416 65.234 25.248c17.874 16.832 26.808 37.314 26.808 61.444v201.139c0 24.131-8.934 44.759-26.808 61.885-17.869 17.101-39.613 25.651-65.234 25.651zM717.251 870.4c-7.747 0-14.454-2.668-20.116-7.997-5.663-5.335-8.494-11.648-8.494-18.949v-81.638c-53.612-5.606-99.031-25.938-136.259-61-37.228-35.067-59.118-77.297-65.666-126.689-1.201-8.402 1.029-15.549 6.691-21.448s13.253-8.847 22.777-8.847c6.543 0 12.365 2.115 17.454 6.339 5.059 4.204 8.177 9.953 9.349 17.254 5.975 39.828 25.334 73.492 58.076 100.992 32.768 27.469 71.496 41.206 116.188 41.206 44.063 0 82.478-13.737 115.246-41.206 32.768-27.5 52.127-61.164 58.071-100.992 1.203-7.301 4.193-13.051 8.965-17.254 4.777-4.224 10.737-6.339 17.884-6.339 9.523 0 17.116 2.949 22.779 8.847 5.658 5.898 7.89 13.046 6.687 21.448-5.944 49.393-27.535 91.622-64.763 126.689-37.258 35.062-82.678 55.393-136.264 61v81.638c0 7.301-2.826 13.614-8.489 18.949-5.663 5.33-12.37 7.997-20.116 7.997zM717.251 577.521c10.122 0 18.458-3.231 25.006-9.697 6.548-6.461 9.82-14.459 9.82-23.992v-201.139c0-9.534-3.272-17.384-9.82-23.551s-14.884-9.251-25.006-9.251c-10.122 0-18.616 3.084-25.477 9.251-6.835 6.167-10.25 14.018-10.25 23.551v201.139c0 9.533 3.415 17.531 10.25 23.992 6.861 6.467 15.355 9.697 25.477 9.697z","M716.8 307.2c28.277 0 51.2 22.923 51.2 51.2v204.8c0 28.277-22.923 51.2-51.2 51.2s-51.2-22.923-51.2-51.2v-204.8c0-28.277 22.923-51.2 51.2-51.2z"],"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["promote-outlined"],"grid":0},"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"properties":{"order":806,"id":111,"name":"promote-outlined","prevSize":32,"code":59728},"setIdx":0,"setId":1,"iconIdx":118},{"icon":{"paths":["M512.491 938.667c43.157 0 82.889-7.561 119.194-22.682 36.275-15.117 67.665-35.443 94.161-60.971 26.466-25.502 47.223-55.262 62.263-89.284 15.040-34.018 22.558-69.926 22.558-107.729v-219.238c0-12.6-4.407-23.159-13.222-31.676-8.841-8.492-19.797-12.739-32.879-12.739-13.077 0-24.034 4.246-32.875 12.739-8.815 8.518-13.222 19.076-13.222 31.676v170.099h-11.772c-39.232 4.412-70.293 18.436-93.18 42.074-22.886 23.612-35.635 51.166-38.251 82.667h-35.311c3.268-39.061 17.655-72.448 43.157-100.169s59.174-45.675 101.026-53.867v-370.439c0-12.6-4.407-23.159-13.222-31.676-8.841-8.492-19.802-12.739-32.879-12.739s-23.868 4.246-32.367 12.739c-8.499 8.518-12.749 19.076-12.749 31.676v274.993h-35.311v-355.318c0-12.6-4.42-22.995-13.261-31.185-8.815-8.19-19.435-12.285-31.859-12.285-13.077 0-24.026 4.095-32.841 12.285-8.841 8.19-13.261 18.585-13.261 31.185v355.318h-34.33v-307.124c0-11.97-4.407-22.365-13.22-31.185-8.841-8.82-19.8-13.23-32.878-13.23s-24.024 4.41-32.839 13.23c-8.841 8.82-13.261 19.215-13.261 31.185v330.748h-35.311v-220.183c0-12.6-4.407-23.146-13.222-31.639-8.841-8.518-19.473-12.777-31.897-12.777-13.078 0-24.024 4.259-32.839 12.777-8.841 8.492-13.261 19.039-13.261 31.639v370.438c0 37.803 7.52 73.711 22.56 107.729 15.040 34.022 35.964 63.782 62.774 89.284 26.81 25.527 58.367 45.854 94.671 60.971 36.277 15.121 75.996 22.682 119.153 22.682z"],"attrs":[{"fill":"rgb(9, 157, 253)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["raise-hand"],"defaultCode":59687,"grid":0},"attrs":[{"fill":"rgb(9, 157, 253)"}],"properties":{"order":113,"id":112,"name":"raise-hand","prevSize":32,"code":59687},"setIdx":0,"setId":1,"iconIdx":119},{"icon":{"paths":["M917.333 512c0-212.077-171.921-384-384-384-212.077 0-384 171.923-384 384 0 212.079 171.923 384 384 384 212.079 0 384-171.921 384-384zM960 512c0 235.639-191.027 426.667-426.667 426.667-235.642 0-426.667-191.027-426.667-426.667 0-235.642 191.025-426.667 426.667-426.667 235.639 0 426.667 191.025 426.667 426.667zM533.333 768c-141.385 0-256-114.615-256-256s114.615-256 256-256c141.385 0 256 114.615 256 256s-114.615 256-256 256z"],"width":1067,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["recording"],"defaultCode":59688,"grid":0},"attrs":[{}],"properties":{"order":114,"id":113,"name":"recording","prevSize":32,"code":59688},"setIdx":0,"setId":1,"iconIdx":120},{"icon":{"paths":["M361.624 701.85l150.427-150.374 150.426 150.374 39.424-39.424-150.374-150.426 150.374-150.426-39.424-39.424-150.426 150.374-150.427-150.374-39.424 39.424 150.374 150.426-150.374 150.426 39.424 39.424zM512.051 900.25c-53.318 0-103.63-10.122-150.939-30.362-47.274-20.275-88.525-48.010-123.75-83.2-35.191-35.226-62.925-76.477-83.2-123.75-20.241-47.309-30.362-97.623-30.362-150.938 0-54.033 10.121-104.516 30.362-151.45 20.275-46.933 48.009-88.013 83.2-123.238 35.226-35.191 76.476-62.925 123.75-83.2 47.309-20.241 97.622-30.362 150.939-30.362 54.031 0 104.515 10.121 151.45 30.362 46.93 20.275 88.013 48.009 123.238 83.2 35.19 35.226 62.925 76.305 83.2 123.238 20.239 46.934 30.362 97.417 30.362 151.45 0 53.315-10.122 103.629-30.362 150.938-20.275 47.273-48.010 88.525-83.2 123.75-35.226 35.19-76.308 62.925-123.238 83.2-46.935 20.239-97.418 30.362-151.45 30.362z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["remove"],"defaultCode":59689,"grid":0},"attrs":[{}],"properties":{"order":115,"id":114,"name":"remove1","prevSize":32,"code":59689},"setIdx":0,"setId":1,"iconIdx":121},{"icon":{"paths":["M512.051 900.25c-53.318 0-103.63-10.122-150.939-30.362-47.274-20.275-88.525-48.010-123.75-83.2-35.191-35.226-62.925-76.477-83.2-123.75-20.241-47.309-30.362-97.623-30.362-150.938 0-54.033 10.121-104.516 30.362-151.45 20.275-46.933 48.009-88.013 83.2-123.238 35.226-35.191 76.476-62.925 123.75-83.2 47.309-20.241 97.622-30.362 150.939-30.362 54.031 0 104.515 10.121 151.45 30.362 46.93 20.275 88.013 48.009 123.238 83.2 35.19 35.226 62.925 76.305 83.2 123.238 20.239 46.934 30.362 97.417 30.362 151.45 0 53.315-10.122 103.629-30.362 150.938-20.275 47.273-48.010 88.525-83.2 123.75-35.226 35.19-76.308 62.925-123.238 83.2-46.935 20.239-97.418 30.362-151.45 30.362zM512.051 844.8c92.431 0 171.008-32.358 235.725-97.075s97.075-143.293 97.075-235.725c0-40.55-6.932-79.121-20.787-115.712-13.86-36.625-33.249-69.53-58.163-98.714l-468.277 468.276c29.184 24.919 62.089 44.303 98.714 58.163 36.591 13.86 75.162 20.787 115.714 20.787zM258.2 726.426l468.277-468.276c-29.184-24.917-62.090-44.305-98.714-58.163-36.593-13.858-75.162-20.787-115.712-20.787-92.434 0-171.010 32.358-235.726 97.075s-97.075 143.292-97.075 235.725c0 40.551 6.929 79.12 20.787 115.713 13.858 36.623 33.246 69.53 58.163 98.714z"],"attrs":[{"fill":"rgb(231, 65, 76)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["remove-meeting"],"grid":0},"attrs":[{"fill":"rgb(231, 65, 76)"}],"properties":{"order":116,"id":115,"name":"remove-meeting","prevSize":32,"code":59715},"setIdx":0,"setId":1,"iconIdx":122},{"icon":{"paths":["M490.667 682.667h85.333v-177.067l68.267 67.2 60.8-60.8-171.733-170.667-170.667 170.667 60.8 59.733 67.2-67.2v178.133zM192 853.333c-23.467 0-43.549-8.35-60.245-25.045-16.725-16.725-25.088-36.821-25.088-60.288v-512c0-23.467 8.363-43.549 25.088-60.245 16.697-16.725 36.779-25.088 60.245-25.088h682.667c23.467 0 43.563 8.363 60.288 25.088 16.695 16.697 25.045 36.779 25.045 60.245v512c0 23.467-8.35 43.563-25.045 60.288-16.725 16.695-36.821 25.045-60.288 25.045h-682.667z"],"width":1067,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["screen-share"],"defaultCode":59690,"grid":0},"attrs":[{}],"properties":{"order":117,"id":116,"name":"screen-share","prevSize":32,"code":59690},"setIdx":0,"setId":1,"iconIdx":123},{"icon":{"paths":["M190.073 849.715c-14.605 5.858-28.594 4.604-41.966-3.767-13.405-8.367-20.107-20.924-20.107-37.662v-177.020c0-10.044 3.051-19.247 9.153-27.618 6.069-8.371 14.378-13.811 24.926-16.32l303.060-75.328-303.060-75.328c-10.548-2.509-18.857-7.731-24.926-15.667-6.102-7.968-9.153-17.392-9.153-28.273v-177.018c0-16.739 6.702-29.294 20.107-37.663 13.372-8.37 27.361-9.625 41.966-3.767l679.15 295.031c17.852 8.367 26.778 22.596 26.778 42.684s-8.926 34.317-26.778 42.684l-679.15 295.031z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["send"],"defaultCode":59691,"grid":0},"attrs":[{}],"properties":{"order":118,"id":117,"name":"send","prevSize":32,"code":59691},"setIdx":0,"setId":1,"iconIdx":124},{"icon":{"paths":["M599.983 938.667h-134.409c-9.626 0-18.142-3.366-25.549-10.103s-11.849-15.347-13.329-25.826l-13.33-101.052c-11.849-3.742-23.875-9.357-36.079-16.845-12.234-7.484-23.535-15.343-33.902-23.578l-92.197 40.422c-9.627 3.742-19.061 4.117-28.303 1.122-9.271-2.995-16.87-8.981-22.794-17.967l-66.648-117.892c-5.184-8.981-6.665-18.534-4.443-28.655 2.222-10.091 7.035-18.129 14.441-24.119l81.089-61.751c-1.481-6.737-2.399-13.474-2.755-20.211-0.385-6.737-0.577-13.474-0.577-20.211 0-5.99 0.192-12.352 0.577-19.089 0.356-6.737 1.274-13.845 2.755-21.333l-81.089-61.753c-7.405-5.988-12.219-14.028-14.441-24.118-2.222-10.12-0.741-19.671 4.443-28.654l66.648-116.772c5.184-8.983 12.589-14.971 22.216-17.965s18.884-2.62 27.77 1.123l93.308 39.298c10.368-8.234 21.668-15.899 33.902-22.995 12.204-7.126 24.23-12.935 36.079-17.426l13.33-101.053c1.481-10.48 5.922-19.088 13.329-25.825s15.923-10.105 25.549-10.105h134.409c10.368 0 19.251 3.369 26.658 10.105s11.849 15.345 13.329 25.825l13.329 101.053c13.329 5.239 25.357 11.033 36.079 17.381 10.752 6.378 21.683 14.058 32.794 23.040l94.417-39.298c8.887-3.743 17.967-4.117 27.238-1.123 9.242 2.994 16.823 8.982 22.75 17.965l66.645 116.772c5.184 8.983 6.665 18.534 4.446 28.654-2.223 10.090-7.036 18.129-14.443 24.118l-82.197 62.875c1.481 7.488 2.219 14.225 2.219 20.211v19.089c0 5.99-0.192 12.156-0.576 18.505-0.354 6.379-1.276 13.683-2.756 21.918l81.092 61.751c8.145 5.99 13.329 14.029 15.548 24.119 2.223 10.121 0.371 19.674-5.551 28.655l-66.65 116.77c-5.184 8.981-12.591 14.972-22.217 17.967-9.626 2.991-19.255 2.62-28.881-1.122l-91.085-39.3c-11.11 8.981-22.396 16.841-33.86 23.578-11.49 6.737-23.164 12.352-35.012 16.845l-13.329 101.052c-1.481 10.479-5.922 19.089-13.329 25.826s-16.29 10.103-26.658 10.103zM533.333 646.737c37.026 0 68.501-13.099 94.417-39.3 25.92-26.197 38.878-58.010 38.878-95.437s-12.958-69.239-38.878-95.439c-25.916-26.199-57.391-39.298-94.417-39.298s-68.501 13.099-94.417 39.298c-25.92 26.199-38.88 58.011-38.88 95.439s12.959 69.239 38.88 95.437c25.916 26.202 57.391 39.3 94.417 39.3z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["settings"],"defaultCode":59692,"grid":0},"attrs":[{}],"properties":{"order":802,"id":118,"name":"settings","prevSize":32,"code":59692},"setIdx":0,"setId":1,"iconIdx":125},{"icon":{"paths":["M775.313 938.667c-33.643 0-62.17-11.669-85.577-35.008s-35.106-51.785-35.106-85.333c0-5.107 0.363-10.765 1.097-16.981 0.73-6.182 2.193-11.465 4.386-15.838l-324.753-190.362c-11.703 11.669-24.869 20.787-39.497 27.349-14.629 6.566-30.354 9.847-47.177 9.847-33.646 0-62.171-11.669-85.577-35.008s-35.109-51.785-35.109-85.333c0-33.549 11.703-61.995 35.109-85.333s51.931-35.008 85.577-35.008c16.823 0 32.549 3.282 47.177 9.846s27.794 15.681 39.497 27.351l324.753-190.36c-2.193-4.376-3.657-9.671-4.386-15.885-0.734-6.185-1.097-11.83-1.097-16.936 0-33.55 11.699-61.994 35.106-85.333s51.934-35.008 85.577-35.008c33.647 0 62.174 11.669 85.577 35.008 23.407 23.339 35.11 51.784 35.11 85.333s-11.703 61.994-35.11 85.333c-23.403 23.339-51.93 35.009-85.577 35.009-16.823 0-32.546-3.282-47.177-9.846-14.626-6.564-27.793-15.681-39.497-27.351l-324.754 190.36c2.194 4.373 3.657 9.481 4.389 15.313 0.731 5.837 1.097 11.669 1.097 17.506 0 5.107-0.366 10.752-1.097 16.934-0.731 6.217-2.194 11.511-4.389 15.885l324.754 190.362c11.703-11.669 24.87-20.787 39.497-27.354 14.63-6.562 30.353-9.843 47.177-9.843 33.647 0 62.174 11.669 85.577 35.008 23.407 23.339 35.11 51.785 35.11 85.333s-11.703 61.995-35.11 85.333c-23.403 23.339-51.93 35.008-85.577 35.008z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["share"],"defaultCode":59693,"grid":0},"attrs":[{}],"properties":{"order":120,"id":119,"name":"share","prevSize":32,"code":59693},"setIdx":0,"setId":1,"iconIdx":126},{"icon":{"paths":["M599.532 857.62v-66.135c61.158-19.2 110.582-54.584 148.27-106.153 37.688-51.543 56.53-109.665 56.53-174.378 0-64.711-18.842-122.851-56.53-174.421-37.688-51.542-87.112-86.912-148.27-106.112v-66.134c78.935 21.334 143.462 64 193.582 128 50.145 64 75.218 136.889 75.218 218.666s-25.073 154.666-75.218 218.666c-50.12 64-114.647 106.665-193.582 128zM155.8 618.685v-213.331h158.934l182.4-183.466v580.268l-182.4-183.47h-158.934zM599.532 667.756v-313.602c28.447 15.645 50.488 37.689 66.135 66.134s23.465 59.022 23.465 91.733c0 32.712-7.818 62.935-23.465 90.665-15.647 27.735-37.688 49.423-66.135 65.070z"],"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["speaker"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":121,"id":120,"name":"speaker","prevSize":32,"code":59685},"setIdx":0,"setId":1,"iconIdx":127},{"icon":{"paths":["M937.685 502.549c0 90.628-28.373 174.511-76.565 242.931l-29.623-29.628c41.126-60.582 65.216-134.076 65.216-213.303 0-207.401-165.090-375.532-368.742-375.532-80.205 0-154.432 26.079-214.947 70.365l-29.627-29.627c68.26-51.802 152.9-82.464 244.574-82.464 226.278 0 409.715 186.813 409.715 417.258z","M754.79 850.086c-64.947 44.049-142.938 69.717-226.82 69.717-226.279 0-409.715-186.812-409.715-417.254 0-82.7 23.624-159.78 64.374-224.623l-115.962-115.963 45.255-45.255 784.416 784.416-45.252 45.257-96.294-96.294zM724.838 820.139l-91.345-91.349c-31.945 15.462-67.686 24.115-105.408 24.115-135.769 0-245.83-112.085-245.83-250.351 0-36.796 7.795-71.74 21.794-103.208l-91.47-91.47c-33.857 56.768-53.353 123.404-53.353 194.674 0 207.398 165.092 375.531 368.744 375.531 72.388 0 139.908-21.244 196.868-57.941z","M773.914 502.554c0 44.983-11.648 87.194-32.043 123.678l-339.105-339.106c36.697-22.188 79.547-34.927 125.32-34.927 135.765 0 245.828 112.087 245.828 250.356z"],"attrs":[{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["stop-recording"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"}],"properties":{"order":122,"id":121,"name":"stop-recording","prevSize":32,"code":59694},"setIdx":0,"setId":1,"iconIdx":128},{"icon":{"paths":["M359.467 664.533c6.4 6.4 13.867 9.6 22.4 9.6s16-3.2 22.4-9.6l107.733-107.733 108.8 108.8c5.687 5.687 12.8 8.533 21.333 8.533s16-3.2 22.4-9.6c6.4-6.4 9.6-13.867 9.6-22.4s-3.2-16-9.6-22.4l-107.733-107.733 108.8-108.8c5.687-5.689 8.533-12.8 8.533-21.333s-3.2-16-9.6-22.4c-6.4-6.4-13.867-9.6-22.4-9.6s-16 3.2-22.4 9.6l-107.733 107.733-108.8-108.8c-5.689-5.689-12.8-8.533-21.333-8.533s-16 3.2-22.4 9.6c-6.4 6.4-9.6 13.867-9.6 22.4s3.2 16 9.6 22.4l107.733 107.733-108.8 108.8c-5.689 5.687-8.533 12.8-8.533 21.333s3.2 16 9.6 22.4zM149.333 853.333c-17.067 0-32-6.4-44.8-19.2s-19.2-27.733-19.2-44.8v-554.667c0-17.067 6.4-32 19.2-44.8s27.733-19.2 44.8-19.2h725.333c17.779 0 32.887 6.4 45.333 19.2s18.667 27.733 18.667 44.8v554.667c0 17.067-6.221 32-18.667 44.8s-27.554 19.2-45.333 19.2h-725.333z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["stop-screen-share"],"defaultCode":59695,"grid":0},"attrs":[{}],"properties":{"order":123,"id":122,"name":"stop-screen-share","prevSize":32,"code":59695},"setIdx":0,"setId":1,"iconIdx":129},{"icon":{"paths":["M257.123 586.543l112.28 112.939c5.989 6.775 13.669 10.167 23.040 10.167 9.341 0 17.381-3.392 24.118-10.167s10.105-14.878 10.105-24.303c0-9.399-3.369-17.109-10.105-23.13l-58.386-58.731h307.651l-58.389 58.731c-6.737 6.020-10.103 13.73-10.103 23.13 0 9.425 3.366 17.527 10.103 24.303s14.793 10.167 24.166 10.167c9.34 0 17.007-3.392 22.993-10.167l112.282-112.939c7.484-7.531 11.226-16.943 11.226-28.237s-3.742-21.082-11.226-29.363l-112.282-111.814c-5.986-6.776-13.653-10.164-22.993-10.164-9.374 0-17.429 3.388-24.166 10.164s-10.103 14.683-10.103 23.72c0 9.033 2.995 16.939 8.981 23.714l60.634 60.988h-309.896l60.631-60.988c5.988-6.775 8.983-14.682 8.983-23.714 0-9.037-3.369-16.943-10.105-23.72s-14.776-10.164-24.118-10.164c-9.372 0-17.052 3.388-23.040 10.164l-112.28 111.814c-7.485 8.282-11.228 18.069-11.228 29.363s3.743 20.706 11.228 28.237zM166.176 896c-22.456 0-41.544-7.906-57.263-23.718s-23.579-35.012-23.579-57.6v-515.011c0-22.588 7.86-41.788 23.579-57.6s34.807-23.718 57.263-23.718h136.982l59.509-64.377c7.485-8.282 16.468-14.682 26.947-19.2s21.333-6.776 32.561-6.776h179.651c11.226 0 22.080 2.259 32.559 6.776s19.465 10.918 26.948 19.2l59.507 64.377h136.986c22.455 0 41.54 7.906 57.263 23.718 15.718 15.812 23.578 35.012 23.578 57.6v515.011c0 22.588-7.859 41.788-23.578 57.6-15.723 15.812-34.807 23.718-57.263 23.718h-691.651z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["switch-camera"],"defaultCode":59696,"grid":0},"attrs":[{}],"properties":{"order":124,"id":123,"name":"switch-camera","prevSize":32,"code":59696},"setIdx":0,"setId":1,"iconIdx":130},{"icon":{"paths":["M1341.235 162.025c35.499-40.678 31.289-102.43-9.387-137.925s-102.434-31.294-137.921 9.384l-620.733 711.364-414.268-332.33c-42.111-33.782-103.637-27.030-137.419 15.082-33.783 42.111-27.030 103.637 15.082 137.419l487.421 391.014c19.833 15.91 43.969 22.83 67.487 21.299 25.074-1.621 49.522-12.851 67.347-33.279l682.391-782.029z"],"width":1365,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["tick"],"defaultCode":59698,"grid":0},"attrs":[{}],"properties":{"order":125,"id":124,"name":"tick","prevSize":32,"code":59698},"setIdx":0,"setId":1,"iconIdx":131},{"icon":{"paths":["M433.152 686.080l275.456-275.456-43.008-43.008-232.448 232.448-116.736-116.736-43.008 43.008 159.744 159.744zM491.52 901.12c-53.932 0-104.612-10.24-152.044-30.72-47.459-20.48-88.596-48.128-123.412-82.944s-62.464-75.952-82.944-123.412c-20.48-47.432-30.72-98.111-30.72-152.044s10.24-104.625 30.72-152.084c20.48-47.432 48.128-88.556 82.944-123.372s75.953-62.464 123.412-82.944c47.432-20.48 98.111-30.72 152.044-30.72s104.624 10.24 152.084 30.72c47.432 20.48 88.556 48.128 123.372 82.944s62.464 75.94 82.944 123.372c20.48 47.459 30.72 98.152 30.72 152.084s-10.24 104.612-30.72 152.044c-20.48 47.46-48.128 88.596-82.944 123.412s-75.94 62.464-123.372 82.944c-47.46 20.48-98.152 30.72-152.084 30.72z"],"width":983,"attrs":[{"fill":"rgb(54, 179, 126)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["tick-fill"],"defaultCode":59697,"grid":0},"attrs":[{"fill":"rgb(54, 179, 126)"}],"properties":{"order":126,"id":125,"name":"tick-fill","prevSize":32,"code":59697},"setIdx":0,"setId":1,"iconIdx":132},{"icon":{"paths":["M259.713 446.842l105.819 105.821-141.124 141.164c-9.997 9.999-9.998 26.209 0 36.209 9.997 9.994 26.206 9.994 36.204 0l470.652-470.652c9.994-9.998 9.994-26.207 0-36.204-9.999-9.997-26.209-9.997-36.204 0l-142.5 142.453-105.819-105.819 36.204-36.204-39.208-39.209-259.437 259.437 39.209 39.209 36.204-36.204z","M823.834 504.059l-39.209-39.209-127.473 0.035-192.317 192.318-0.035 127.473 39.253 39.301 140.289-140.288 156.908 156.908 39.209-0.036 0.036-39.209-156.882-156.933 140.221-140.36z"],"attrs":[{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["unpin-filled"],"grid":0},"attrs":[{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"}],"properties":{"order":127,"id":126,"name":"unpin-filled","prevSize":32,"code":59729},"setIdx":0,"setId":1,"iconIdx":133},{"icon":{"paths":["M259.713 446.842l105.819 105.821 39.208-39.209-105.819-105.821 108.612-108.612 105.819 105.819 39.209-39.209-105.819-105.819 36.204-36.204-39.208-39.209-259.437 259.437 39.209 39.209 36.204-36.204z","M632.023 520.771l55.798-55.8 96.85-0.027 39.209 39.209-140.293 140.292 156.908 156.908-0.036 39.209-39.209 0.036-156.908-156.908-140.289 140.288-39.209-39.209 0.027-96.845 55.798-55.798v96.819l208.174-208.174h-96.819z","M224.408 693.827c-9.998 9.999-9.998 26.209 0 36.209 9.997 9.994 26.206 9.994 36.204 0l470.652-470.652c9.994-9.998 9.994-26.207 0-36.204-9.999-9.997-26.209-9.997-36.204 0l-470.652 470.648z"],"attrs":[{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["unpin-outlined"],"grid":0},"attrs":[{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"}],"properties":{"order":128,"id":127,"name":"unpin-outlined","prevSize":32,"code":59730},"setIdx":0,"setId":1,"iconIdx":134},{"icon":{"paths":["M908.817 709.333l-150.4-149.333v80l-64-64v-307.198c0-3.555-1.237-6.571-3.712-9.045-2.505-2.503-5.888-3.755-10.155-3.755h-307.2l-64-64h371.2c22.046 0 40.533 7.467 55.467 22.4s22.4 33.067 22.4 54.4v195.198l150.4-149.331v394.665zM852.284 945.067l-787.2-786.131 45.867-44.8 786.134 786.131-44.8 44.8zM189.883 193.069l62.933 62.933h-57.6c-3.555 0-6.571 1.251-9.045 3.755-2.503 2.475-3.755 5.49-3.755 9.045v486.398c0 3.558 1.251 6.588 3.755 9.088 2.475 2.475 5.49 3.712 9.045 3.712h485.334c4.267 0 7.65-1.237 10.155-3.712 2.475-2.5 3.712-5.53 3.712-9.088v-57.6l62.933 62.933c-1.421 19.913-9.417 36.796-23.979 50.645-14.592 13.884-32.201 20.821-52.821 20.821h-485.334c-21.333 0-39.467-7.467-54.4-22.4s-22.4-33.067-22.4-54.4v-486.398c0-19.911 6.941-37.163 20.821-51.755 13.853-14.563 30.734-22.557 50.645-23.979z","M851.2 943.996l-787.2-786.133 45.867-44.8 786.133 786.133-44.8 44.8z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-off"],"defaultCode":59699,"grid":0},"attrs":[{},{}],"properties":{"order":129,"id":128,"name":"video-off","prevSize":32,"code":59699},"setIdx":0,"setId":1,"iconIdx":135},{"icon":{"paths":["M823.475 919.424l-736.001-733.849c-6.383-6.383-9.574-14.2-9.574-23.45s3.55-17.425 10.65-24.525c7.134-7.134 15.138-10.701 24.013-10.701s17.238 3.567 25.088 10.701l733.851 734.924c6.385 6.385 9.574 14.198 9.574 23.45s-3.19 17.065-9.574 23.45c-7.818 7.849-15.99 11.776-24.525 11.776s-16.369-3.927-23.501-11.776zM758.4 631.424l-440.526-439.449h358.401c23.485 0 43.044 7.816 58.675 23.45 15.631 15.667 23.45 34.867 23.45 57.6v185.6l112.026-111.974c7.818-7.817 16.179-9.592 25.088-5.325 8.873 4.266 13.312 11.008 13.312 20.224v300.85c0 9.216-4.439 15.959-13.312 20.224-8.909 4.265-17.27 2.493-25.088-5.325l-112.026-111.974v66.099zM193.1 193.051l564.276 565.298c0 19.215-8.535 36.285-25.6 51.2-17.065 14.95-35.569 22.426-55.501 22.426h-476.775c-22.767 0-41.967-7.818-57.6-23.45-15.667-15.667-23.501-34.867-23.501-57.6v-477.9c0-19.183 7.305-37.308 21.914-54.374 14.575-17.067 32.171-25.6 52.787-25.6z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-off-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":130,"id":129,"name":"video-off-filled","prevSize":32,"code":59731},"setIdx":0,"setId":1,"iconIdx":136},{"icon":{"paths":["M870.426 677.325l-112.026-111.974v66.099l-68.25-68.25v-290.15c0-3.55-1.244-6.57-3.738-9.062s-5.873-3.738-10.138-3.738h-290.1l-68.301-68.25h358.401c22.769 0 42.153 7.817 58.163 23.45 15.974 15.667 23.962 34.867 23.962 57.6v185.6l112.026-111.974c7.101-7.134 15.273-8.909 24.525-5.325 9.252 3.55 13.875 10.291 13.875 20.224v300.851c0 9.933-4.623 16.676-13.875 20.224-9.252 3.584-17.423 1.807-24.525-5.325zM823.475 919.45l-736.001-733.85c-6.383-6.383-9.574-14.029-9.574-22.938 0-8.874 3.909-16.879 11.725-24.013 6.417-7.1 14.234-10.65 23.45-10.65 9.25 0 17.442 3.55 24.576 10.65l733.851 733.901c6.385 6.385 9.764 14.029 10.138 22.938 0.343 8.873-3.036 16.86-10.138 23.962-7.132 7.132-15.309 10.701-24.525 10.701-9.252 0-17.085-3.569-23.501-10.701zM192.025 193.075l67.174 67.174h-60.774c-3.55 0-6.57 1.246-9.062 3.738s-3.738 5.513-3.738 9.062l1.075 477.901c0 3.548 1.246 6.569 3.738 9.062s5.513 3.738 9.062 3.738h476.775c4.265 0 7.644-1.244 10.138-3.738s3.738-5.514 3.738-9.062v-59.75l67.226 67.174c-2.15 20.649-10.87 38.077-26.163 52.275-15.293 14.234-33.603 21.35-54.938 21.35h-476.775c-22.767 0-41.967-7.818-57.6-23.45-15.667-15.667-23.501-34.867-23.501-57.6v-477.901c0-21.334 7.117-39.646 21.35-54.938s31.659-23.638 52.275-25.037z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-off-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":131,"id":130,"name":"video-off-outlined","prevSize":32,"code":59732},"setIdx":0,"setId":1,"iconIdx":137},{"icon":{"paths":["M194.133 832c-21.333 0-39.467-7.467-54.4-22.4s-22.4-33.067-22.4-54.4v-486.4c0-21.333 7.467-39.467 22.4-54.4s33.067-22.4 54.4-22.4h485.333c22.046 0 40.533 7.467 55.467 22.4s22.4 33.067 22.4 54.4v195.2l149.333-149.333v394.667l-149.333-149.333v195.2c0 21.333-7.467 39.467-22.4 54.4s-33.421 22.4-55.467 22.4h-485.333z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-on"],"defaultCode":59700,"grid":0},"attrs":[{}],"properties":{"order":132,"id":131,"name":"video-on","prevSize":32,"code":59700},"setIdx":0,"setId":1,"iconIdx":138},{"icon":{"paths":["M198.45 833.101c-22.767 0-41.967-7.834-57.6-23.501-15.633-15.631-23.45-34.816-23.45-57.549v-477.902c0-22.733 7.817-41.933 23.45-57.6 15.633-15.633 34.833-23.45 57.6-23.45h476.827c23.45 0 42.988 7.817 58.624 23.45 15.667 15.667 23.501 34.867 23.501 57.6v184.525l111.974-111.974c7.813-7.816 15.99-9.591 24.525-5.325 8.53 4.267 12.8 11.008 12.8 20.224v300.852c0 9.216-4.27 15.959-12.8 20.224-8.535 4.265-16.712 2.493-24.525-5.325l-111.974-111.974v186.675c0 22.733-7.834 41.917-23.501 57.549-15.636 15.667-35.174 23.501-58.624 23.501h-476.827z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-on-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":133,"id":132,"name":"video-on-filled","prevSize":32,"code":59733},"setIdx":0,"setId":1,"iconIdx":139},{"icon":{"paths":["M198.45 832c-22.767 0-41.967-7.818-57.6-23.45-15.633-15.667-23.45-34.867-23.45-57.6v-477.901c0-22.733 7.817-41.933 23.45-57.6 15.633-15.633 34.833-23.45 57.6-23.45h476.827c22.733 0 42.102 7.817 58.112 23.45 16.005 15.667 24.013 34.867 24.013 57.6v185.6l111.974-111.974c7.132-7.134 15.135-8.909 24.013-5.325 8.873 3.55 13.312 10.291 13.312 20.224v300.851c0 9.933-4.439 16.676-13.312 20.224-8.878 3.584-16.881 1.807-24.013-5.325l-111.974-111.974v185.6c0 22.733-8.008 41.933-24.013 57.6-16.010 15.631-35.379 23.45-58.112 23.45h-476.827zM198.45 763.75h476.827c4.229 0 7.593-1.244 10.086-3.738 2.488-2.493 3.738-5.514 3.738-9.062v-477.901c0-3.55-1.249-6.57-3.738-9.062-2.493-2.492-5.857-3.738-10.086-3.738h-476.827c-3.55 0-6.57 1.246-9.062 3.738s-3.738 5.513-3.738 9.062v477.901c0 3.548 1.246 6.569 3.738 9.062s5.513 3.738 9.062 3.738zM185.65 763.75v0z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-on-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":134,"id":133,"name":"video-on-outlined","prevSize":32,"code":59734},"setIdx":0,"setId":1,"iconIdx":140},{"icon":{"paths":["M384 682.667h85.333v-128h128v-85.333h-128v-128h-85.333v128h-128v85.333h128v128zM170.667 853.333c-23.467 0-43.549-8.35-60.245-25.045-16.725-16.725-25.088-36.821-25.088-60.288v-512c0-23.467 8.363-43.549 25.088-60.245 16.697-16.725 36.779-25.088 60.245-25.088h512c23.467 0 43.563 8.363 60.288 25.088 16.695 16.697 25.045 36.779 25.045 60.245v192l170.667-170.667v469.333l-170.667-170.667v192c0 23.467-8.35 43.563-25.045 60.288-16.725 16.695-36.821 25.045-60.288 25.045h-512z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-plus"],"defaultCode":59701,"grid":0},"attrs":[{}],"properties":{"order":135,"id":134,"name":"video-plus","prevSize":32,"code":59701},"setIdx":0,"setId":1,"iconIdx":141}],"height":1024,"metadata":{"name":"icomoon"},"preferences":{"showGlyphs":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"icomoon"},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"classSelector":".icon","name":"icomoon"},"historySize":50,"showCodes":true,"gridSize":16}} \ No newline at end of file +{"IcoMoonType":"selection","icons":[{"icon":{"paths":["M512.481 938.732c43.156 0 82.888-7.557 119.194-22.676 36.275-15.119 67.666-35.446 94.162-60.974 26.47-25.503 47.222-55.26 62.264-89.283 15.037-34.017 22.559-69.929 22.559-107.73v-219.239c0-12.6-4.408-23.158-13.225-31.676-8.837-8.493-19.799-12.739-32.876-12.739s-24.038 4.246-32.881 12.739c-8.812 8.518-13.22 19.076-13.22 31.676v170.098h-11.771c-39.235 4.413-70.292 18.437-93.179 42.071-22.886 23.613-35.635 51.169-38.252 82.673h-35.313c3.272-39.060 17.654-72.453 43.156-100.173s59.177-45.676 101.028-53.862v-370.441c0-12.6-4.408-23.158-13.22-31.676-8.842-8.493-19.799-12.739-32.881-12.739-13.076 0-23.864 4.246-32.369 12.739-8.499 8.518-12.749 19.076-12.749 31.676v274.995h-35.313v-355.319c0-12.6-4.419-22.995-13.261-31.185-8.812-8.19-19.43-12.285-31.857-12.285-13.077 0-24.023 4.095-32.838 12.285-8.841 8.19-13.261 18.585-13.261 31.185v355.319h-34.33v-307.124c0-11.97-4.407-22.365-13.221-31.185-8.841-8.82-19.8-13.23-32.878-13.23s-24.024 4.41-32.839 13.23c-8.841 8.82-13.261 19.215-13.261 31.185v330.749h-35.311v-220.185c0-12.6-4.407-23.146-13.221-31.639-8.841-8.518-19.473-12.776-31.897-12.776-13.078 0-24.024 4.259-32.839 12.776-8.841 8.492-13.261 19.038-13.261 31.639v370.439c0 37.801 7.52 73.713 22.56 107.73 15.039 34.022 35.964 63.78 62.774 89.283 26.81 25.528 58.367 45.855 94.671 60.974 36.278 15.119 75.996 22.676 119.152 22.676z"],"attrs":[{"fill":"rgb(255, 171, 0)"}],"isMulticolor":false,"isMulticolor2":false,"grid":0,"tags":["raise-hand-fill"]},"attrs":[{"fill":"rgb(255, 171, 0)"}],"properties":{"order":886,"id":143,"name":"raise-hand-fill","prevSize":32,"code":59820},"setIdx":0,"setId":1,"iconIdx":0},{"icon":{"paths":["M192 874.732c-9.066 0-16.665-3.067-22.795-9.206-6.137-6.134-9.205-13.737-9.205-22.804 0-9.073 3.068-16.671 9.205-22.794 6.13-6.129 13.728-9.196 22.795-9.196h53.334v-622.761c0-10.93 3.698-20.089 11.093-27.477 7.389-7.396 16.548-11.094 27.478-11.094h280.607c10.931 0 20.091 3.698 27.479 11.094 7.393 7.388 11.095 16.547 11.095 27.477v4.096h137.011c10.931 0 20.091 3.698 27.479 11.094 7.393 7.388 11.090 16.547 11.090 27.477v580.094h53.335c9.068 0 16.666 3.072 22.794 9.206 6.139 6.139 9.206 13.742 9.206 22.804 0 9.078-3.067 16.676-9.206 22.799-6.129 6.129-13.727 9.19-22.794 9.19h-78.572c-11.126 0-20.367-3.697-27.72-11.090-7.363-7.388-11.044-16.548-11.044-27.479v-580.096h-111.58v580.096c0 10.931-3.702 20.091-11.095 27.479-7.388 7.393-16.548 11.090-27.479 11.090h-372.511zM517.75 512.067c0-10.446-3.681-19.349-11.040-26.709-7.36-7.353-16.263-11.030-26.71-11.030s-19.35 3.676-26.71 11.030c-7.353 7.36-11.029 16.263-11.029 26.709 0 10.445 3.676 19.348 11.029 26.711 7.36 7.352 16.263 11.028 26.71 11.028s19.35-3.676 26.71-11.028c7.359-7.363 11.040-16.266 11.040-26.711z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["open-room"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":885,"id":0,"name":"open-room","prevSize":32,"code":59819},"setIdx":0,"setId":1,"iconIdx":1},{"icon":{"paths":["M371.106 408.733c-36.8 0-68.042-12.842-93.728-38.528-25.686-25.692-38.528-56.938-38.528-93.738s12.842-68.043 38.528-93.728c25.686-25.692 56.928-38.538 93.728-38.538s68.046 12.846 93.739 38.538c25.685 25.686 38.528 56.928 38.528 93.728s-12.843 68.046-38.528 93.738c-25.693 25.686-56.939 38.528-93.739 38.528zM759.255 888.315c-51.292 0-94.879-17.935-130.765-53.811-35.871-35.876-53.811-79.457-53.811-130.749 0-51.231 17.94-94.909 53.811-131.041 35.886-36.132 79.473-54.2 130.765-54.2 51.22 0 94.899 18.068 131.031 54.2s54.195 79.811 54.195 131.041c0 51.292-18.063 94.874-54.195 130.749s-79.811 53.811-131.031 53.811zM735.171 799.462l131.604-131.2-32.983-32.573-98.621 98.217-50.381-51.446-32.978 33.633 83.359 83.369zM85.25 699.515v-73.513c0-16.297 3.897-30.587 11.69-42.87 7.794-12.278 18.684-22.508 32.672-30.674 36.992-21.258 74.741-37.617 113.248-49.079 38.514-11.456 81.263-17.184 128.245-17.184 37.853 0 73.408 4.061 106.667 12.182s64.901 19.543 94.935 34.263c-22.154 22.641-39.818 48.184-53.002 76.626-13.186 28.447-20.87 58.532-23.053 90.25h-411.403z","M622.454 379.679c25.687-25.692 38.528-56.938 38.528-93.738s-12.841-68.043-38.528-93.728c-25.682-25.693-56.924-38.539-93.727-38.539-1.864 0-3.369-0.025-4.511-0.075-1.152-0.057-2.657-0.057-4.511 0 15.037 18.432 27.167 38.627 36.383 60.586 9.216 21.966 13.824 45.871 13.824 71.712 0 25.834-4.516 49.834-13.548 72-9.037 22.165-21.258 42.297-36.659 60.394 2.673 0.598 4.178 0.733 4.511 0.406 0.328-0.327 1.828-0.491 4.511-0.491 36.803 0 68.045-12.842 93.727-38.528z"],"attrs":[{"fill":"rgb(232, 234, 237)"},{"fill":"rgb(232, 234, 237)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["people-assigned"],"grid":0},"attrs":[{"fill":"rgb(232, 234, 237)"},{"fill":"rgb(232, 234, 237)"}],"properties":{"order":880,"id":1,"name":"people-assigned","prevSize":32,"code":59812},"setIdx":0,"setId":1,"iconIdx":2},{"icon":{"paths":["M512 574.673l-173.781 173.129c-5.909 5.905-13.227 9.033-21.952 9.387-8.725 0.358-16.398-2.773-23.019-9.387-6.179-6.187-9.269-13.679-9.269-22.485 0-8.802 3.090-16.299 9.269-22.485l172.727-172.715c12.523-12.523 27.861-18.782 46.025-18.782s33.506 6.259 46.029 18.782l172.723 172.715c5.901 5.909 9.033 13.227 9.387 21.952s-2.773 16.401-9.387 23.019c-6.187 6.182-13.683 9.271-22.485 9.271s-16.299-3.089-22.485-9.271l-173.781-173.129zM512 318.672l-173.781 173.129c-5.909 5.905-13.227 9.033-21.952 9.387-8.725 0.358-16.398-2.773-23.019-9.387-6.179-6.187-9.269-13.679-9.269-22.485 0-8.802 3.090-16.299 9.269-22.485l172.727-172.714c12.523-12.523 27.861-18.784 46.025-18.784s33.506 6.261 46.029 18.784l172.723 172.714c5.901 5.909 9.033 13.227 9.387 21.952s-2.773 16.401-9.387 23.019c-6.187 6.182-13.683 9.271-22.485 9.271s-16.299-3.089-22.485-9.271l-173.781-173.129z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["double-up-arrow"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":881,"id":2,"name":"double-up-arrow","prevSize":32,"code":59813},"setIdx":0,"setId":1,"iconIdx":3},{"icon":{"paths":["M350.783 832.073c-78.001 0-144.476-26.79-199.424-80.371-54.947-53.577-82.421-119.228-82.421-196.962 0-74.176 26.229-137.272 78.688-189.29s116.239-81.063 191.339-87.136l-55.627-56.949c-6.187-6.621-9.28-14.112-9.28-22.475 0-8.37 3.061-15.616 9.184-21.739 6.677-6.677 14.489-9.949 23.435-9.813s16.37 3.157 22.272 9.067l106.017 106.005c7.706 7.715 11.563 16.715 11.563 26.997s-3.857 19.282-11.563 26.997l-105.356 105.354c-6.62 6.613-14.222 9.92-22.805 9.92-8.59 0-16.224-3.307-22.901-9.92-6.123-6.618-9.116-14.293-8.981-23.018s3.157-16.043 9.067-21.952l53.173-53.163c-56.782 6.67-105.017 29.629-144.704 68.875-39.68 39.255-59.52 86.666-59.52 142.239 0 59.571 21.344 110.016 64.032 151.339 42.695 41.331 94.101 61.995 154.219 61.995h75.487c9.067 0 16.666 3.068 22.797 9.207 6.135 6.135 9.203 13.739 9.203 22.805 0 9.071-3.068 16.67-9.203 22.793-6.131 6.131-13.73 9.195-22.797 9.195h-75.892zM604.096 465.301c-11.123 0-20.361-3.695-27.712-11.085-7.36-7.386-11.042-16.546-11.042-27.477v-195.935c0-11.107 3.695-20.338 11.085-27.691 7.394-7.36 16.555-11.040 27.477-11.040h274.688c11.119 0 20.356 3.698 27.712 11.093 7.36 7.389 11.038 16.547 11.038 27.477v195.936c0 11.099-3.695 20.332-11.093 27.692-7.386 7.351-16.546 11.029-27.477 11.029h-274.675zM604.096 832.073c-11.123 0-20.361-3.695-27.712-11.093-7.36-7.386-11.042-16.546-11.042-27.477v-195.934c0-11.102 3.695-20.331 11.085-27.691 7.394-7.356 16.555-11.029 27.477-11.029h274.688c11.119 0 20.356 3.695 27.712 11.081 7.36 7.39 11.038 16.55 11.038 27.477v195.938c0 11.106-3.695 20.335-11.093 27.691-7.386 7.36-16.546 11.038-27.477 11.038h-274.675zM629.342 768.073h224v-145.237h-224v145.237z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["move-up"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":882,"id":3,"name":"move-up","prevSize":32,"code":59814},"setIdx":0,"setId":1,"iconIdx":4},{"icon":{"paths":["M26.961 130.157c14.291-15.673 37.964-17.691 54.64-5.32l3.24 2.64 839.241 765.48c13.824 15.462 13.992 39.215-0.283 54.878-14.287 15.663-37.966 17.678-54.637 5.321l-3.24-2.638c-98.587-89.883-188.695-172.073-286.962-261.64v117.637c0 10.49-3.543 19.309-10.641 26.403-7.090 7.098-15.909 10.637-26.399 10.637h-357.6c-8.702 0-15.996-2.949-21.88-8.839-5.888-5.886-8.836-13.181-8.84-21.881 0-8.704 2.954-16.003 8.84-21.877 5.884-5.886 13.176-8.843 21.88-8.843h51.2v-406.399l-205.88-187.68-2.96-3c-13.836-15.462-13.994-39.212 0.28-54.88zM541.921 147.236c10.494 0 19.309 3.541 26.399 10.64 7.098 7.092 10.641 15.909 10.641 26.4v3.92h131.518c10.494 0 19.309 3.541 26.403 10.64 7.098 7.092 10.637 15.908 10.637 26.4v383.478l-61.44-56.439v-302.64h-107.119v204.319l-327.4-300.559c6.019-4.103 13.022-6.16 21-6.16h269.36zM446.48 495.395c-20.353 0-36.868 16.49-36.88 36.844 0 20.357 16.523 36.876 36.88 36.876 20.353-0.008 36.839-16.523 36.839-36.876-0.008-20.349-16.49-36.835-36.839-36.844z"],"width":983,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["close-room"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":883,"id":4,"name":"close-room","prevSize":32,"code":59815},"setIdx":0,"setId":1,"iconIdx":5},{"icon":{"paths":["M853.070 526.139h-86.651c-8.704 0-15.999-2.945-21.885-8.835-5.89-5.89-8.835-13.189-8.835-21.893 0-8.712 2.945-16.007 8.835-21.885 5.886-5.882 13.181-8.827 21.885-8.827h86.651c8.704 0 15.995 2.949 21.881 8.839 5.89 5.89 8.839 13.189 8.839 21.893 0 8.712-2.949 16.003-8.839 21.881-5.886 5.886-13.177 8.827-21.881 8.827zM682.684 688.484c5.251-7.193 11.96-11.346 20.132-12.452 8.163-1.098 15.843 0.979 23.040 6.226l69.079 51.909c7.197 5.255 11.342 11.969 12.444 20.132 1.106 8.163-0.971 15.843-6.226 23.040-5.251 7.188-11.96 11.334-20.124 12.44s-15.843-0.967-23.040-6.222l-69.079-51.909c-7.197-5.251-11.342-11.956-12.44-20.124-1.106-8.163 0.963-15.843 6.214-23.040zM793.358 255.099l-69.079 51.907c-7.197 5.25-14.877 7.325-23.040 6.226-8.163-1.106-14.877-5.256-20.132-12.452-5.251-7.188-7.32-14.868-6.214-23.040 1.098-8.165 5.243-14.872 12.44-20.122l69.079-51.907c7.197-5.25 14.877-7.325 23.040-6.226 8.163 1.106 14.877 5.253 20.132 12.442 5.251 7.195 7.324 14.875 6.218 23.040-1.102 8.172-5.247 14.882-12.444 20.132zM215.034 600.973h-41.748c-20.364 0-37.796-7.25-52.296-21.75s-21.75-31.928-21.75-52.294v-63.017c0-20.365 7.25-37.798 21.75-52.298 14.5-14.498 31.932-21.748 52.296-21.748h158.331l127.365-76.165c12.341-7.407 24.789-7.448 37.335-0.123 12.554 7.318 18.833 17.992 18.833 32.020v299.643c0 14.029-6.279 24.703-18.833 32.018-12.546 7.328-24.994 7.287-37.335-0.123l-127.365-76.165h-55.142v129.18c0 8.704-2.945 15.999-8.837 21.881-5.891 5.894-13.189 8.839-21.893 8.839-8.711 0-16.005-2.945-21.883-8.839-5.885-5.882-8.827-13.177-8.827-21.881v-129.18zM571.859 623.186v-255.529c16.069 14.548 29.012 32.908 38.83 55.079 9.822 22.176 14.733 46.432 14.733 72.778 0 26.35-4.911 50.582-14.733 72.692-9.818 22.106-22.761 40.432-38.83 54.981z"],"width":983,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["announcement"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":874,"id":5,"name":"announcement","prevSize":32,"code":59816},"setIdx":0,"setId":1,"iconIdx":6},{"icon":{"paths":["M429.294 884.478c-9.191 0-17.146-3.084-23.867-9.257-6.722-6.169-10.765-13.849-12.13-23.040l-11.894-92.471c-10.975-3.678-22.226-8.823-33.753-15.442-11.527-6.615-21.832-13.705-30.917-21.266l-85.543 36.078c-8.77 3.83-17.474 4.276-26.112 1.335s-15.426-8.454-20.362-16.54l-61.596-107.282c-4.674-8.086-6.118-16.724-4.332-25.915 1.785-9.187 6.38-16.593 13.784-22.213l74.043-56.005c-0.945-6.091-1.615-12.21-2.009-18.354s-0.591-12.259-0.591-18.35c0-5.833 0.197-11.751 0.591-17.764s1.064-12.591 2.009-19.73l-74.043-56.007c-7.404-5.618-12.064-13.023-13.98-22.213s-0.408-17.827 4.529-25.915l61.596-106.494c4.936-8.087 11.658-13.601 20.166-16.542s17.145-2.494 25.914 1.339l85.543 35.682c9.873-7.825 20.415-14.98 31.626-21.465s22.227-11.698 33.044-15.636l12.288-92.474c1.365-9.19 5.409-16.87 12.13-23.040s14.676-9.256 23.867-9.256h124.060c9.187 0 17.211 3.086 24.064 9.256s10.961 13.85 12.325 23.040l11.895 92.869c12.288 4.463 23.409 9.675 33.362 15.635 9.949 5.96 19.993 12.984 30.126 21.072l87.122-35.682c8.77-3.834 17.408-4.346 25.911-1.536 8.507 2.809 15.233 8.257 20.169 16.344l61.596 106.888c4.936 8.087 6.443 16.725 4.526 25.915-1.913 9.19-6.574 16.594-13.98 22.213l-75.616 57.187c1.47 6.615 2.269 12.8 2.4 18.551s0.197 11.538 0.197 17.367c0 5.566-0.131 11.227-0.393 16.978-0.262 5.747-1.208 12.325-2.834 19.73l74.83 56.398c7.406 5.62 12.063 13.025 13.98 22.213 1.917 9.191 0.41 17.83-4.526 25.915l-61.6 106.652c-4.936 8.086-11.825 13.64-20.677 16.658-8.847 3.019-17.658 2.613-26.427-1.221l-84.677-36.073c-10.134 8.086-20.48 15.241-31.035 21.463s-21.373 11.305-32.453 15.241l-11.895 92.869c-1.364 9.191-5.472 16.871-12.325 23.040-6.853 6.173-14.877 9.257-24.064 9.257h-124.060zM450.56 823.038h80.503l14.729-109.724c20.898-5.464 40.002-13.222 57.303-23.278 17.306-10.056 33.989-22.987 50.057-38.793l101.773 42.77 40.329-69.632-88.855-66.953c3.416-10.609 5.738-21.004 6.971-31.191 1.237-10.187 1.851-20.48 1.851-30.88 0-10.658-0.614-20.951-1.851-30.876-1.233-9.925-3.555-20.062-6.971-30.405l89.641-67.743-40.329-69.632-102.953 43.402c-13.705-14.651-30.126-27.596-49.271-38.833-19.141-11.238-38.502-19.194-58.089-23.867l-12.919-109.726h-81.289l-13.55 109.331c-20.898 4.936-40.198 12.498-57.895 22.686s-34.58 23.316-50.648 39.385l-101.77-42.378-40.33 69.632 88.458 65.932c-3.414 9.712-5.803 19.821-7.168 30.323s-2.048 21.557-2.048 33.165c0 10.658 0.682 21.107 2.048 31.347s3.623 20.349 6.774 30.327l-88.064 66.953 40.33 69.632 101.376-43.008c15.544 15.966 32.164 29.041 49.861 39.227s37.257 18.014 58.683 23.474l13.312 109.33zM491.991 618.238c34.083 0 63.082-11.96 86.999-35.877 23.921-23.921 35.881-52.92 35.881-87.003 0-34.079-11.96-63.078-35.881-86.999-23.917-23.919-52.916-35.879-86.999-35.879-34.501 0-63.603 11.96-87.314 35.879-23.709 23.921-35.564 52.92-35.564 86.999 0 34.083 11.855 63.082 35.564 87.003 23.711 23.917 52.813 35.877 87.314 35.877z"],"width":983,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["settings-outlined"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":875,"id":6,"name":"settings-outlined","prevSize":32,"code":59817},"setIdx":0,"setId":1,"iconIdx":7},{"icon":{"paths":["M690.876 334.375c21.547 0.001 39.778 7.451 54.707 22.375 14.933 14.933 22.417 33.196 22.417 54.75v407.376c0 21.551-7.484 39.774-22.417 54.707s-33.156 22.417-54.707 22.417h-400.417c-21.551 0-39.776-7.484-54.708-22.417s-22.415-33.156-22.417-54.707v-407.376c0-21.554 7.483-39.817 22.417-54.75 14.93-14.924 33.163-22.374 54.708-22.375h400.417zM439.834 688.043c-21.666 0-41.581 2.603-59.667 7.748-18.071 5.15-36.043 12.467-53.917 21.961-8.365 4.591-15.077 11.025-20.083 19.29-5.004 8.265-7.5 17.681-7.5 28.207v14.379c0.001 8.614 2.773 15.957 8.333 21.999 5.559 6.033 12.318 9.041 20.25 9.041h225.126c7.932 0 14.69-3.106 20.25-9.335 5.559-6.234 8.333-13.483 8.333-21.705v-14.379c0-10.526-2.496-19.942-7.501-28.207-5.001-8.26-11.682-14.699-20.041-19.29-17.877-9.498-35.878-16.811-53.956-21.961-18.078-5.146-37.965-7.748-59.627-7.748zM583.791 693.082c11.115 8.704 19.699 19.14 25.749 31.292 6.046 12.151 9.084 24.93 9.084 38.336v16.917c0 5.419-0.631 10.79-1.873 16.081-1.242 5.286-3.055 10.274-5.419 14.959h42.752c7.932 0 14.69-3.008 20.25-9.041 5.559-6.042 8.333-13.385 8.333-21.999v-16.917c-0.004-9.045-2.667-17.404-7.957-25.041-5.295-7.637-12.787-14.289-22.502-19.917-10.295-5.841-21.124-10.884-32.457-15.168-11.341-4.279-23.339-7.458-35.959-9.502zM439.834 512c-18.113 0-33.645 6.993-46.542 21.001-12.898 14.003-19.333 30.874-19.333 50.543 0.004 19.657 6.439 36.493 19.333 50.5 12.898 14.003 28.428 20.996 46.542 20.996 18.103-0.004 33.604-7.002 46.498-20.996 12.894-14.007 19.332-30.844 19.337-50.5 0-19.669-6.438-36.54-19.337-50.543-12.894-13.999-28.395-20.996-46.498-21.001zM528.333 512.457c-2.697 0.316-5.389 0.998-8.081 2.044 7.262 9.847 12.89 20.663 16.956 32.375 4.062 11.708 6.127 23.91 6.127 36.625s-2.018 24.981-5.999 36.791c-3.989 11.802-9.668 22.562-17.084 32.247 2.112 0.627 4.809 1.186 8.081 1.711 3.281 0.525 6.003 0.789 8.124 0.789 18.108 0 33.609-7.002 46.502-20.996 12.894-14.007 19.371-30.844 19.375-50.5 0-19.669-6.477-36.54-19.375-50.543-12.894-13.999-28.395-21.001-46.502-21.001-2.701 0-5.423 0.141-8.124 0.457zM701.542 209.625c7.334 0.001 13.402 2.417 18.206 7.208 4.804 4.796 7.206 10.871 7.211 18.208 0 7.339-2.402 13.436-7.211 18.25-4.804 4.808-10.867 7.207-18.206 7.209h-422.167c-7.343-0.001-13.402-2.41-18.208-7.209-4.798-4.8-7.208-10.864-7.208-18.208 0.002-7.334 2.411-13.396 7.208-18.208 4.806-4.812 10.866-7.249 18.208-7.25h422.167zM660.501 85.333c7.343 0 13.44 2.409 18.249 7.209 4.8 4.798 7.206 10.868 7.206 18.208 0 7.339-2.398 13.436-7.206 18.25-4.804 4.811-10.906 7.208-18.249 7.208h-340.084c-7.353 0-13.45-2.408-18.25-7.208-4.8-4.798-7.208-10.876-7.208-18.208 0-7.343 2.404-13.437 7.208-18.25 4.8-4.815 10.897-7.209 18.25-7.209h340.084z"],"attrs":[{"fill":"rgb(217, 217, 217)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["breakout-room"],"grid":0},"attrs":[{"fill":"rgb(217, 217, 217)"}],"properties":{"order":801,"id":7,"name":"breakout-room","prevSize":32,"code":59818},"setIdx":0,"setId":1,"iconIdx":8},{"icon":{"paths":["M409.65 499.15c-36.966 0-68.25-12.97-93.85-38.912-25.6-25.975-38.4-57.088-38.4-93.338 0-37.001 12.8-68.301 38.4-93.901s56.883-38.4 93.85-38.4c36.966 0 68.25 12.8 93.85 38.4 25.602 25.6 38.402 56.9 38.402 93.901 0 36.25-12.8 67.362-38.402 93.338-25.6 25.942-56.883 38.912-93.85 38.912zM123.8 789.299v-73.626c0-16.348 3.908-30.566 11.725-42.65s18.842-22.39 33.075-30.925c37.683-21.33 76.083-37.699 115.2-49.101 39.117-11.366 81.066-17.050 125.85-17.050 7.1 0 13.67 0.174 19.712 0.512 6.042 0.379 12.63 0.922 19.763 1.638-3.55 9.216-6.042 18.452-7.475 27.699-1.434 9.252-2.509 18.145-3.226 26.675l-28.774-1.075c-36.966 0-72.874 4.27-107.725 12.8-34.85 8.535-69.7 23.47-104.55 44.8-6.383 3.553-11.008 7.475-13.875 11.776-2.833 4.234-4.25 9.201-4.25 14.899v18.125h267.725c2.15 9.252 5.171 18.673 9.062 28.262 3.926 9.626 8.38 18.708 13.363 27.238h-345.6zM409.65 443.701c21.334 0 39.458-7.646 54.374-22.938 14.95-15.292 22.426-33.604 22.426-54.938 0-20.617-7.475-38.4-22.426-53.35-14.916-14.916-33.041-22.374-54.374-22.374s-39.458 7.458-54.374 22.374c-14.95 14.95-22.426 33.092-22.426 54.426s7.475 39.458 22.426 54.374c14.916 14.95 33.041 22.426 54.374 22.426z","M590.264 819.2l33.597-132.050-111.862-89.17 146.724-11.986 58.076-125.194 58.076 125.768 146.724 11.412-111.862 89.17 33.597 132.050-126.536-69.745-126.536 69.745z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["spotlight"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":876,"id":8,"name":"spotlight","prevSize":32,"code":59811},"setIdx":0,"setId":1,"iconIdx":9},{"icon":{"paths":["M386.464 746.65c-21.554 0-39.798-7.465-54.731-22.4s-22.4-33.178-22.4-54.728v-485.741c0-21.554 7.466-39.798 22.4-54.731s33.177-22.4 54.731-22.4h357.738c21.555 0 39.798 7.466 54.733 22.4 14.93 14.933 22.4 33.177 22.4 54.731v485.741c0 21.55-7.47 39.793-22.4 54.728-14.935 14.935-33.178 22.4-54.733 22.4h-357.738zM386.464 682.65h357.738c3.287 0 6.292-1.367 9.027-4.106 2.734-2.729 4.106-5.74 4.106-9.021v-485.741c0-3.286-1.372-6.294-4.106-9.024-2.734-2.738-5.74-4.107-9.027-4.107h-357.738c-3.286 0-6.294 1.369-9.024 4.107-2.738 2.73-4.107 5.738-4.107 9.024v485.741c0 3.282 1.369 6.292 4.107 9.021 2.73 2.739 5.738 4.106 9.024 4.106zM237.131 895.985c-21.554 0-39.797-7.47-54.731-22.4-14.934-14.935-22.4-33.178-22.4-54.733v-549.738h64v549.738c0 3.287 1.369 6.292 4.107 9.027 2.73 2.734 5.738 4.106 9.024 4.106h421.736v64h-421.736z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["clipboard_outlined"],"grid":0},"attrs":[{}],"properties":{"order":791,"id":9,"name":"clipboard_outlined","prevSize":32,"code":59810},"setIdx":0,"setId":1,"iconIdx":10},{"icon":{"paths":["M487.97 882.235c-50.869 0-98.778-9.621-143.725-28.872-44.948-19.246-84.29-45.729-118.026-79.448-33.736-33.724-60.233-73.031-79.489-117.931s-28.884-92.872-28.884-143.921c0-51.051 9.625-98.88 28.876-143.489s45.74-83.782 79.466-117.518c33.727-33.736 72.995-60.233 117.803-79.489s92.685-28.884 143.633-28.884c24.015 0 47.991 2.136 71.929 6.408s46.899 11.213 68.886 20.821v58.025c-22.508-10.732-45.446-18.82-68.813-24.264s-47.367-8.166-72.002-8.166c-87.824 0-162.605 30.893-224.344 92.678s-92.61 136.621-92.61 224.508c0 87.888 30.893 162.645 92.678 224.276s136.621 92.443 224.51 92.443c87.884 0 162.64-30.871 224.271-92.609s92.448-136.519 92.448-224.344c0-24.869-2.789-48.994-8.363-72.376s-13.858-46.194-24.849-68.438h59.387c9.221 22.661 15.965 45.76 20.241 69.297 4.272 23.537 6.407 47.376 6.407 71.518 0 50.946-9.626 98.826-28.872 143.633s-45.734 84.075-79.453 117.804c-33.719 33.724-72.953 60.216-117.692 79.462-44.739 19.251-92.545 28.877-143.414 28.877zM790.591 209.728h-46.85c-7.519 0-13.819-2.532-18.905-7.597-5.081-5.065-7.626-11.341-7.626-18.828s2.545-13.759 7.626-18.815c5.086-5.056 11.386-7.583 18.905-7.583h46.85v-46.731c0-7.483 2.54-13.756 7.631-18.818 5.086-5.062 11.342-7.594 18.768-7.594s13.648 2.531 18.666 7.594c5.013 5.062 7.519 11.335 7.519 18.818v46.731h46.519c7.422 0 13.717 2.512 18.881 7.536s7.743 11.249 7.743 18.676c0 7.426-2.531 13.679-7.592 18.758s-11.337 7.617-18.817 7.617h-46.733v46.849c0 7.518-2.531 13.819-7.597 18.904s-11.342 7.627-18.827 7.627c-7.285 0-13.468-2.531-18.544-7.594-5.081-5.062-7.617-11.335-7.617-18.818v-46.731zM617.901 468.073c14.785 0 27.326-5.175 37.615-15.526s15.433-22.918 15.433-37.704c0-14.786-5.179-27.323-15.526-37.612-10.352-10.289-22.918-15.433-37.708-15.433-14.785 0-27.321 5.175-37.61 15.526s-15.433 22.918-15.433 37.704c0 14.786 5.174 27.323 15.526 37.612 10.347 10.289 22.918 15.433 37.703 15.433zM357.526 468.073c14.786 0 27.323-5.175 37.612-15.526s15.435-22.918 15.435-37.704c0-14.786-5.176-27.323-15.526-37.612s-22.919-15.433-37.704-15.433c-14.786 0-27.323 5.175-37.612 15.526s-15.433 22.918-15.433 37.704c0 14.786 5.175 27.323 15.525 37.612s22.919 15.433 37.704 15.433zM487.172 695.003c32.030 0 61.931-6.773 89.691-20.319 27.765-13.541 51.546-32.402 71.344-56.574 3.959-6.042 4.403-12.127 1.331-18.247-3.077-6.12-7.948-9.182-14.614-9.182h-294.602c-6.668 0-11.54 3.062-14.613 9.182s-2.687 12.205 1.159 18.247c19.233 24.171 42.888 43.032 70.967 56.574 28.079 13.546 57.858 20.319 89.338 20.319z"],"width":975,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["add_reaction"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":877,"id":10,"name":"add_reaction","prevSize":32,"code":59809},"setIdx":0,"setId":1,"iconIdx":11},{"icon":{"paths":["M261.968 771.872h104.086c7.483 0 13.758 2.516 18.824 7.539 5.059 5.027 7.589 11.249 7.589 18.671 0 7.431-2.53 13.722-7.589 18.876-5.066 5.159-11.341 7.743-18.824 7.743h-124.241c-9.258 0-17.016-3.135-23.274-9.396-6.264-6.261-9.397-14.019-9.397-23.274v-124.245c0-7.48 2.513-13.756 7.538-18.822 5.025-5.057 11.249-7.587 18.671-7.587 7.429 0 13.721 2.531 18.875 7.587 5.16 5.066 7.741 11.342 7.741 18.822v104.087zM798.735 771.872v-104.087c0-7.48 2.511-13.756 7.539-18.822 5.027-5.057 11.254-7.587 18.681-7.587 7.422 0 13.717 2.531 18.876 7.587 5.154 5.066 7.729 11.342 7.729 18.822v124.245c0 9.255-3.126 17.013-9.387 23.274s-14.019 9.396-23.269 9.396h-124.255c-7.48 0-13.756-2.516-18.812-7.539-5.066-5.027-7.597-11.249-7.597-18.671 0-7.431 2.531-13.722 7.597-18.876 5.057-5.164 11.332-7.743 18.812-7.743h104.087zM261.968 235.492v104.086c0 7.483-2.513 13.758-7.538 18.824-5.025 5.059-11.249 7.589-18.671 7.589-7.43 0-13.721-2.53-18.875-7.589-5.16-5.066-7.741-11.341-7.741-18.824v-124.241c0-9.258 3.132-17.015 9.397-23.274 6.258-6.264 14.016-9.397 23.274-9.397h124.241c7.483 0 13.758 2.513 18.824 7.538 5.059 5.025 7.589 11.249 7.589 18.671 0 7.429-2.53 13.721-7.589 18.875-5.066 5.16-11.341 7.741-18.824 7.741h-104.086zM798.735 235.492h-104.087c-7.48 0-13.756-2.513-18.812-7.538-5.066-5.025-7.597-11.249-7.597-18.671 0-7.429 2.531-13.721 7.597-18.875 5.057-5.16 11.332-7.741 18.812-7.741h124.255c9.25 0 17.008 3.132 23.269 9.397 6.261 6.258 9.387 14.016 9.387 23.274v124.241c0 7.483-2.506 13.758-7.524 18.824-5.027 5.059-11.254 7.589-18.686 7.589-7.426 0-13.717-2.53-18.871-7.589-5.164-5.066-7.743-11.341-7.743-18.824v-104.086z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["fullscreen"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":878,"id":11,"name":"fullscreen","prevSize":32,"code":59808},"setIdx":0,"setId":1,"iconIdx":12},{"icon":{"paths":["M952.264 512c0-226.216-183.383-409.6-409.6-409.6s-409.601 183.384-409.601 409.6c0 226.217 183.384 409.6 409.601 409.6s409.6-183.383 409.6-409.6zM911.304 512c0 203.592-165.048 368.64-368.64 368.64-203.595 0-368.641-165.048-368.641-368.64 0-203.594 165.046-368.64 368.641-368.64 203.592 0 368.64 165.046 368.64 368.64z","M696.264 512c0 84.831-68.769 153.6-153.6 153.6s-153.6-68.769-153.6-153.6c0-84.831 68.769-153.6 153.6-153.6s153.6 68.769 153.6 153.6z"],"width":1075,"attrs":[{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["recording-status"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"}],"properties":{"order":5,"id":12,"name":"recording-status","prevSize":32,"code":59807},"setIdx":0,"setId":1,"iconIdx":13},{"icon":{"paths":["M308.847 697.119c-51.214 0-94.87-18.068-130.966-54.211-36.096-36.137-54.144-79.841-54.144-131.112 0-51.278 18.048-94.912 54.144-130.902 36.096-35.997 79.751-53.994 130.966-53.994h116.021c7.858 0 14.442 2.638 19.754 7.914 5.319 5.276 7.978 11.815 7.978 19.616 0 7.794-2.659 14.4-7.978 19.819-5.312 5.411-11.897 8.117-19.754 8.117h-116.075c-35.797 0-66.343 12.65-91.637 37.952-25.302 25.301-37.952 55.865-37.952 91.692 0 35.825 12.65 66.386 37.952 91.689 25.294 25.303 55.84 37.955 91.637 37.955h116.075c7.858 0 14.442 2.637 19.754 7.91 5.319 5.274 7.978 11.807 7.978 19.61 0 7.798-2.659 14.408-7.978 19.814-5.312 5.422-11.897 8.131-19.754 8.131h-116.021zM397.103 539.74c-7.843 0-14.474-2.637-19.893-7.91-5.426-5.279-8.138-11.812-8.138-19.61s2.652-14.405 7.957-19.816c5.312-5.419 11.89-8.128 19.734-8.128h230.141c7.844 0 14.479 2.638 19.896 7.914 5.427 5.276 8.136 11.811 8.136 19.606 0 7.802-2.652 14.407-7.956 19.818-5.309 5.417-11.889 8.125-19.732 8.125h-230.143zM599.142 697.119c-7.859 0-14.444-2.637-19.758-7.916-5.32-5.274-7.977-11.812-7.977-19.615 0-7.793 2.657-14.403 7.977-19.82 5.315-5.412 11.899-8.115 19.758-8.115h116.070c35.799 0 66.345-12.652 91.638-37.955 25.303-25.303 37.955-55.864 37.955-91.689 0-35.827-12.652-66.391-37.955-91.692-25.293-25.302-55.839-37.952-91.638-37.952h-116.070c-7.859 0-14.444-2.638-19.758-7.915-5.32-5.269-7.977-11.804-7.977-19.605s2.657-14.407 7.977-19.818c5.315-5.419 11.899-8.128 19.758-8.128h116.019c51.215 0 94.868 18.069 130.964 54.208s54.144 79.843 54.144 131.112c0 51.282-18.048 94.915-54.144 130.903-36.096 35.999-79.749 53.996-130.964 53.996h-116.019z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["copy-link"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":854,"id":13,"name":"copy-link","prevSize":32,"code":59806},"setIdx":0,"setId":1,"iconIdx":14},{"icon":{"paths":["M460.553 655.586l185.847-119.381c9.135-5.803 13.7-13.854 13.7-24.166 0-10.308-4.565-18.389-13.7-24.243l-185.847-119.384c-9.57-6.564-19.392-7.090-29.457-1.577-10.063 5.513-15.095 14.106-15.095 25.781v238.766c0 11.678 5.032 20.271 15.095 25.783 10.065 5.513 19.887 4.988 29.457-1.579zM512.068 917.333c-56.060 0-108.754-10.641-158.082-31.915-49.329-21.278-92.238-50.155-128.727-86.626s-65.378-79.364-86.663-128.67c-21.286-49.306-31.929-101.99-31.929-158.050 0-56.064 10.638-108.758 31.915-158.086s50.151-92.238 86.624-128.727c36.474-36.49 79.364-65.378 128.671-86.663 49.306-21.286 101.991-31.929 158.051-31.929s108.757 10.638 158.084 31.915c49.331 21.277 92.237 50.151 128.73 86.624 36.489 36.474 65.374 79.364 86.66 128.671 21.286 49.306 31.932 101.991 31.932 158.051s-10.641 108.757-31.915 158.084c-21.278 49.331-50.155 92.237-86.626 128.73-36.471 36.489-79.364 65.374-128.67 86.66s-101.99 31.932-158.054 31.932z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["play-circle"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":853,"id":14,"name":"play-circle","prevSize":32,"code":59805},"setIdx":0,"setId":1,"iconIdx":15},{"icon":{"paths":["M268.011 490.335l116.705 116.751c5.364 5.364 8.176 11.591 8.437 18.676 0.26 7.080-2.422 13.6-8.046 19.549-5.624 5.564-11.913 8.28-18.865 8.148-6.952-0.127-13.24-3.009-18.864-8.631l-158.183-158.244c-6.666-6.421-9.999-13.914-9.999-22.476s3.333-16.178 9.999-22.846l158.183-158.242c5.624-5.627 11.968-8.505 19.033-8.635s13.297 2.748 18.695 8.635c5.624 5.4 8.436 11.569 8.436 18.506s-2.812 13.219-8.436 18.846l-117.096 117.139h355.503c51.078 0 94.652 18.040 130.716 54.121 36.069 36.079 54.101 79.667 54.101 130.765v99.557c0 7.495-2.506 13.77-7.519 18.827s-11.235 7.587-18.666 7.587c-7.436 0-13.731-2.531-18.885-7.587s-7.734-11.332-7.734-18.827v-99.557c0-36.464-12.888-67.594-38.668-93.379-25.776-25.79-56.891-38.683-93.345-38.683h-355.503z"],"width":975,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["reply"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":863,"id":15,"name":"reply","prevSize":32,"code":59801},"setIdx":0,"setId":1,"iconIdx":16},{"icon":{"paths":["M182.013 420.216l273.206 273.307c10.24 10.242 15.609 22.127 16.107 35.654 0.496 13.525-4.624 25.968-15.36 37.328-10.738 10.62-22.743 15.805-36.015 15.555s-25.276-5.743-36.013-16.485l-301.578-301.688c-6.236-6.24-10.846-12.936-13.829-20.086s-4.475-14.882-4.475-23.197c0-8.315 1.492-16.047 4.475-23.197s7.457-13.969 13.421-20.453l301.985-302.097c10.737-10.742 22.849-16.237 36.337-16.485 13.488-0.249 25.385 5.245 35.691 16.485 10.736 10.31 16.105 22.086 16.105 35.329s-5.369 25.237-16.105 35.978l-273.953 274.052zM570.341 470.639l222.801 222.885c10.24 10.242 15.609 22.127 16.107 35.654 0.497 13.525-4.623 25.968-15.361 37.328-10.736 10.62-22.74 15.805-36.012 15.555s-25.277-5.743-36.015-16.485l-301.576-301.688c-6.237-6.24-10.847-12.936-13.831-20.086-2.982-7.15-4.472-14.882-4.472-23.197s1.49-16.047 4.472-23.197c2.984-7.151 7.457-13.969 13.422-20.453l301.985-302.097c10.738-10.742 22.85-16.237 36.336-16.485 13.489-0.249 25.385 5.245 35.691 16.485 10.738 10.31 16.107 22.086 16.107 35.329s-5.369 25.237-16.107 35.978l-223.547 223.629h433.821c97.513 0 180.699 34.441 249.549 103.322 68.859 68.881 103.284 152.097 103.284 249.647v190.059c0 14.311-4.785 26.29-14.355 35.943s-21.448 14.485-35.635 14.485c-14.196 0-26.214-4.831-36.054-14.485s-14.764-21.632-14.764-35.943v-190.059c0-69.619-24.604-129.044-73.812-178.277-49.217-49.231-108.618-73.846-178.213-73.846h-433.821z"],"width":1396,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["reply_all"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":862,"id":16,"name":"reply_all","prevSize":32,"code":59802},"setIdx":0,"setId":1,"iconIdx":17},{"icon":{"paths":["M327.825 795.243c-17.959 0-33.333-6.393-46.121-19.188s-19.184-28.175-19.184-46.139v-475.115h-22.341c-7.481 0-13.751-2.512-18.812-7.536s-7.591-11.249-7.591-18.675c0-7.427 2.53-13.719 7.591-18.876s11.331-7.737 18.812-7.737h148.262v-12.19c0-8.799 3.112-16.312 9.338-22.54s13.736-9.342 22.532-9.342h131.234c8.797 0 16.306 3.114 22.533 9.342s9.338 13.742 9.338 22.54v12.19h148.261c7.48 0 13.751 2.512 18.812 7.537 5.061 5.023 7.592 11.249 7.592 18.676s-2.531 13.719-7.592 18.876c-5.061 5.158-11.332 7.736-18.812 7.736h-22.343v474.671c0 18.466-6.393 34.041-19.183 46.733-12.785 12.693-28.16 19.037-46.119 19.037h-316.207zM656.53 254.802h-341.205v475.115c0 3.647 1.172 6.641 3.516 8.987s5.338 3.516 8.984 3.516h316.207c3.647 0 6.641-1.17 8.982-3.516 2.345-2.345 3.516-5.339 3.516-8.987v-475.115zM431.048 669.277c7.424 0 13.713-2.531 18.869-7.592s7.734-11.337 7.734-18.817v-288.511c0-7.483-2.511-13.755-7.534-18.818s-11.245-7.594-18.669-7.594c-7.424 0-13.714 2.531-18.869 7.594s-7.733 11.335-7.733 18.818v288.511c0 7.48 2.511 13.756 7.533 18.817s11.245 7.592 18.669 7.592zM540.409 669.277c7.426 0 13.712-2.531 18.871-7.592 5.154-5.061 7.734-11.337 7.734-18.817v-288.511c0-7.483-2.511-13.755-7.534-18.818s-11.244-7.594-18.671-7.594c-7.422 0-13.712 2.531-18.866 7.594-5.159 5.062-7.739 11.335-7.739 18.818v288.511c0 7.48 2.511 13.756 7.534 18.817s11.249 7.592 18.671 7.592z"],"width":975,"attrs":[{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["delete"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"}],"properties":{"order":10,"id":17,"name":"delete","prevSize":32,"code":59803},"setIdx":0,"setId":1,"iconIdx":18},{"icon":{"paths":["M781.073 862.779l-103.97-104.009h-461.817v-70.012c0-14.336 3.711-27.648 11.132-39.936 7.421-12.293 17.797-22.318 31.128-30.081 31.715-18.388 64.862-32.875 99.441-43.447 34.579-10.576 69.913-16.647 106.003-18.208h6.445c2.213 0 4.361 0.127 6.444 0.39l-356.752-356.885 37.338-37.352 661.944 662.189-37.337 37.352zM268.091 705.946h356.208l-94.52-95.568c-7.29-0.522-14.351-0.975-21.172-1.37-6.822-0.39-13.878-0.585-21.168-0.585-35.777 0-70.811 4.613-105.104 13.834s-66.567 22.791-96.824 40.711c-5.434 3.033-9.696 6.802-12.785 11.298s-4.635 9.328-4.635 14.492v17.189zM726.938 623.036c5.988 3.803 10.83 8.426 14.531 13.873 3.696 5.442 6.090 11.654 7.183 18.632l-48.191-48.206c4.54 2.438 9.016 4.886 13.429 7.329s8.763 5.232 13.049 8.372zM557.115 460.184l-39.6-38.602c13.044-5.996 23.479-14.967 31.305-26.916 7.821-11.949 11.732-25.006 11.732-39.17 0-20.073-7.119-37.22-21.363-51.443s-31.364-21.333-51.366-21.333c-14.109 0-27.149 3.913-39.119 11.739s-20.957 18.265-26.961 31.317l-38.588-39.618c11.457-18.129 26.52-32.039 45.189-41.728s38.367-14.535 59.094-14.535c35.153 0 64.927 12.203 89.323 36.61 24.4 24.407 36.596 54.193 36.596 89.357 0 20.734-4.842 40.439-14.526 59.115-9.689 18.676-23.591 33.745-41.716 45.206z"],"width":975,"attrs":[{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["block_user"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"}],"properties":{"order":11,"id":18,"name":"block_user","prevSize":32,"code":59804},"setIdx":0,"setId":1,"iconIdx":19},{"icon":{"paths":["M798.848 547.797l-595.526 251.072c-12.854 5.146-25.065 4.036-36.634-3.319-11.57-7.36-17.355-18.039-17.355-32.043v-502.97c0-14.004 5.785-24.684 17.355-32.041s23.781-8.465 36.634-3.323l595.526 251.077c15.863 7.002 23.791 18.927 23.791 35.772 0 16.849-7.927 28.774-23.791 35.776zM213.332 725.355l505.601-213.333-505.601-213.332v157.537l231.383 55.795-231.383 55.795v157.538z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_send"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":860,"id":19,"name":"chat_send","prevSize":32,"code":59768},"setIdx":0,"setId":1,"iconIdx":20},{"icon":{"paths":["M203.375 798.861c-12.889 5.15-25.118 4.053-36.687-3.285s-17.355-18.027-17.355-32.068v-180.1l295.381-71.386-295.381-71.381v-180.102c0-14.040 5.785-24.73 17.355-32.068s23.798-8.434 36.687-3.284l595.473 251.064c15.863 7.104 23.791 19.055 23.791 35.853 0 16.794-7.927 28.693-23.791 35.695l-595.473 251.063z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_send_fill"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":859,"id":20,"name":"chat_send_fill","prevSize":32,"code":59771},"setIdx":0,"setId":1,"iconIdx":21},{"icon":{"paths":["M657.327 461.129c15.522 0 28.689-5.436 39.492-16.303s16.205-24.065 16.205-39.59c0-15.525-5.436-28.689-16.303-39.492s-24.064-16.205-39.586-16.205c-15.526 0-28.689 5.434-39.492 16.302-10.807 10.867-16.209 24.064-16.209 39.589s5.436 28.691 16.303 39.494c10.867 10.803 24.064 16.205 39.59 16.205zM366.865 461.129c15.525 0 28.689-5.436 39.492-16.303s16.206-24.065 16.206-39.59c0-15.525-5.434-28.689-16.302-39.492s-24.064-16.205-39.589-16.205c-15.525 0-28.689 5.434-39.494 16.302s-16.205 24.064-16.205 39.589c0 15.525 5.434 28.691 16.302 39.494s24.064 16.205 39.589 16.205zM512.068 917.333c-56.060 0-108.754-10.641-158.082-31.915-49.329-21.278-92.238-50.155-128.727-86.626s-65.378-79.364-86.663-128.67c-21.286-49.306-31.929-101.99-31.929-158.050 0-56.064 10.638-108.758 31.915-158.086s50.151-92.238 86.624-128.727c36.474-36.49 79.364-65.378 128.671-86.663 49.306-21.286 101.991-31.929 158.051-31.929s108.757 10.638 158.084 31.915c49.331 21.277 92.237 50.151 128.73 86.624 36.489 36.474 65.374 79.364 86.66 128.671 21.286 49.306 31.932 101.991 31.932 158.051s-10.641 108.757-31.915 158.084c-21.278 49.331-50.155 92.237-86.626 128.73-36.471 36.489-79.364 65.374-128.67 86.66s-101.99 31.932-158.054 31.932zM512 853.333c95.287 0 176-33.067 242.133-99.2s99.2-146.846 99.2-242.133c0-95.29-33.067-176.001-99.2-242.134s-146.846-99.2-242.133-99.2c-95.29 0-176.001 33.067-242.134 99.2s-99.2 146.844-99.2 242.134c0 95.287 33.067 176 99.2 242.133s146.844 99.2 242.134 99.2zM512 733.538c38.293 0 73.617-9.067 105.971-27.2s58.628-42.735 78.822-73.805c4.036-7.77 3.908-15.578-0.384-23.428-4.297-7.846-11.008-11.772-20.143-11.772h-328.533c-9.135 0-15.85 3.925-20.144 11.772-4.294 7.851-4.422 15.659-0.383 23.428 20.194 31.070 46.633 55.671 79.315 73.805 32.685 18.133 67.842 27.2 105.478 27.2z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_emoji"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":858,"id":21,"name":"chat_emoji","prevSize":32,"code":59772},"setIdx":0,"setId":1,"iconIdx":22},{"icon":{"paths":["M298.667 938.667c-70.692 0-128-57.306-128-128v-682.667c0-23.564 19.102-42.667 42.667-42.667h408.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v494.327c0 70.694-57.306 128-128 128h-426.667z","M597.333 298.667v-213.333h24.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v24.994h-213.333c-23.565 0-42.667-19.103-42.667-42.667z","M170.667 554.667h496.926c23.564 0 42.667 19.103 42.667 42.667v170.667c0 23.564-19.103 42.667-42.667 42.667h-496.926c-23.564 0-42.667-19.103-42.667-42.667v-170.667c0-23.564 19.103-42.667 42.667-42.667z","M226.979 752.235v-139.093h39.253c14.223 0 26.525 2.419 36.907 7.253 10.383 4.838 18.418 12.373 24.107 22.613 5.831 10.099 8.747 23.113 8.747 39.040 0 15.932-2.844 29.086-8.533 39.467-5.689 10.385-13.653 18.133-23.893 23.253-10.097 4.979-21.973 7.467-35.627 7.467h-40.96zM258.339 726.848h5.973c7.965 0 14.863-1.421 20.693-4.267 5.973-2.842 10.596-7.535 13.867-14.080 3.271-6.541 4.907-15.36 4.907-26.453s-1.635-19.767-4.907-26.027c-3.271-6.4-7.893-10.88-13.867-13.44-5.831-2.701-12.729-4.053-20.693-4.053h-5.973v88.32z","M418.929 754.795c-12.515 0-23.537-2.914-33.067-8.747-9.387-5.828-16.782-14.148-22.187-24.96-5.262-10.948-7.893-23.962-7.893-39.040 0-15.215 2.631-28.087 7.893-38.613 5.405-10.667 12.8-18.773 22.187-24.32 9.529-5.687 20.551-8.533 33.067-8.533s23.469 2.846 32.855 8.533c9.527 5.547 16.922 13.653 22.187 24.32 5.402 10.667 8.107 23.539 8.107 38.613 0 15.078-2.705 28.092-8.107 39.040-5.265 10.812-12.659 19.132-22.187 24.96-9.387 5.833-20.339 8.747-32.855 8.747zM418.929 727.701c9.53 0 17.069-4.122 22.615-12.373 5.547-8.247 8.32-19.341 8.32-33.28 0-13.935-2.773-24.815-8.32-32.64-5.547-7.821-13.086-11.733-22.615-11.733s-17.067 3.913-22.613 11.733c-5.547 7.825-8.32 18.705-8.32 32.64 0 13.939 2.773 25.033 8.32 33.28 5.547 8.252 13.085 12.373 22.613 12.373z","M566.255 754.795c-11.661 0-22.4-2.701-32.213-8.107-9.813-5.402-17.707-13.367-23.68-23.893-5.828-10.667-8.747-23.748-8.747-39.253 0-15.36 2.987-28.442 8.96-39.253 6.118-10.948 14.153-19.268 24.107-24.96 10.099-5.828 20.979-8.747 32.64-8.747 8.96 0 16.998 1.852 24.107 5.547 7.113 3.558 13.013 7.753 17.707 12.587l-16.64 20.053c-3.554-3.268-7.322-5.901-11.307-7.893-3.84-2.133-8.247-3.2-13.227-3.2-6.255 0-12.015 1.779-17.28 5.333-5.12 3.558-9.242 8.678-12.373 15.36-2.987 6.686-4.48 14.72-4.48 24.107 0 14.225 3.059 25.318 9.173 33.28 6.118 7.966 14.293 11.947 24.533 11.947 5.692 0 10.739-1.28 15.147-3.84 4.553-2.56 8.533-5.615 11.947-9.173l16.64 19.627c-11.661 13.653-26.667 20.48-45.013 20.48z"],"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(18, 127, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":true,"tags":["chat_attachment_doc"],"grid":0},"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(18, 127, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":15,"id":22,"name":"chat_attachment_doc","prevSize":32,"code":59773,"codes":[59773,59774,59775,59776,59777,59778]},"setIdx":0,"setId":1,"iconIdx":23},{"icon":{"paths":["M298.667 938.667c-70.692 0-128-57.306-128-128v-682.667c0-23.564 19.102-42.667 42.667-42.667h408.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v494.327c0 70.694-57.306 128-128 128h-426.667z","M597.333 298.667v-213.333h24.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v24.994h-213.333c-23.565 0-42.667-19.103-42.667-42.667z","M298.667 469.333h426.667c23.564 0 42.667 19.103 42.667 42.667v298.667c0 23.564-19.103 42.667-42.667 42.667h-426.667c-23.564 0-42.667-19.103-42.667-42.667v-298.667c0-23.564 19.103-42.667 42.667-42.667z","M608.119 755.81l43.482-43.234c16.64-16.546 43.52-16.546 60.164 0l141.568 140.757h-341.333l96.119-97.523z","M367.119 624.316l-239.119 217.378 576 54.306-278.439-270.703c-16.163-15.714-41.762-16.145-58.443-0.981z","M640 597.333c0-23.564-19.103-42.667-42.667-42.667s-42.667 19.103-42.667 42.667c0 23.564 19.103 42.667 42.667 42.667s42.667-19.103 42.667-42.667z"],"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(114, 205, 255)"},{"fill":"rgb(128, 183, 74)","stroke":"rgb(128, 183, 74)","strokeLinejoin":"round","strokeLinecap":"round","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(78, 147, 27)","stroke":"rgb(78, 147, 27)","strokeLinejoin":"round","strokeLinecap":"round","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(245, 215, 69)"}],"isMulticolor":true,"isMulticolor2":true,"tags":["chat_attachment_image"],"grid":0},"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(114, 205, 255)"},{"fill":"rgb(128, 183, 74)","stroke":"rgb(128, 183, 74)","strokeLinejoin":"round","strokeLinecap":"round","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(78, 147, 27)","stroke":"rgb(78, 147, 27)","strokeLinejoin":"round","strokeLinecap":"round","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(245, 215, 69)"}],"properties":{"order":16,"id":23,"name":"chat_attachment_image","prevSize":32,"code":59779,"codes":[59779,59780,59781,59782,59783,59784]},"setIdx":0,"setId":1,"iconIdx":24},{"icon":{"paths":["M298.667 938.667c-70.692 0-128-57.306-128-128v-682.667c0-23.564 19.102-42.667 42.667-42.667h408.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v494.327c0 70.694-57.306 128-128 128h-426.667z","M597.333 298.667v-213.333h24.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v24.994h-213.333c-23.565 0-42.667-19.103-42.667-42.667z","M170.667 576h496.926c11.782 0 21.333 9.551 21.333 21.333v170.667c0 11.782-9.551 21.333-21.333 21.333h-496.926c-11.782 0-21.333-9.551-21.333-21.333v-170.667c0-11.782 9.551-21.333 21.333-21.333z","M244.042 752.175v-139.093h47.787c10.24 0 19.484 1.425 27.733 4.267 8.391 2.705 15.075 7.326 20.053 13.867 4.977 6.545 7.467 15.433 7.467 26.667 0 10.812-2.489 19.699-7.467 26.667-4.978 6.972-11.591 12.16-19.84 15.573-8.249 3.273-17.28 4.907-27.093 4.907h-17.28v47.147h-31.36zM275.402 680.068h15.36c17.067 0 25.6-7.394 25.6-22.187 0-7.253-2.276-12.373-6.827-15.36s-11.093-4.48-19.627-4.48h-14.507v42.027z","M371.125 752.175v-139.093h39.253c14.223 0 26.524 2.419 36.905 7.253 10.385 4.838 18.419 12.373 24.107 22.613 5.833 10.099 8.747 23.113 8.747 39.040 0 15.932-2.842 29.086-8.533 39.467-5.687 10.385-13.653 18.133-23.893 23.253-10.095 4.979-21.972 7.467-35.625 7.467h-40.96zM402.485 726.788h5.973c7.965 0 14.863-1.421 20.692-4.267 5.973-2.842 10.598-7.535 13.867-14.080 3.273-6.541 4.907-15.36 4.907-26.453s-1.634-19.767-4.907-26.027c-3.268-6.4-7.893-10.88-13.867-13.44-5.829-2.701-12.727-4.053-20.692-4.053h-5.973v88.32z","M506.543 752.175v-139.093h87.68v26.453h-56.32v32.213h48.213v26.453h-48.213v53.973h-31.36z"],"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(255, 94, 72)","stroke":"rgb(255, 94, 72)","strokeLinejoin":"miter","strokeLinecap":"butt","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":true,"tags":["chat_attachment_pdf"],"grid":0},"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(0, 0, 0)","opacity":0.15},{"fill":"rgb(255, 94, 72)","stroke":"rgb(255, 94, 72)","strokeLinejoin":"miter","strokeLinecap":"butt","strokeMiterlimit":"4","strokeWidth":42.666666666666664},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":17,"id":24,"name":"chat_attachment_pdf","prevSize":32,"code":59785,"codes":[59785,59786,59787,59788,59789,59790]},"setIdx":0,"setId":1,"iconIdx":25},{"icon":{"paths":["M298.667 938.667c-70.692 0-128-57.306-128-128v-682.667c0-23.564 19.102-42.667 42.667-42.667h408.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v494.327c0 70.694-57.306 128-128 128h-426.667z","M170.667 554.667h496.926c23.564 0 42.667 19.103 42.667 42.667v170.667c0 23.564-19.103 42.667-42.667 42.667h-496.926c-23.564 0-42.667-19.103-42.667-42.667v-170.667c0-23.564 19.103-42.667 42.667-42.667z","M597.333 298.667v-213.333h24.994c11.315 0 22.17 4.495 30.17 12.497l188.339 188.34c8 8.001 12.497 18.854 12.497 30.17v24.994h-213.333c-23.565 0-42.667-19.103-42.667-42.667z"],"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(168, 174, 178)"},{"fill":"rgb(0, 0, 0)","opacity":0.15}],"isMulticolor":true,"isMulticolor2":true,"tags":["chat_attachment_unknown"],"grid":0},"attrs":[{"fill":"rgb(223, 227, 229)"},{"fill":"rgb(168, 174, 178)"},{"fill":"rgb(0, 0, 0)","opacity":0.15}],"properties":{"order":18,"id":25,"name":"chat_attachment_unknown","prevSize":32,"code":59791,"codes":[59791,59792,59793]},"setIdx":0,"setId":1,"iconIdx":26},{"icon":{"paths":["M414.933 233.026c44.54-53.079 100.907-82.751 169.11-89.014 68.198-6.263 128.794 12.835 181.781 57.295 52.983 44.461 82.368 100.862 88.145 169.202 5.781 68.339-13.598 129.049-58.138 182.127l-246.566 293.85c-31.646 37.713-71.637 58.778-119.979 63.202-48.34 4.425-91.367-9.186-129.079-40.832-37.713-31.642-58.588-71.654-62.626-120.030-4.038-48.371 9.765-91.418 41.409-129.131l233.381-278.131c18.564-22.124 42.121-34.546 70.665-37.265s53.892 5.217 76.049 23.807c22.153 18.59 34.534 42.222 37.141 70.896 2.611 28.676-5.457 54.182-24.205 76.527l-220.194 262.417c-5.828 6.946-13.065 10.795-21.709 11.55-8.64 0.755-16.436-1.783-23.385-7.616-6.948-5.828-10.798-13.065-11.549-21.7s1.787-16.427 7.615-23.373l220.197-262.417c7.347-8.758 10.594-18.604 9.732-29.536-0.858-10.932-5.666-20.073-14.426-27.421s-18.594-10.498-29.508-9.445c-10.918 1.052-20.049 5.958-27.396 14.715l-233.646 278.448c-19.918 24.418-28.685 51.887-26.302 82.415s15.681 55.949 39.894 76.267c24.111 20.233 51.558 28.907 82.341 26.035 30.783-2.876 56.302-16.384 76.556-40.521l246.566-293.845c33.489-39.232 48.030-84.301 43.614-135.208-4.416-50.904-26.364-92.923-65.852-126.055-38.929-32.666-83.639-46.583-134.131-41.751s-92.655 26.724-126.481 65.676l-233.38 278.132c-5.828 6.946-13.063 10.795-21.705 11.55s-16.438-1.783-23.386-7.612c-6.949-5.833-10.799-13.065-11.549-21.7s1.788-16.427 7.616-23.373l233.381-278.134z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_attachment"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":852,"id":26,"name":"chat_attachment","prevSize":32,"code":59794},"setIdx":0,"setId":1,"iconIdx":27},{"icon":{"paths":["M226.462 874.667c-21.552 0-39.795-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-571.076c0-21.552 7.467-39.795 22.4-54.729s33.176-22.4 54.729-22.4h571.076c21.551 0 39.795 7.467 54.729 22.4s22.4 33.176 22.4 54.729v571.076c0 21.551-7.467 39.795-22.4 54.729s-33.178 22.4-54.729 22.4h-571.076zM226.462 810.667h571.076c3.281 0 6.289-1.37 9.024-4.105s4.105-5.743 4.105-9.024v-571.076c0-3.282-1.37-6.292-4.105-9.027s-5.743-4.102-9.024-4.102h-571.076c-3.282 0-6.292 1.367-9.027 4.102s-4.102 5.745-4.102 9.027v571.076c0 3.281 1.367 6.289 4.102 9.024s5.745 4.105 9.027 4.105z","M405.333 597.333h-42.667c-12.089 0-22.222-4.087-30.4-12.267s-12.267-18.313-12.267-30.4v-85.333c0-12.087 4.089-22.221 12.267-30.4s18.311-12.267 30.4-12.267h64c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933c0 5.687-2.133 10.667-6.4 14.933s-9.246 6.4-14.933 6.4h-64v85.333h42.667v-21.333c0-5.687 2.133-10.667 6.4-14.933s9.245-6.4 14.933-6.4c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933v21.333c0 12.087-4.087 22.221-12.267 30.4s-18.311 12.267-30.4 12.267z","M526.933 590.933c-4.267 4.267-9.246 6.4-14.933 6.4s-10.667-2.133-14.933-6.4c-4.267-4.267-6.4-9.246-6.4-14.933v-128c0-5.687 2.133-10.667 6.4-14.933s9.246-6.4 14.933-6.4c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933v128c0 5.687-2.133 10.667-6.4 14.933z","M612.267 590.933c-4.267 4.267-9.246 6.4-14.933 6.4s-10.667-2.133-14.933-6.4c-4.267-4.267-6.4-9.246-6.4-14.933v-128c0-5.687 2.133-10.667 6.4-14.933s9.246-6.4 14.933-6.4h85.333c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933c0 5.687-2.133 10.667-6.4 14.933s-9.246 6.4-14.933 6.4h-64v21.333h42.667c5.687 0 10.667 2.133 14.933 6.4s6.4 9.246 6.4 14.933c0 5.687-2.133 10.667-6.4 14.933s-9.246 6.4-14.933 6.4h-42.667v42.667c0 5.687-2.133 10.667-6.4 14.933z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_gif"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":851,"id":27,"name":"chat_gif","prevSize":32,"code":59795},"setIdx":0,"setId":1,"iconIdx":28},{"icon":{"paths":["M171.733 852.267c14.933 14.933 33.176 22.4 54.729 22.4h571.076c21.551 0 39.795-7.467 54.729-22.4s22.4-33.178 22.4-54.729v-571.076c0-21.552-7.467-39.795-22.4-54.729s-33.178-22.4-54.729-22.4h-571.076c-21.552 0-39.795 7.467-54.729 22.4s-22.4 33.176-22.4 54.729v571.076c0 21.551 7.467 39.795 22.4 54.729zM405.333 597.333h-42.667c-12.089 0-22.222-4.092-30.4-12.267-8.178-8.179-12.267-18.313-12.267-30.4v-85.333c0-12.092 4.089-22.225 12.267-30.4 8.178-8.179 18.311-12.268 30.4-12.268h64c5.687 0 10.667 2.135 14.933 6.401s6.4 9.242 6.4 14.933c0 5.687-2.133 10.667-6.4 14.933s-9.246 6.4-14.933 6.4h-64v85.333h42.667v-21.333c0-5.692 2.133-10.667 6.4-14.933s9.245-6.4 14.933-6.4c5.687 0 10.667 2.133 14.933 6.4s6.4 9.242 6.4 14.933v21.333c0 12.087-4.087 22.221-12.267 30.4-8.179 8.175-18.311 12.267-30.4 12.267zM526.933 590.933c-4.267 4.267-9.246 6.4-14.933 6.4s-10.667-2.133-14.933-6.4c-4.267-4.267-6.4-9.246-6.4-14.933v-128c0-5.692 2.133-10.667 6.4-14.933s9.246-6.401 14.933-6.401c5.687 0 10.667 2.135 14.933 6.401s6.4 9.242 6.4 14.933v128c0 5.687-2.133 10.667-6.4 14.933zM597.333 597.333c5.687 0 10.667-2.133 14.933-6.4s6.4-9.246 6.4-14.933v-42.667h42.667c5.687 0 10.667-2.133 14.933-6.4s6.4-9.246 6.4-14.933c0-5.692-2.133-10.667-6.4-14.933s-9.246-6.4-14.933-6.4h-42.667v-21.333h64c5.687 0 10.667-2.133 14.933-6.4s6.4-9.246 6.4-14.933c0-5.692-2.133-10.667-6.4-14.933s-9.246-6.401-14.933-6.401h-85.333c-5.687 0-10.667 2.135-14.933 6.401s-6.4 9.242-6.4 14.933v128c0 5.687 2.133 10.667 6.4 14.933s9.246 6.4 14.933 6.4z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat_gif_fill"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":850,"id":28,"name":"chat_gif_fill","prevSize":32,"code":59796},"setIdx":0,"setId":1,"iconIdx":29},{"icon":{"paths":["M917.333 512c0 223.859-181.474 405.333-405.333 405.333s-405.333-181.474-405.333-405.333c0-223.859 181.474-405.333 405.333-405.333s405.333 181.474 405.333 405.333z","M458.509 437.615c-13.248 13.355-29.466 20.032-48.649 20.032-19.185 0-35.401-6.626-48.65-19.874s-19.872-29.564-19.872-48.949c0-19.386 6.624-35.703 19.872-48.951s29.566-19.873 48.951-19.873c18.95 0 35.056 6.725 48.321 20.174s19.9 29.713 19.9 48.791c0 19.078-6.626 35.295-19.874 48.65z","M714.509 501.615c-13.248 13.355-29.466 20.032-48.649 20.032s-35.401-6.626-48.649-19.874c-13.248-13.248-19.874-29.564-19.874-48.951 0-19.383 6.626-35.7 19.874-48.948s29.564-19.873 48.951-19.873c18.948 0 35.055 6.725 48.32 20.174s19.9 29.712 19.9 48.793c0 19.076-6.626 35.294-19.874 48.649z","M563.648 727.266c-35.942 9.139-72.41 8.755-109.402-1.156-36.352-9.741-67.967-27.597-94.844-53.572s-46.046-56.58-57.51-91.819c-1.891-8.546 0.254-16.060 6.433-22.528 6.18-6.473 13.681-8.525 22.505-6.161l317.336 85.030c8.823 2.364 14.293 7.893 16.41 16.589 2.116 8.691 0.218 16.269-5.692 22.724-27.55 24.785-59.294 41.749-95.236 50.893z"],"attrs":[{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(0, 0, 0)"}],"isMulticolor":true,"isMulticolor2":false,"tags":["chat_emoji_fill"],"grid":0},"attrs":[{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(0, 0, 0)"}],"properties":{"order":790,"id":29,"name":"chat_emoji_fill","prevSize":32,"code":59797,"codes":[59797,59798,59799,59800]},"setIdx":0,"setId":1,"iconIdx":30},{"icon":{"paths":["M311.795 874.667c-21.279 0-39.453-7.535-54.523-22.605s-22.606-33.246-22.606-54.524v-541.536h-10.667c-9.080 0-16.684-3.063-22.81-9.189s-9.19-13.73-9.19-22.81c0-9.080 3.063-16.684 9.19-22.81s13.729-9.19 22.81-9.19h159.999c0-10.448 3.679-19.35 11.036-26.707s16.259-11.036 26.707-11.036h180.515c10.445 0 19.349 3.679 26.705 11.036 7.36 7.357 11.038 16.259 11.038 26.707h160c9.079 0 16.683 3.063 22.805 9.19 6.127 6.126 9.195 13.729 9.195 22.81s-3.068 16.684-9.195 22.81c-6.123 6.126-13.726 9.189-22.805 9.189h-10.667v541.536c0 21.278-7.539 39.454-22.609 54.524s-33.242 22.605-54.524 22.605h-400.405zM725.333 256.002h-426.668v541.536c0 3.831 1.231 6.976 3.693 9.438s5.608 3.695 9.437 3.695h400.405c3.831 0 6.976-1.233 9.438-3.695s3.695-5.606 3.695-9.438v-541.536zM512 578.3l88.452 88.452c5.905 5.905 13.333 8.93 22.276 9.067s16.503-2.889 22.686-9.067c6.182-6.182 9.271-13.675 9.271-22.481s-3.089-16.303-9.271-22.481l-88.452-88.452 88.452-88.452c5.905-5.909 8.93-13.333 9.067-22.278 0.137-8.943-2.884-16.505-9.067-22.686s-13.675-9.272-22.481-9.272c-8.806 0-16.303 3.091-22.481 9.272l-88.452 88.45-88.454-88.45c-5.907-5.908-13.332-8.93-22.276-9.067s-16.505 2.885-22.685 9.067c-6.182 6.181-9.273 13.675-9.273 22.481 0 8.809 3.091 16.301 9.273 22.483l88.449 88.452-88.449 88.452c-5.908 5.905-8.931 13.329-9.067 22.276-0.137 8.943 2.885 16.503 9.067 22.686 6.181 6.178 13.674 9.271 22.481 9.271s16.3-3.093 22.481-9.271l88.454-88.452z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["clear-all"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":838,"id":30,"name":"clear-all","prevSize":32,"code":59759},"setIdx":0,"setId":1,"iconIdx":31},{"icon":{"paths":["M724.514 767.996h169.024c9.084 0 16.687 3.063 22.81 9.19 6.127 6.127 9.19 13.73 9.19 22.81s-3.063 16.683-9.19 22.81c-6.123 6.127-13.726 9.19-22.81 9.19h-233.024l64-64zM214.647 831.996c-5.141 0-10.037-0.93-14.687-2.79s-8.971-4.787-12.964-8.781l-63.917-63.915c-14.988-14.989-22.659-33.203-23.014-54.647s6.96-40.013 21.948-55.71l451.283-468.349c14.985-15.699 33.092-23.48 54.315-23.343 21.227 0.137 39.33 7.699 54.319 22.687l193.395 193.393c14.985 14.988 22.549 33.272 22.686 54.851s-7.292 39.863-22.276 54.852l-331.652 340.181c-3.994 3.994-8.384 6.921-13.167 8.781-4.787 1.86-9.749 2.79-14.895 2.79h-301.372zM505.271 767.996l325.5-334.029c2.185-2.189 3.281-5.333 3.281-9.437s-1.097-7.249-3.281-9.437l-192.738-192.737c-2.189-2.188-5.265-3.282-9.233-3.282-3.964 0-7.044 1.231-9.229 3.693l-452.596 467.525c-2.188 2.462-3.282 5.679-3.282 9.643 0 3.968 1.094 7.044 3.282 9.233l58.83 58.829h279.466z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["erasor"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":839,"id":31,"name":"erasor","prevSize":32,"code":59760},"setIdx":0,"setId":1,"iconIdx":32},{"icon":{"paths":["M370.707 833.643l-223.016-223.014c-6.563-6.566-11.486-13.841-14.769-21.828-3.281-7.983-4.922-16.329-4.922-25.024 0-8.149 1.641-16.218 4.922-24.205 3.283-7.987 8.206-15.262 14.769-21.824l237.129-236.884-106.256-102.975c-4.868-4.868-7.344-10.42-7.426-16.656s2.312-11.87 7.18-16.902c4.868-5.033 10.502-7.549 16.902-7.549s12.116 2.516 17.149 7.549l373.417 374.237c6.562 6.562 11.443 13.837 14.647 21.824 3.2 7.987 4.796 16.055 4.796 24.205 0 8.7-1.596 17.041-4.796 25.028-3.204 7.983-8.085 15.262-14.647 21.824l-222.195 222.195c-6.566 6.562-13.841 11.486-21.828 14.771-7.983 3.281-16.052 4.919-24.203 4.919-8.698 0-17.040-1.638-25.026-4.919-7.986-3.285-15.262-8.209-21.826-14.771zM416.738 312.781l-233.19 233.19c-3.281 3.281-5.469 6.562-6.564 9.847-1.094 3.281-1.641 6.562-1.641 9.843h482.79c0-3.281-0.546-6.562-1.643-9.843-1.092-3.285-3.281-6.566-6.562-9.847l-233.19-233.19zM824.286 853.333c-20.13 0-36.894-7.014-50.295-21.043-13.402-14.033-20.105-31.194-20.105-51.49 0-15.369 3.298-30.221 9.89-44.553s15.108-27.759 25.557-40.286l15.343-19.038c5.308-6.182 11.883-9.31 19.733-9.395 7.851-0.081 14.43 2.97 19.733 9.152l17.233 19.281c10.283 12.527 18.761 25.954 25.434 40.286s10.010 29.184 10.010 44.553c0 20.297-7.057 37.457-21.167 51.49-14.114 14.029-31.236 21.043-51.366 21.043z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["color-picker"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":840,"id":32,"name":"color-picker","prevSize":32,"code":59763},"setIdx":0,"setId":1,"iconIdx":33},{"icon":{"paths":["M576 362.667c0-117.821-95.514-213.333-213.333-213.333s-213.333 95.513-213.333 213.333c0 117.82 95.513 213.333 213.333 213.333s213.333-95.514 213.333-213.333zM640 362.667c0 153.169-124.164 277.333-277.333 277.333-153.167 0-277.333-124.164-277.333-277.333 0-153.167 124.166-277.333 277.333-277.333 153.169 0 277.333 124.166 277.333 277.333zM298.667 718.221c0-11.78 9.551-21.333 21.333-21.333h17.067c11.782 0 21.333 9.553 21.333 21.333v96.713c0 11.78 9.551 21.333 21.333 21.333h435.2c11.78 0 21.333-9.553 21.333-21.333v-435.2c0-11.782-9.553-21.333-21.333-21.333h-96.713c-11.78 0-21.333-9.551-21.333-21.333v-17.067c0-11.782 9.553-21.333 21.333-21.333h156.446c11.78 0 21.333 9.551 21.333 21.333v554.667c0 11.78-9.553 21.333-21.333 21.333h-554.667c-11.782 0-21.333-9.553-21.333-21.333v-156.446z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["shapes"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":841,"id":33,"name":"shapes","prevSize":32,"code":59764},"setIdx":0,"setId":1,"iconIdx":34},{"icon":{"paths":["M196.186 873.028c-13.565 3.008-25.244-0.384-35.036-10.176s-13.182-21.47-10.174-35.034l35.692-171.319 180.838 180.838-171.321 35.691zM367.506 837.338l-180.838-180.838 478.689-478.688c14.711-14.714 32.926-22.071 54.643-22.071s39.932 7.357 54.647 22.071l71.548 71.548c14.711 14.714 22.071 32.929 22.071 54.645s-7.36 39.931-22.071 54.645l-478.689 478.688zM710.976 222.774l-436.269 435.854 90.667 90.667 435.855-436.265c2.462-2.462 3.695-5.539 3.695-9.231s-1.233-6.77-3.695-9.232l-71.791-71.793c-2.462-2.462-5.538-3.693-9.233-3.693-3.691 0-6.767 1.231-9.229 3.693z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["pen"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":842,"id":34,"name":"pen","prevSize":32,"code":59765},"setIdx":0,"setId":1,"iconIdx":35},{"icon":{"paths":["M544.956 608.469l264.418-2.867c9.037 0.021 16.521-2.419 22.443-7.326s10.172-10.645 12.749-17.207c2.577-6.566 3.209-13.751 1.894-21.555-1.318-7.799-5.295-14.767-11.938-20.902l-463.807-417.010c-5.917-5.757-12.553-9.034-19.908-9.83s-14.399 0.274-21.129 3.21c-6.73 2.935-12.305 7.37-16.724 13.304s-6.527 13.027-6.319 21.281l-8.943 623.695c-0.023 8.845 2.35 16.418 7.12 22.716s10.44 10.739 17.014 13.321c6.574 2.577 13.735 3.311 21.484 2.189 7.749-1.118 14.664-4.992 20.745-11.618l180.902-191.398zM363.004 708.228l6.292-502.43 373.62 336.696-225.31 1.822-154.602 163.913z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["selector"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":843,"id":35,"name":"selector","prevSize":32,"code":59767},"setIdx":0,"setId":1,"iconIdx":36},{"icon":{"paths":["M716.739 875.075c-10.926 0-20.081-3.697-27.474-11.085-7.393-7.393-11.085-16.553-11.085-27.479v-121.846c0-10.993 3.948-20.101 11.853-27.325 7.905-7.219 17.49-10.829 28.759-10.829v-42.665c0-21.775 7.762-40.417 23.276-55.921 15.519-15.508 34.171-23.26 55.956-23.26 21.791 0 40.356 7.752 55.7 23.26 15.345 15.503 23.014 34.145 23.014 55.921v42.665c11.269 0 20.859 3.61 28.759 10.829 7.905 7.224 11.858 16.333 11.858 27.325v121.846c0 10.926-3.697 20.086-11.085 27.479-7.393 7.388-16.553 11.085-27.479 11.085h-162.053zM756.536 676.511h82.463v-42.665c0-12.088-3.82-22.016-11.448-29.788-7.629-7.767-17.49-11.648-29.578-11.648s-22.016 3.881-29.783 11.648c-7.772 7.772-11.653 17.7-11.653 29.788v42.665zM512.026 605.865c-32.002 0-59.202-11.197-81.602-33.597s-33.6-49.603-33.6-81.602c0-32 11.2-59.2 33.6-81.6s49.6-33.6 81.602-33.6c32 0 59.197 11.2 81.597 33.6s33.603 49.6 33.603 81.6c0 31.999-11.203 59.202-33.603 81.602s-49.597 33.597-81.597 33.597zM512.113 789.33c-90.151 0-174.392-23.89-252.723-71.67-78.332-47.78-137.381-112.225-177.147-193.336-2.791-5.315-4.829-10.726-6.114-16.223-1.286-5.497-1.929-11.309-1.929-17.436s0.643-11.938 1.929-17.436c1.285-5.497 3.323-10.906 6.114-16.224 39.766-81.109 98.813-145.553 177.144-193.335 78.329-47.781 162.542-71.671 252.639-71.671 94.249 0 180.47 25.039 258.662 75.118 78.198 50.078 137.641 117.483 178.34 202.214h-151.055c-21.284 0-41.431 2.968-60.436 8.902-19.005 5.936-36.47 14.483-52.388 25.641 0.276-2.243 0.481-4.444 0.62-6.605 0.133-2.161 0.205-4.363 0.205-6.606 0-48.318-16.922-89.39-50.765-123.213s-74.936-50.735-123.281-50.735c-48.345 0-89.406 16.921-123.184 50.762-33.778 33.843-50.667 74.937-50.667 123.282 0 48.343 16.912 89.405 50.735 123.182 33.823 33.782 74.894 50.668 123.214 50.668 15.263 0 30.003-1.915 44.227-5.745 14.218-3.825 27.459-9.216 39.711-16.164-1.26 5.197-2.094 10.353-2.504 15.468s-0.614 10.409-0.614 15.877v108.227c-13.455 2.017-26.911 3.697-40.366 5.038-13.455 1.347-26.911 2.017-40.366 2.017z"],"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["view-only"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":29,"id":36,"name":"view-only","prevSize":32,"code":59770},"setIdx":0,"setId":1,"iconIdx":37},{"icon":{"paths":["M853.333 352v-82.871c0-3.282-1.365-6.292-4.1-9.027-2.739-2.735-5.747-4.102-9.028-4.102h-82.871c-9.079 0-16.683-3.063-22.81-9.19s-9.19-13.729-9.19-22.81c0-9.080 3.063-16.684 9.19-22.81s13.73-9.19 22.81-9.19h82.871c21.551 0 39.795 7.467 54.729 22.4s22.4 33.176 22.4 54.729v82.871c0 9.080-3.063 16.684-9.19 22.81s-13.73 9.19-22.81 9.19c-9.079 0-16.683-3.063-22.81-9.19s-9.19-13.729-9.19-22.81zM106.668 352v-82.871c0-21.552 7.467-39.795 22.4-54.729s33.176-22.4 54.729-22.4h82.871c9.080 0 16.683 3.063 22.81 9.19s9.189 13.729 9.189 22.81c0 9.080-3.063 16.684-9.189 22.81s-13.73 9.19-22.81 9.19h-82.871c-3.283 0-6.292 1.367-9.027 4.102s-4.102 5.745-4.102 9.027v82.871c0 9.080-3.063 16.684-9.19 22.81s-13.729 9.19-22.809 9.19c-9.080 0-16.684-3.063-22.81-9.19s-9.19-13.729-9.19-22.81zM840.205 832h-82.871c-9.079 0-16.683-3.063-22.81-9.19s-9.19-13.73-9.19-22.81c0-9.084 3.063-16.687 9.19-22.81 6.127-6.127 13.73-9.19 22.81-9.19h82.871c3.281 0 6.289-1.37 9.028-4.105 2.735-2.735 4.1-5.743 4.1-9.024v-82.871c0-9.084 3.063-16.687 9.19-22.81 6.127-6.127 13.73-9.19 22.81-9.19s16.683 3.063 22.81 9.19c6.127 6.123 9.19 13.726 9.19 22.81v82.871c0 21.551-7.467 39.795-22.4 54.729s-33.178 22.4-54.729 22.4zM183.796 832c-21.553 0-39.796-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-82.871c0-9.084 3.063-16.687 9.19-22.81 6.127-6.127 13.73-9.19 22.81-9.19s16.683 3.063 22.809 9.19c6.127 6.123 9.19 13.726 9.19 22.81v82.871c0 3.281 1.367 6.289 4.102 9.024s5.745 4.105 9.027 4.105h82.871c9.080 0 16.683 3.063 22.81 9.19 6.126 6.123 9.189 13.726 9.189 22.81 0 9.079-3.063 16.683-9.189 22.81s-13.73 9.19-22.81 9.19h-82.871zM274.052 587.486v-150.972c0-21.28 7.535-39.454 22.605-54.525s33.245-22.605 54.522-22.605h321.64c21.278 0 39.454 7.535 54.524 22.605s22.605 33.245 22.605 54.525v150.972c0 21.278-7.535 39.454-22.605 54.524s-33.246 22.605-54.524 22.605h-321.64c-21.278 0-39.452-7.535-54.522-22.605s-22.605-33.246-22.605-54.524zM351.18 600.614h321.64c3.831 0 6.976-1.229 9.438-3.691s3.691-5.606 3.691-9.438v-150.972c0-3.831-1.229-6.976-3.691-9.438s-5.606-3.693-9.438-3.693h-321.64c-3.829 0-6.974 1.231-9.435 3.693s-3.693 5.606-3.693 9.438v150.972c0 3.831 1.231 6.976 3.693 9.438s5.606 3.691 9.435 3.691z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["fit-to-screen"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":844,"id":37,"name":"fit-to-screen","prevSize":32,"code":59769},"setIdx":0,"setId":1,"iconIdx":38},{"icon":{"paths":["M374.154 437.338h-50.872c-9.067 0-16.666-3.068-22.799-9.203-6.133-6.137-9.2-13.74-9.2-22.81s3.067-16.669 9.2-22.796c6.132-6.127 13.732-9.19 22.799-9.19h50.872v-50.872c0-9.067 3.068-16.667 9.204-22.8s13.739-9.199 22.81-9.199c9.070 0 16.669 3.066 22.794 9.199 6.127 6.133 9.19 13.734 9.19 22.8v50.872h50.871c9.067 0 16.666 3.068 22.801 9.203 6.131 6.136 9.199 13.74 9.199 22.81s-3.068 16.669-9.199 22.795c-6.135 6.127-13.734 9.19-22.801 9.19h-50.871v50.871c0 9.067-3.068 16.666-9.203 22.801-6.136 6.131-13.74 9.199-22.81 9.199-9.071 0-16.67-3.068-22.796-9.199-6.126-6.135-9.19-13.734-9.19-22.801v-50.871zM406.153 666.261c-72.925 0-134.642-25.25-185.154-75.75s-75.767-112.201-75.767-185.105c0-72.906 25.249-134.631 75.748-185.174s112.201-75.814 185.107-75.814c72.906 0 134.632 25.256 185.175 75.767s75.814 112.23 75.814 185.154c0 30.467-5.116 59.57-15.343 87.304-10.231 27.733-23.881 51.853-40.947 72.367l245.5 245.5c5.909 5.905 8.93 13.333 9.067 22.276s-2.884 16.503-9.067 22.686c-6.182 6.182-13.675 9.271-22.481 9.271s-16.299-3.089-22.481-9.271l-245.5-245.5c-21.333 17.617-45.867 31.398-73.6 41.357-27.733 9.954-56.422 14.933-86.071 14.933zM406.153 602.261c54.975 0 101.537-19.076 139.694-57.229 38.153-38.157 57.229-84.719 57.229-139.694s-19.076-101.539-57.229-139.693c-38.157-38.154-84.719-57.231-139.694-57.231s-101.539 19.077-139.693 57.231c-38.153 38.154-57.23 84.718-57.23 139.693s19.077 101.537 57.23 139.694c38.154 38.153 84.718 57.229 139.693 57.229z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["zoom-in"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":845,"id":38,"name":"zoom-in","prevSize":32,"code":59747},"setIdx":0,"setId":1,"iconIdx":39},{"icon":{"paths":["M343.115 444.753c-9.066 0-16.666-3.068-22.799-9.203s-9.2-13.739-9.2-22.809c0-9.071 3.066-16.669 9.2-22.796s13.733-9.189 22.799-9.189h139.488c9.067 0 16.666 3.068 22.801 9.203 6.131 6.136 9.199 13.74 9.199 22.81s-3.068 16.671-9.199 22.794c-6.135 6.127-13.734 9.19-22.801 9.19h-139.488zM412.859 673.677c-72.925 0-134.643-25.25-185.154-75.75-50.512-50.496-75.767-112.201-75.767-185.104 0-72.906 25.249-134.631 75.748-185.175s112.201-75.814 185.107-75.814c72.907 0 134.629 25.256 185.172 75.767 50.547 50.512 75.819 112.23 75.819 185.154 0 30.467-5.116 59.569-15.347 87.303-10.227 27.733-23.876 51.857-40.943 72.367l245.5 245.5c5.905 5.909 8.93 13.333 9.067 22.276s-2.884 16.508-9.067 22.686c-6.182 6.182-13.675 9.271-22.481 9.271s-16.303-3.089-22.481-9.271l-245.5-245.495c-21.333 17.613-45.867 31.398-73.6 41.353s-56.422 14.933-86.072 14.933zM412.859 609.677c54.976 0 101.539-19.076 139.691-57.229 38.157-38.153 57.233-84.719 57.233-139.693s-19.076-101.539-57.233-139.693c-38.153-38.154-84.715-57.231-139.691-57.231-54.975 0-101.539 19.077-139.693 57.231s-57.23 84.719-57.23 139.693c0 54.974 19.077 101.541 57.23 139.693s84.718 57.229 139.693 57.229z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["zoom-out"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":846,"id":39,"name":"zoom-out","prevSize":32,"code":59748},"setIdx":0,"setId":1,"iconIdx":40},{"icon":{"paths":["M339.55 787.14c-9.080 0-16.683-3.063-22.81-9.19s-9.189-13.73-9.189-22.81c0-9.079 3.063-16.683 9.189-22.81s13.73-9.186 22.81-9.186h274.218c44.523 0 82.842-14.703 114.953-44.105s48.162-65.711 48.162-108.924c0-43.213-16.051-79.454-48.162-108.719s-70.43-43.896-114.953-43.896h-299.574l96.247 96.248c6.181 6.178 9.271 13.675 9.271 22.481s-3.090 16.299-9.271 22.481c-6.181 6.182-13.743 9.203-22.687 9.067s-16.369-3.157-22.277-9.067l-146.214-146.215c-3.993-3.993-6.81-8.205-8.451-12.636s-2.462-9.216-2.462-14.359c0-5.142 0.82-9.929 2.462-14.359s4.458-8.642 8.451-12.636l146.214-146.214c6.182-6.181 13.676-9.271 22.482-9.271s16.3 3.090 22.481 9.271c6.181 6.181 9.203 13.743 9.067 22.687s-3.159 16.368-9.067 22.276l-96.247 96.247h299.574c62.413 0 115.866 20.827 160.367 62.481 44.497 41.654 66.748 93.033 66.748 154.132 0 61.103-22.251 112.546-66.748 154.338-44.501 41.792-97.954 62.686-160.367 62.686h-274.218z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["undo"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":847,"id":40,"name":"undo","prevSize":32,"code":59749},"setIdx":0,"setId":1,"iconIdx":41},{"icon":{"paths":["M710.071 419.689h-299.57c-44.526 0-82.844 14.633-114.953 43.898s-48.164 65.506-48.164 108.719c0 43.213 16.055 79.522 48.164 108.924s70.427 44.1 114.953 44.1h274.218c9.079 0 16.683 3.063 22.81 9.19s9.186 13.73 9.186 22.81c0 9.079-3.059 16.683-9.186 22.81s-13.73 9.19-22.81 9.19h-274.218c-62.413 0-115.869-20.894-160.368-62.686s-66.748-93.239-66.748-154.338c0-61.103 22.249-112.478 66.748-154.133 44.499-41.654 97.955-62.481 160.368-62.481h299.57l-96.247-96.247c-5.905-5.908-8.93-13.333-9.067-22.276s2.889-16.506 9.067-22.687c6.182-6.181 13.675-9.271 22.481-9.271s16.303 3.090 22.485 9.271l146.214 146.214c3.994 3.994 6.81 8.206 8.448 12.636 1.643 4.431 2.462 9.217 2.462 14.359s-0.819 9.928-2.462 14.359c-1.638 4.431-4.454 8.643-8.448 12.636l-146.214 146.217c-5.909 5.905-13.333 8.93-22.276 9.067s-16.508-2.889-22.69-9.067c-6.178-6.182-9.271-13.675-9.271-22.485 0-8.806 3.093-16.299 9.271-22.481l96.247-96.245z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["redo"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":848,"id":41,"name":"redo","prevSize":32,"code":59750},"setIdx":0,"setId":1,"iconIdx":42},{"icon":{"paths":["M512 704c-53.278 0-98.597-18.684-135.958-56.043s-56.041-82.679-56.041-135.957c0-53.278 18.68-98.598 56.041-135.959s82.68-56.041 135.958-56.041c53.278 0 98.598 18.68 135.957 56.041 37.363 37.361 56.043 82.682 56.043 135.959s-18.679 98.598-56.043 135.957c-37.359 37.359-82.679 56.043-135.957 56.043zM85.333 544c-9.067 0-16.666-3.068-22.799-9.203-6.134-6.14-9.201-13.743-9.201-22.81 0-9.071 3.066-16.67 9.2-22.797 6.132-6.127 13.732-9.19 22.799-9.19h96.001c9.066 0 16.666 3.068 22.799 9.203s9.201 13.739 9.201 22.81c0 9.071-3.066 16.67-9.2 22.797s-13.733 9.19-22.799 9.19h-96.001zM842.667 544c-9.067 0-16.666-3.068-22.801-9.203-6.131-6.14-9.199-13.743-9.199-22.81 0-9.071 3.068-16.67 9.199-22.797 6.135-6.127 13.734-9.19 22.801-9.19h96c9.067 0 16.666 3.068 22.801 9.203 6.131 6.135 9.199 13.739 9.199 22.81s-3.068 16.67-9.199 22.797c-6.135 6.127-13.734 9.19-22.801 9.19h-96zM511.987 213.332c-9.071 0-16.67-3.067-22.797-9.2s-9.19-13.733-9.19-22.799v-96.001c0-9.067 3.068-16.667 9.203-22.8s13.739-9.199 22.81-9.199c9.071 0 16.67 3.066 22.797 9.199s9.19 13.734 9.19 22.8v96.001c0 9.066-3.068 16.666-9.203 22.799s-13.739 9.2-22.81 9.2zM511.987 970.667c-9.071 0-16.67-3.068-22.797-9.203-6.127-6.131-9.19-13.73-9.19-22.797v-96c0-9.067 3.068-16.666 9.203-22.801 6.135-6.131 13.739-9.199 22.81-9.199s16.67 3.068 22.797 9.199c6.127 6.135 9.19 13.734 9.19 22.801v96c0 9.067-3.068 16.666-9.203 22.797-6.135 6.135-13.739 9.203-22.81 9.203zM256.247 300.387l-53.662-52.185c-6.345-5.908-9.409-13.333-9.19-22.276s3.324-16.724 9.315-23.343c6.535-6.619 14.206-9.929 23.013-9.929s16.301 3.31 22.482 9.929l52.595 53.252c6.181 6.619 9.271 14.113 9.271 22.482s-3.022 15.863-9.067 22.481c-6.044 6.618-13.36 9.749-21.948 9.394s-16.191-3.625-22.81-9.806zM775.795 821.414l-52.595-53.252c-6.182-6.618-9.271-14.221-9.271-22.81s3.089-15.974 9.271-22.153c5.769-6.618 12.983-9.749 21.641-9.395s16.294 3.622 22.912 9.805l53.662 52.186c6.345 5.905 9.408 13.333 9.19 22.276s-3.324 16.725-9.314 23.343c-6.537 6.618-14.208 9.929-23.014 9.929s-16.299-3.311-22.481-9.929zM723.2 301.004c-6.618-6.044-9.749-13.36-9.395-21.948 0.358-8.588 3.627-16.192 9.805-22.81l52.186-53.662c5.909-6.345 13.333-9.409 22.276-9.19s16.725 3.324 23.343 9.315c6.618 6.535 9.929 14.206 9.929 23.012s-3.311 16.301-9.929 22.482l-53.252 52.595c-6.618 6.181-14.114 9.271-22.481 9.271s-15.863-3.023-22.481-9.067zM202.586 821.504c-6.619-6.677-9.929-14.421-9.929-23.228s3.31-16.303 9.929-22.481l53.251-52.595c6.619-6.182 14.222-9.271 22.809-9.271 8.588 0 15.973 3.089 22.154 9.271 6.345 5.769 9.34 12.983 8.985 21.641s-3.488 16.294-9.396 22.912l-52.184 53.662c-6.181 6.618-13.675 9.818-22.482 9.6s-16.519-3.388-23.137-9.51z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["light"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":849,"id":42,"name":"light","prevSize":32,"code":59751},"setIdx":0,"setId":1,"iconIdx":43},{"icon":{"paths":["M513.229 874.667c-101.085 0-186.801-35.17-257.146-105.515s-105.518-156.062-105.518-257.148c0-84.186 26.051-159.014 78.153-224.491s121.503-107.679 208.202-126.606c8.973-2.243 16.875-2.024 23.714 0.656 6.835 2.681 12.39 6.674 16.657 11.98s6.78 11.747 7.548 19.323c0.764 7.576-1.011 15.139-5.333 22.687-8.806 16.027-15.343 32.711-19.61 50.051s-6.4 35.473-6.4 54.399c0 70.018 24.452 129.478 73.353 178.382 48.905 48.9 108.365 73.353 178.381 73.353 20.954 0 40.973-2.654 60.062-7.957 19.093-5.308 35.802-11.46 50.133-18.462 7.002-3.063 13.841-4.143 20.514-3.243 6.673 0.905 12.39 3.132 17.148 6.69 5.197 3.554 9.067 8.41 11.61 14.562s2.829 13.389 0.862 21.705c-15.155 83.908-55.974 153.161-122.462 207.748-66.487 54.592-143.108 81.886-229.867 81.886z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["dark"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":837,"id":43,"name":"dark","prevSize":32,"code":59752},"setIdx":0,"setId":1,"iconIdx":44},{"icon":{"paths":["M833.293 190.706c12.497 12.497 12.497 32.758 0 45.255l-597.332 597.332c-12.497 12.497-32.758 12.497-45.255 0s-12.497-32.755 0-45.252l597.335-597.335c12.497-12.497 32.755-12.497 45.252 0z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["line"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":836,"id":44,"name":"line","prevSize":32,"code":59753},"setIdx":0,"setId":1,"iconIdx":45},{"icon":{"paths":["M138.667 181.333c0-23.564 19.103-42.667 42.667-42.667h661.333c23.565 0 42.667 19.103 42.667 42.667v661.333c0 23.565-19.102 42.667-42.667 42.667h-661.333c-23.564 0-42.667-19.102-42.667-42.667v-661.333zM202.667 202.667v618.667h618.667v-618.667h-618.667z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["square"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":835,"id":45,"name":"square","prevSize":32,"code":59754},"setIdx":0,"setId":1,"iconIdx":46},{"icon":{"paths":["M138.667 512c0-206.186 167.147-373.333 373.333-373.333s373.333 167.147 373.333 373.333c0 206.187-167.147 373.333-373.333 373.333s-373.333-167.147-373.333-373.333zM512 202.667c-170.84 0-309.333 138.493-309.333 309.333 0 170.842 138.493 309.333 309.333 309.333 170.842 0 309.333-138.492 309.333-309.333 0-170.84-138.492-309.333-309.333-309.333z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["circle"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":834,"id":46,"name":"circle","prevSize":32,"code":59755},"setIdx":0,"setId":1,"iconIdx":47},{"icon":{"paths":["M453.751 308.968l356.915-95.635-95.633 356.915-108.015-108.015-371.058 371.059c-12.497 12.497-32.758 12.497-45.255 0s-12.497-32.755 0-45.252l371.060-371.060-108.015-108.012z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":833,"id":47,"name":"arrow","prevSize":32,"code":59756},"setIdx":0,"setId":1,"iconIdx":48},{"icon":{"paths":["M1024 512c0 282.772-229.233 512-512 512-282.768 0-511.999-229.233-511.999-512 0-282.754 229.245-511.998 511.999-511.998 282.762 0 512 229.238 512 511.998z"],"attrs":[{"fill":"rgb(234, 196, 67)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["gradient"],"grid":0},"attrs":[{"fill":"rgb(234, 196, 67)"}],"properties":{"order":41,"id":48,"name":"gradient","prevSize":32,"code":59757},"setIdx":0,"setId":1,"iconIdx":49},{"icon":{"paths":["M191.999 896c-17.722 0-32.819-6.238-45.292-18.709s-18.707-27.571-18.707-45.291h63.999v64zM128 746.667v-85.333h63.999v85.333h-63.999zM128 576v-85.333h63.999v85.333h-63.999zM128 405.332v-85.333h63.999v85.333h-63.999zM128 234.666c0-17.722 6.236-32.82 18.707-45.292s27.569-18.707 45.292-18.707v63.999h-63.999zM277.332 896v-64h85.333v64h-85.333zM277.332 234.666v-63.999h85.333v63.999h-85.333zM448 234.666v-63.999h85.333v63.999h-85.333zM618.667 234.666v-63.999h85.333v63.999h-85.333zM789.333 405.332v-85.333h64v85.333h-64zM789.333 234.666v-63.999c17.719 0 32.819 6.236 45.291 18.707s18.709 27.569 18.709 45.292h-64z","M512 554.667l113.779 341.333 56.887-170.667 170.667-56.887-341.333-113.779z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["area-selection"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":832,"id":49,"name":"area-selection","prevSize":32,"code":59758},"setIdx":0,"setId":1,"iconIdx":50},{"icon":{"paths":["M384.047 917.739c-32.579 0-60.287-11.405-83.124-34.21s-34.256-50.496-34.256-83.076c0-32.576 11.403-60.288 34.208-83.123 22.805-22.839 50.497-34.257 83.076-34.257s60.286 11.405 83.125 34.21c22.835 22.801 34.253 50.496 34.253 83.076 0 32.576-11.401 60.284-34.206 83.123-22.805 22.835-50.497 34.257-83.076 34.257zM570.419 769.472c-4.813-32-17.203-60.608-37.167-85.824-19.968-25.22-44.608-43.435-73.929-54.647l125.786-125.623h-322.709c-20.020 0-36.759-6.976-50.216-20.928s-20.184-31.074-20.184-51.366c0-12.684 3.255-24.359 9.764-35.025s14.988-19.501 25.436-26.502l493.781-301.374c10.338-6.181 21.252-7.726 32.738-4.636 11.49 3.090 20.322 9.737 26.505 19.938 6.178 10.202 7.795 20.937 4.838 32.205-2.953 11.268-9.242 19.993-18.871 26.174l-402.296 252.801h397.705c19.759 0 36.433 6.793 50.018 20.378 13.585 13.588 20.382 30.257 20.382 50.021 0 11.499-1.327 22.839-3.981 34.018-2.654 11.183-8.055 20.847-16.205 28.996l-241.395 241.395z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["highlight"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":831,"id":50,"name":"highlight","prevSize":32,"code":59761},"setIdx":0,"setId":1,"iconIdx":51},{"icon":{"paths":["M480.004 800.661v-160.661c0-9.079 3.063-16.683 9.186-22.81 6.127-6.123 13.73-9.186 22.81-9.186 9.084 0 16.687 3.063 22.81 9.186 6.127 6.127 9.19 13.73 9.19 22.81v160.004l65.152-65.809c6.345-6.345 13.99-9.583 22.933-9.719s16.725 3.102 23.343 9.719c6.618 6.622 9.929 14.332 9.929 23.138s-3.311 16.521-9.929 23.138l-116.433 116.433c-3.994 3.994-8.205 6.81-12.634 8.448-4.433 1.643-9.216 2.462-14.362 2.462-5.141 0-9.924-0.819-14.357-2.462-4.429-1.638-8.64-4.454-12.634-8.448l-117.088-117.090c-6.345-6.345-9.477-13.875-9.396-22.601s3.433-16.397 10.051-23.019c6.618-6.618 14.222-9.924 22.81-9.924s16.191 3.307 22.81 9.924l65.808 66.466zM224 544l65.807 65.152c6.345 6.345 9.586 13.99 9.723 22.933s-3.104 16.725-9.723 23.343c-6.619 6.618-14.332 9.929-23.138 9.929s-16.519-3.311-23.138-9.929l-116.43-116.433c-3.994-3.994-6.811-8.205-8.451-12.634-1.641-4.433-2.462-9.216-2.462-14.362 0-5.141 0.821-9.924 2.462-14.357 1.641-4.429 4.458-8.64 8.451-12.634l116.43-116.432c6.345-6.345 13.88-9.586 22.605-9.722s16.396 3.104 23.014 9.722c6.619 6.618 9.929 14.222 9.929 22.81s-3.309 16.191-9.929 22.81l-65.806 65.808h160.658c9.080 0 16.684 3.063 22.81 9.186 6.126 6.127 9.189 13.73 9.189 22.81 0 9.084-3.063 16.687-9.189 22.81-6.127 6.127-13.73 9.19-22.81 9.19h-160.002zM800.661 544h-160.661c-9.079 0-16.683-3.063-22.81-9.19-6.123-6.123-9.186-13.726-9.186-22.81 0-9.079 3.063-16.683 9.186-22.81 6.127-6.123 13.73-9.186 22.81-9.186h160.004l-65.809-65.152c-6.345-6.345-9.583-13.988-9.719-22.932s3.102-16.724 9.719-23.343c6.622-6.619 14.332-9.929 23.138-9.929s16.521 3.31 23.138 9.929l116.433 116.432c3.994 3.994 6.81 8.205 8.448 12.634 1.643 4.433 2.462 9.216 2.462 14.357 0 5.146-0.819 9.929-2.462 14.362-1.638 4.429-4.454 8.64-8.448 12.634l-117.090 117.086c-6.345 6.349-13.769 9.481-22.276 9.395-8.503-0.081-16.064-3.43-22.686-10.048-6.618-6.618-9.929-14.221-9.929-22.81s3.311-16.192 9.929-22.81l65.809-65.809zM480.004 223.344l-66.464 66.463c-6.345 6.345-13.77 9.586-22.276 9.722s-16.069-3.104-22.687-9.722c-6.619-6.619-9.929-14.223-9.929-22.81s3.31-16.191 9.929-22.81l116.432-117.087c3.994-3.994 8.205-6.811 12.634-8.451 4.433-1.641 9.216-2.462 14.357-2.462 5.146 0 9.929 0.821 14.362 2.462 4.429 1.641 8.64 4.458 12.634 8.451l117.086 117.087c6.349 6.345 9.587 13.77 9.724 22.276s-3.102 16.068-9.724 22.686c-6.618 6.619-14.221 9.929-22.81 9.929-8.585 0-16.188-3.309-22.81-9.929l-66.462-65.806v160.658c0 9.080-3.063 16.684-9.19 22.81-6.123 6.127-13.726 9.19-22.81 9.19-9.079 0-16.683-3.063-22.81-9.19-6.123-6.126-9.186-13.729-9.186-22.81v-160.658z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["move"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":830,"id":51,"name":"move","prevSize":32,"code":59762},"setIdx":0,"setId":1,"iconIdx":52},{"icon":{"paths":["M466.871 282.665h-186.666c-12.535 0-23.191-4.392-31.966-13.176s-13.162-19.45-13.162-31.999c0-12.548 4.387-23.267 13.162-32.156s19.43-13.333 31.966-13.333h463.999c12.535 0 23.189 4.392 31.966 13.177 8.772 8.784 13.163 19.45 13.163 31.999s-4.39 23.267-13.163 32.156c-8.777 8.889-19.43 13.333-31.966 13.333h-186.667v504.207c0 12.535-4.395 23.189-13.18 31.966-8.781 8.772-19.447 13.163-31.996 13.163s-23.266-4.429-32.158-13.282c-8.887-8.858-13.333-19.61-13.333-32.256v-503.797z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["text"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":829,"id":52,"name":"text","prevSize":32,"code":59766},"setIdx":0,"setId":1,"iconIdx":53},{"icon":{"paths":["M209.154 846.438c-14.809 5.198-27.832 2.179-39.070-9.060-11.237-11.235-14.257-24.257-9.058-39.068l113.977-318.46c9.19-25.784 27.084-41.591 53.681-47.419s49.427 0.786 68.489 19.849l158.010 158.011c19.063 19.063 25.678 41.89 19.849 68.489-5.829 26.595-21.635 44.491-47.419 53.678l-318.459 113.979zM576.057 508.285c-5.091-5.095-7.639-10.818-7.639-17.17s2.548-12.079 7.639-17.17l207.479-207.48c20.533-20.532 45.371-30.799 74.514-30.799 29.147 0 53.985 10.266 74.514 30.799l3.703 3.703c4.411 4.411 6.685 9.793 6.816 16.146 0.131 6.355-2.351 12.078-7.447 17.172-4.829 5.094-10.42 7.641-16.777 7.641-6.353 0-12.075-2.547-17.17-7.641l-2.679-2.678c-11.133-11.133-24.654-16.699-40.567-16.699-15.909 0-29.434 5.566-40.567 16.699l-208.499 208.502c-4.411 4.411-9.794 6.615-16.151 6.615-6.353 0-12.075-2.544-17.17-7.639zM426.398 358.625c-5.095-5.094-7.639-10.818-7.639-17.172s2.544-12.078 7.639-17.171l7.406-7.405c12.182-12.184 18.272-26.94 18.272-44.27s-6.091-32.086-18.272-44.268l-9.691-9.689c-4.411-4.411-6.615-9.793-6.615-16.148 0-6.353 2.548-12.077 7.639-17.171 5.095-5.093 10.818-7.64 17.17-7.64 6.357 0 12.079 2.547 17.175 7.64l8.663 8.665c21.844 21.845 32.768 48.049 32.768 78.611s-10.924 56.766-32.768 78.612l-8.43 8.43c-4.411 4.411-9.794 6.616-16.146 6.616s-12.079-2.546-17.17-7.64zM501.228 433.455c-5.095-5.095-7.639-10.818-7.639-17.17s2.544-12.079 7.639-17.172l125.559-125.558c11.133-11.133 16.699-24.655 16.699-40.567s-5.566-29.434-16.699-40.567l-44.663-44.662c-4.411-4.411-6.615-9.794-6.615-16.148s2.544-12.077 7.639-17.171c5.095-5.093 10.818-7.64 17.17-7.64s12.079 2.547 17.175 7.64l43.635 43.638c20.795 20.795 31.195 45.765 31.195 74.91s-10.4 54.114-31.195 74.91l-126.579 126.582c-4.411 4.411-9.798 6.615-16.151 6.615s-12.075-2.548-17.17-7.639zM650.887 583.115c-5.091-5.091-7.639-10.818-7.639-17.17s2.548-12.079 7.639-17.17l31.822-31.822c22.11-22.11 48.787-33.165 80.032-33.165s57.922 11.055 80.028 33.165l32.846 32.846c4.411 4.411 6.619 9.794 6.619 16.146s-2.548 12.079-7.643 17.17c-5.091 5.095-10.818 7.639-17.17 7.639s-12.079-2.544-17.17-7.639l-31.822-31.822c-12.448-12.444-27.677-18.67-45.687-18.67-18.014 0-33.243 6.226-45.687 18.67l-32.846 32.846c-4.411 4.411-9.794 6.615-16.151 6.615-6.353 0-12.075-2.544-17.17-7.639z"],"attrs":[{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["celebration"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"}],"properties":{"order":46,"id":53,"name":"celebration","prevSize":32,"code":59746},"setIdx":0,"setId":1,"iconIdx":54},{"icon":{"paths":["M319.992 797.88c-19.217 0-35.403-6.63-48.559-19.891s-19.733-29.496-19.733-48.701v-36.756c0-7.875 2.633-14.464 7.899-19.773s11.802-7.962 19.61-7.962c7.808 0 14.419 2.652 19.834 7.962s8.123 11.899 8.123 19.773v36.756c0 3.282 1.368 6.292 4.102 9.027 2.736 2.734 5.745 4.101 9.028 4.101h434.541c3.282 0 6.292-1.367 9.027-4.101s4.101-5.745 4.101-9.027v-36.756c0-7.875 2.637-14.464 7.9-19.773s11.802-7.962 19.61-7.962c7.808 0 14.418 2.652 19.835 7.962s8.12 11.899 8.12 19.773v36.756c0 19.205-6.636 35.441-19.907 48.701-13.266 13.261-29.512 19.891-48.727 19.891h-434.803zM509.833 281.123l-87.385 87.385c-5.634 5.634-12.102 8.588-19.405 8.862s-14.077-2.544-20.325-8.451c-5.841-5.908-8.693-12.513-8.556-19.814s3.159-13.907 9.065-19.815l130.543-130.542c3.569-3.282 7.316-5.744 11.249-7.385s8.187-2.462 12.759-2.462c4.572 0 8.827 0.821 12.759 2.462s7.542 4.103 10.824 7.385l130.54 130.542c5.637 5.634 8.591 11.993 8.863 19.076s-2.509 13.579-8.351 19.487c-6.246 5.907-12.954 8.793-20.122 8.656-7.163-0.136-13.701-3.159-19.61-9.066l-87.383-86.318v347.736c0 7.869-2.632 14.459-7.9 19.768-5.263 5.309-11.802 7.962-19.61 7.962s-14.418-2.652-19.835-7.962c-5.412-5.309-8.121-11.899-8.121-19.768v-347.736z"],"width":1075,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["upload-new"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":828,"id":54,"name":"upload-new","prevSize":32,"code":59745},"setIdx":0,"setId":1,"iconIdx":55},{"icon":{"paths":["M539.525 714.65c7.112 0 13.332-2.493 18.652-7.475 5.34-4.982 8.013-11.382 8.013-19.2v-147.2h148.219c7.096 0 13.138-2.493 18.12-7.475 4.982-4.966 7.47-11.366 7.47-19.2 0-7.816-2.488-14.216-7.47-19.199-4.982-4.966-11.382-7.45-19.195-7.45h-147.144v-152.55c0-7.1-2.673-13.142-8.013-18.125-5.32-4.984-11.889-7.475-19.702-7.475-7.117 0-13.343 2.491-18.683 7.475-5.325 4.983-7.984 11.383-7.984 19.2v151.475h-151.419c-7.097 0-13.136 2.483-18.118 7.45-4.982 4.983-7.473 11.383-7.473 19.199 0 7.834 2.491 14.234 7.473 19.2 4.981 4.982 11.379 7.475 19.193 7.475h150.344v148.275c0 7.101 2.659 13.143 7.984 18.125 5.34 4.982 11.919 7.475 19.732 7.475zM537.4 917.299c-56.863 0-109.998-10.486-159.404-31.462s-92.237-49.597-128.49-85.862c-36.253-36.265-64.863-79.114-85.831-128.538s-31.451-102.579-31.451-159.462c0-56.883 10.484-109.858 31.451-158.925s49.577-91.913 85.831-128.538c36.253-36.625 79.083-65.425 128.49-86.4s102.541-31.462 159.404-31.462c56.863 0 109.814 10.487 158.863 31.462s91.878 49.775 128.492 86.4c36.613 36.625 65.403 79.471 86.369 128.538s31.447 102.042 31.447 158.925c0 56.883-10.481 110.039-31.447 159.462s-49.756 92.273-86.369 128.538c-36.613 36.265-79.442 64.886-128.492 85.862s-102.001 31.462-158.863 31.462z"],"width":1075,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["add"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":827,"id":55,"name":"add","prevSize":32,"code":59741},"setIdx":0,"setId":1,"iconIdx":56},{"icon":{"paths":["M609.9 138.325c4.265 4.266 9.595 6.4 15.99 6.4 5.699 0 10.68-2.134 14.945-6.4s6.4-9.242 6.4-14.925c0-6.4-2.135-11.734-6.4-16s-9.247-6.4-14.945-6.4c-6.395 0-11.725 2.133-15.99 6.4s-6.4 9.6-6.4 16c0 5.683 2.135 10.658 6.4 14.925z","M138.622 612.296c4.265 4.265 9.239 6.4 14.919 6.4 6.398 0 11.729-2.135 15.994-6.4s6.397-9.6 6.397-16c0-6.4-2.132-11.556-6.397-15.462-4.265-3.927-9.596-5.888-15.994-5.888-5.681 0-10.654 1.961-14.919 5.888-4.265 3.907-6.397 9.062-6.397 15.462s2.132 11.735 6.397 16z","M138.622 443.207c4.265 3.926 9.239 5.888 14.919 5.888 6.398 0 11.729-1.962 15.994-5.888 4.265-3.908 6.397-9.062 6.397-15.462s-2.132-11.733-6.397-16c-4.265-4.266-9.596-6.4-15.994-6.4-5.681 0-10.654 2.134-14.919 6.4s-6.397 9.6-6.397 16c0 6.4 2.132 11.554 6.397 15.462z","M254.317 795.208c7.456 7.475 16.515 11.213 27.177 11.213s19.722-3.738 27.177-11.213c7.473-7.46 11.209-16.522 11.209-27.187s-3.736-19.727-11.209-27.187c-7.455-7.475-16.515-11.213-27.177-11.213s-19.722 3.738-27.177 11.213c-7.472 7.46-11.208 16.522-11.208 27.187s3.736 19.727 11.208 27.187z","M254.317 623.483c7.456 7.475 16.515 11.213 27.177 11.213s19.722-3.738 27.177-11.213c7.473-7.46 11.209-16.522 11.209-27.187s-3.736-19.738-11.209-27.213c-7.455-7.46-16.515-11.187-27.177-11.187s-19.722 3.727-27.177 11.187c-7.472 7.475-11.208 16.548-11.208 27.213s3.736 19.727 11.208 27.187z","M254.317 454.958c7.456 7.458 16.515 11.187 27.177 11.187s19.722-3.729 27.177-11.187c7.473-7.475 11.209-16.546 11.209-27.213s-3.736-19.729-11.209-27.187c-7.455-7.475-16.515-11.213-27.177-11.213s-19.722 3.738-27.177 11.213c-7.472 7.458-11.208 16.521-11.208 27.187s3.736 19.738 11.208 27.213z","M254.317 283.207c7.456 7.475 16.515 11.213 27.177 11.213s19.722-3.738 27.177-11.213c7.473-7.458 11.209-16.52 11.209-27.187s-3.736-19.729-11.209-27.187c-7.455-7.475-16.515-11.213-27.177-11.213s-19.722 3.738-27.177 11.213c-7.472 7.458-11.208 16.521-11.208 27.187s3.736 19.729 11.208 27.187z","M425.978 283.207c7.455 7.475 16.515 11.213 27.177 11.213s19.73-3.738 27.203-11.213c7.456-7.458 11.183-16.52 11.183-27.187s-3.727-19.729-11.183-27.187c-7.472-7.475-16.54-11.213-27.203-11.213s-19.722 3.738-27.177 11.213c-7.473 7.458-11.209 16.521-11.209 27.187s3.736 19.729 11.209 27.187z","M437.161 424.545c4.265 4.267 9.596 6.4 15.994 6.4 5.698 0 10.68-2.133 14.945-6.4s6.398-9.6 6.398-16c0-5.683-2.133-10.658-6.398-14.925s-9.247-6.4-14.945-6.4c-6.397 0-11.729 2.134-15.994 6.4s-6.398 9.242-6.398 14.925c0 6.4 2.132 11.734 6.398 16z","M594.442 283.207c7.47 7.475 16.538 11.213 27.203 11.213 10.66 0 19.722-3.738 27.177-11.213 7.47-7.458 11.208-16.52 11.208-27.187s-3.738-19.729-11.208-27.187c-7.455-7.475-16.517-11.213-27.177-11.213-10.665 0-19.732 3.738-27.203 11.213-7.455 7.458-11.187 16.521-11.187 27.187s3.732 19.729 11.187 27.187z","M766.126 795.208c7.455 7.475 16.517 11.213 27.177 11.213 10.665 0 19.722-3.738 27.177-11.213 7.475-7.46 11.208-16.522 11.208-27.187s-3.732-19.727-11.208-27.187c-7.455-7.475-16.512-11.213-27.177-11.213-10.66 0-19.722 3.738-27.177 11.213-7.47 7.46-11.208 16.522-11.208 27.187s3.738 19.727 11.208 27.187z","M766.126 623.483c7.455 7.475 16.517 11.213 27.177 11.213 10.665 0 19.722-3.738 27.177-11.213 7.475-7.46 11.208-16.522 11.208-27.187s-3.732-19.738-11.208-27.213c-7.455-7.46-16.512-11.187-27.177-11.187-10.66 0-19.722 3.727-27.177 11.187-7.47 7.475-11.208 16.548-11.208 27.213s3.738 19.727 11.208 27.187z","M766.126 454.958c7.455 7.458 16.517 11.187 27.177 11.187 10.665 0 19.722-3.729 27.177-11.187 7.475-7.475 11.208-16.546 11.208-27.213s-3.732-19.729-11.208-27.187c-7.455-7.475-16.512-11.213-27.177-11.213-10.66 0-19.722 3.738-27.177 11.213-7.47 7.458-11.208 16.521-11.208 27.187s3.738 19.738 11.208 27.213z","M766.126 283.207c7.455 7.475 16.517 11.213 27.177 11.213 10.665 0 19.722-3.738 27.177-11.213 7.475-7.458 11.208-16.52 11.208-27.187s-3.732-19.729-11.208-27.187c-7.455-7.475-16.512-11.213-27.177-11.213-10.66 0-19.722 3.738-27.177 11.213-7.47 7.458-11.208 16.521-11.208 27.187s3.738 19.729 11.208 27.187z","M905.262 612.296c4.265 4.265 9.595 6.4 15.995 6.4 5.683 0 10.655-2.135 14.92-6.4s6.395-9.6 6.395-16c0-6.4-2.13-11.556-6.395-15.462-4.265-3.927-9.236-5.888-14.92-5.888-6.4 0-11.73 1.961-15.995 5.888-4.265 3.907-6.395 9.062-6.395 15.462s2.13 11.735 6.395 16z","M905.262 443.207c4.265 3.926 9.595 5.888 15.995 5.888 5.683 0 10.655-1.962 14.92-5.888 4.265-3.908 6.395-9.062 6.395-15.462s-2.13-11.733-6.395-16c-4.265-4.266-9.236-6.4-14.92-6.4-6.4 0-11.73 2.134-15.995 6.4s-6.395 9.6-6.395 16c0 6.4 2.13 11.554 6.395 15.462z","M632.289 430.945c-6.4 0-11.73-2.134-15.995-6.4s-6.395-9.6-6.395-16c0-5.683 2.13-10.658 6.395-14.925s9.595-6.4 15.995-6.4c5.699 0 10.68 2.133 14.945 6.4s6.395 9.242 6.395 14.925c0 6.4-2.13 11.733-6.395 16s-9.247 6.4-14.945 6.4z","M445.683 150.42c-6.397 0-11.729-2.134-15.994-6.4s-6.398-9.242-6.398-14.925c0-6.4 2.132-11.734 6.398-16s9.596-6.4 15.994-6.4c5.698 0 10.68 2.134 14.945 6.4s6.397 9.6 6.397 16c0 5.683-2.132 10.658-6.397 14.925s-9.247 6.4-14.945 6.4z","M650.737 589.117c0 63.944-51.82 115.779-115.738 115.779s-115.734-51.835-115.734-115.779c0-63.944 51.816-115.78 115.734-115.78s115.738 51.836 115.738 115.78z","M353.156 870.707v-41.231c0-50.591 37.581-91.607 83.94-91.607h195.858c46.362 0 83.942 41.016 83.942 91.607v41.231c-46.536 53.591-112.374 65.644-185.37 65.644-69.409 0-132.349-16.599-178.372-65.644z"],"width":1075,"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["blur"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":826,"id":56,"name":"blur","prevSize":32,"code":59742},"setIdx":0,"setId":1,"iconIdx":57},{"icon":{"paths":["M537.462 900.301c-53.297 0-103.59-10.122-150.882-30.362-47.257-20.275-88.492-48.010-123.704-83.2-35.178-35.226-62.901-76.477-83.169-123.75-20.233-47.309-30.35-97.623-30.35-150.938 0-54.034 10.117-104.518 30.35-151.451 20.268-46.934 47.991-88.013 83.169-123.238 35.213-35.191 76.447-62.925 123.704-83.2 47.291-20.241 97.585-30.362 150.882-30.362 54.011 0 104.479 10.121 151.393 30.362 46.915 20.275 87.982 48.009 123.192 83.2 35.18 35.226 62.899 76.305 83.169 123.238 20.234 46.934 30.351 97.417 30.351 151.451 0 53.315-10.117 103.629-30.351 150.938-20.27 47.273-47.99 88.525-83.169 123.75-35.21 35.19-76.278 62.925-123.192 83.2-46.915 20.239-97.382 30.362-151.393 30.362zM537.462 844.851c92.401 0 170.947-32.358 235.638-97.075s97.039-143.293 97.039-235.725c0-40.552-6.927-79.122-20.782-115.714-13.85-36.625-33.234-69.53-58.138-98.714l-468.104 468.277c29.173 24.914 62.066 44.303 98.677 58.163 36.577 13.855 75.133 20.787 115.669 20.787zM283.706 726.477l468.099-468.277c-29.169-24.918-62.065-44.305-98.673-58.163-36.577-13.858-75.136-20.787-115.671-20.787-92.399 0-170.945 32.358-235.637 97.075s-97.039 143.292-97.039 235.726c0 40.55 6.926 79.119 20.78 115.712 13.853 36.623 33.233 69.53 58.142 98.714z"],"width":1075,"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["remove"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":814,"id":57,"name":"remove","prevSize":32,"code":59743},"setIdx":0,"setId":1,"iconIdx":58},{"icon":{"paths":["M170.667 395.1v-72.533l194.133-193.067h71.467l-265.6 265.6zM170.667 204.167v-62.933l12.8-11.733h61.867l-74.667 74.667zM686.933 266.033c-6.4-6.4-12.983-12.445-19.755-18.133-6.741-5.689-13.312-10.667-19.712-14.933l103.467-103.467h72.533l-136.533 136.533zM236.8 643.635l106.667-106.667c4.267 6.4 8.533 12.442 12.8 18.133 4.267 5.687 8.889 11.021 13.867 16l-30.933 30.933c-16.355 4.267-33.593 10.138-51.712 17.621-18.147 7.45-35.043 15.445-50.688 23.979zM749.867 395.1v-1.067c-0.713-9.956-2.133-19.911-4.267-29.867s-5.333-19.2-9.6-27.733l160-160v72.533l-146.133 146.133zM478.933 208.433l80-78.933h71.467l-74.667 74.667c-4.979-0.711-9.6-1.423-13.867-2.133s-8.887-1.067-13.867-1.067c-7.821 0.711-16 1.593-24.533 2.645-8.533 1.081-16.713 2.688-24.533 4.821zM170.667 588.169v-71.467l148.267-148.268c-2.133 8.533-3.726 16.881-4.779 25.045-1.081 8.192-1.621 16.199-1.621 24.021 0 4.977 0.185 9.602 0.555 13.868 0.341 4.267 0.867 8.533 1.579 12.8l-144 144zM856.533 673.502c-4.979-6.4-10.667-12.262-17.067-17.579-6.4-5.35-13.513-10.513-21.333-15.488l77.867-77.867v72.533l-39.467 38.4zM752 584.969c-1.421-3.558-3.2-6.942-5.333-10.155-2.133-3.187-4.267-6.204-6.4-9.045l-9.045-11.221c-3.213-3.9-6.601-7.27-10.155-10.112l174.933-176.002v73.602l-144 142.933zM533.333 572.169c-41.246 0-76.446-14.579-105.6-43.733-29.155-29.158-43.733-64.358-43.733-105.602 0-41.245 14.578-76.445 43.733-105.6 29.154-29.156 64.354-43.733 105.6-43.733s76.446 14.577 105.6 43.733c29.154 29.155 43.733 64.355 43.733 105.6 0 41.243-14.579 76.443-43.733 105.602-29.154 29.154-64.354 43.733-105.6 43.733zM533.333 508.169c23.467 0 43.563-8.363 60.288-25.088 16.695-16.7 25.045-36.779 25.045-60.247 0-23.467-8.35-43.563-25.045-60.288-16.725-16.697-36.821-25.045-60.288-25.045s-43.55 8.348-60.245 25.045c-16.725 16.725-25.088 36.821-25.088 60.288 0 23.468 8.363 43.547 25.088 60.247 16.695 16.725 36.779 25.088 60.245 25.088zM256 870.835c-23.564 0-42.667-19.106-42.667-42.667v-25.6c0-22.046 5.689-41.788 17.067-59.221 11.378-17.408 26.667-30.737 45.867-39.979 34.845-17.779 74.141-32.542 117.888-44.288 43.721-11.721 90.112-17.579 139.179-17.579s95.475 5.858 139.221 17.579c43.721 11.746 82.999 26.509 117.845 44.288 19.2 9.242 34.487 22.754 45.867 40.533 11.379 17.775 17.067 37.333 17.067 58.667v25.6c0 23.561-19.102 42.667-42.667 42.667h-554.667zM278.4 806.835h509.867c-0.713-12.092-3.029-21.692-6.955-28.8-3.895-7.113-10.825-13.158-20.779-18.133-26.313-12.8-58.483-25.062-96.512-36.779-38.059-11.75-81.621-17.621-130.688-17.621s-92.617 5.871-130.645 17.621c-38.059 11.716-70.243 23.979-96.555 36.779-9.245 4.975-16 11.021-20.267 18.133-4.267 7.108-6.755 16.708-7.467 28.8z","M810.667 832h-554.667c0-85.333 89.6-149.333 277.333-149.333s277.333 64 277.333 149.333z","M640 426.667c0 70.692-47.756 128-106.667 128s-106.667-57.308-106.667-128c0-70.692 47.756-128 106.667-128s106.667 57.308 106.667 128z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["vb"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":815,"id":58,"name":"vb","prevSize":32,"code":59744},"setIdx":0,"setId":1,"iconIdx":59},{"icon":{"paths":["M226.462 832c-21.552 0-39.795-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-485.743c0-21.552 7.467-39.795 22.4-54.729s33.176-22.4 54.729-22.4h571.076c21.551 0 39.795 7.467 54.729 22.4s22.4 33.176 22.4 54.729v485.743c0 21.551-7.467 39.795-22.4 54.729s-33.178 22.4-54.729 22.4h-571.076zM315.077 633.434h111.589c12.361 0 22.836-4.292 31.425-12.881s12.885-19.063 12.885-31.424v-13.129c0-5.308-1.724-9.681-5.171-13.129s-7.821-5.171-13.129-5.171h-14.276c-5.308 0-9.681 1.724-13.129 5.171-3.446 3.447-5.169 7.821-5.169 13.129 0 1.638-0.684 3.145-2.051 4.514-1.367 1.365-2.872 2.048-4.513 2.048h-85.333c-1.641 0-3.145-0.683-4.513-2.048-1.367-1.37-2.051-2.876-2.051-4.514v-128c0-1.643 0.684-3.145 2.051-4.514 1.367-1.365 2.871-2.052 4.513-2.052h85.333c1.641 0 3.146 0.687 4.513 2.052 1.367 1.37 2.051 2.871 2.051 4.514 0 5.303 1.723 9.681 5.169 13.129 3.447 3.443 7.821 5.167 13.129 5.167h14.276c5.308 0 9.681-1.724 13.129-5.167 3.447-3.447 5.171-7.825 5.171-13.129v-13.129c0-12.363-4.297-22.838-12.885-31.426s-19.063-12.882-31.425-12.882h-111.589c-12.363 0-22.838 4.294-31.426 12.882s-12.882 19.063-12.882 31.426v154.257c0 12.361 4.294 22.835 12.882 31.424s19.063 12.881 31.426 12.881zM708.919 390.563h-111.586c-12.365 0-22.839 4.294-31.428 12.882s-12.881 19.063-12.881 31.426v154.257c0 12.361 4.292 22.835 12.881 31.424s19.063 12.881 31.428 12.881h111.586c12.365 0 22.839-4.292 31.428-12.881s12.881-19.063 12.881-31.424v-13.129c0-5.308-1.724-9.681-5.167-13.129-3.447-3.447-7.825-5.171-13.129-5.171h-14.276c-5.308 0-9.685 1.724-13.129 5.171-3.447 3.447-5.171 7.821-5.171 13.129 0 1.638-0.683 3.145-2.052 4.514-1.365 1.365-2.871 2.048-4.51 2.048h-85.333c-1.643 0-3.149-0.683-4.514-2.048-1.37-1.37-2.052-2.876-2.052-4.514v-128c0-1.643 0.683-3.145 2.052-4.514 1.365-1.365 2.871-2.052 4.514-2.052h85.333c1.638 0 3.145 0.687 4.51 2.052 1.37 1.37 2.052 2.871 2.052 4.514 0 5.303 1.724 9.681 5.171 13.129 3.443 3.443 7.821 5.167 13.129 5.167h14.276c5.303 0 9.681-1.724 13.129-5.167 3.443-3.447 5.167-7.825 5.167-13.129v-13.129c0-12.363-4.292-22.838-12.881-31.426s-19.063-12.882-31.428-12.882z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["captions"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":816,"id":59,"name":"captions","prevSize":32,"code":59713},"setIdx":0,"setId":1,"iconIdx":60},{"icon":{"paths":["M917.824 963.443l-145.724-145.719h-514.953c-21.552 0-39.795-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-485.742c0-4.431 0.615-8.479 1.846-12.145s2.941-7.030 5.129-10.093l-101.661-101.661 45.618-45.621 832.492 832.491-45.619 45.619zM905.353 738.953l-133.909-133.909c4.156-3.554 7.275-8.013 9.353-13.372 2.078-5.363 3.119-10.97 3.119-16.819v-29.538h-50.871v18.048h-3.281l-95.181-95.177v-34.462c0-1.643 0.683-3.145 2.052-4.514 1.365-1.37 2.871-2.052 4.51-2.052h85.333c1.643 0 3.149 0.683 4.514 2.052 1.37 1.37 2.052 2.871 2.052 4.514v17.229h50.871v-30.357c0-12.363-4.292-22.838-12.881-31.426s-19.063-12.882-31.428-12.882h-111.586c-11.268 0-20.992 3.679-29.171 11.036-8.175 7.357-12.949 16.533-14.315 27.529v3.282l-240.411-240.41h484.102c21.551 0 39.795 7.467 54.729 22.4s22.4 33.176 22.4 54.729v484.1zM345.763 619.157h111.59c12.361 0 22.835-4.292 31.424-12.881s12.885-19.063 12.885-31.424v-7.881l-21.662-21.658h-29.214v16.41c0 1.638-0.683 3.145-2.048 4.51-1.37 1.37-2.871 2.052-4.514 2.052h-85.333c-1.641 0-3.145-0.683-4.513-2.052-1.368-1.365-2.051-2.871-2.051-4.51v-132.595c0-1.643 0.684-3.146 2.051-4.514s2.871-2.051 4.513-2.051h18.052l-41.025-41.025h-6.564c-7.987 2.188-14.633 6.92-19.939 14.195s-7.959 15.563-7.959 24.862v154.257c0 12.361 4.294 22.835 12.882 31.424s19.063 12.881 31.426 12.881z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["captions-off"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":817,"id":60,"name":"captions-off","prevSize":32,"code":59736},"setIdx":0,"setId":1,"iconIdx":61},{"icon":{"paths":["M511.986 899.594c-5.106 0-10.23-1.178-15.371-3.528-5.142-2.355-9.71-5.53-13.703-9.523l-231.385-231.383c-6.619-6.548-9.928-14.561-9.928-24.028 0-9.472 3.299-17.377 9.898-23.726 6.424-6.344 14.229-9.513 23.416-9.513s16.954 3.169 23.3 9.513l179.694 178.632v-647.304c0-9.067 3.423-17.022 10.27-23.867s14.806-10.267 23.878-10.267c9.068 0 17.024 3.422 23.859 10.267 6.84 6.844 10.255 14.8 10.255 23.867v647.304l179.287-178.632c6.62-6.615 14.52-9.923 23.711-9.923 9.185 0 16.988 3.308 23.414 9.923 6.6 6.62 9.897 14.602 9.897 23.946s-3.308 17.28-9.928 23.808l-231.383 231.383c-3.994 3.994-8.581 7.168-13.757 9.523-5.176 2.35-10.322 3.528-15.425 3.528z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["view-last"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":818,"id":61,"name":"view-last","prevSize":32,"code":59740},"setIdx":0,"setId":1,"iconIdx":62},{"icon":{"paths":["M512 650.172c-5.141 0-9.929-0.819-14.357-2.458-4.433-1.643-8.644-4.459-12.638-8.452l-132.675-132.676c-5.907-5.909-8.929-13.333-9.065-22.276s2.884-16.503 9.065-22.686c6.181-6.182 13.784-9.382 22.81-9.6s16.629 2.761 22.811 8.943l82.050 82.052v-326.403c0-9.080 3.063-16.684 9.19-22.81s13.73-9.189 22.81-9.189c9.079 0 16.683 3.063 22.81 9.189s9.19 13.729 9.19 22.81v326.403l82.052-82.052c5.905-5.909 13.44-8.819 22.605-8.738 9.161 0.081 16.832 3.213 23.014 9.395 6.178 6.182 9.271 13.675 9.271 22.481s-3.093 16.299-9.271 22.481l-132.676 132.676c-3.994 3.994-8.205 6.81-12.638 8.452-4.429 1.638-9.216 2.458-14.357 2.458zM269.13 832c-21.552 0-39.795-7.467-54.729-22.4s-22.4-33.178-22.4-54.729v-82.871c0-9.079 3.063-16.683 9.19-22.81s13.729-9.19 22.81-9.19c9.080 0 16.684 3.063 22.81 9.19s9.19 13.73 9.19 22.81v82.871c0 3.281 1.367 6.289 4.102 9.028 2.735 2.735 5.745 4.1 9.027 4.1h485.742c3.281 0 6.289-1.365 9.028-4.1 2.735-2.739 4.1-5.747 4.1-9.028v-82.871c0-9.079 3.063-16.683 9.19-22.81s13.73-9.19 22.81-9.19c9.079 0 16.683 3.063 22.81 9.19s9.19 13.73 9.19 22.81v82.871c0 21.551-7.467 39.795-22.4 54.729s-33.178 22.4-54.729 22.4h-485.742z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["download"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":819,"id":62,"name":"download","prevSize":32,"code":59737},"setIdx":0,"setId":1,"iconIdx":63},{"icon":{"paths":["M390.564 672h242.869c10.927 0 20.087-3.699 27.477-11.089 7.394-7.39 11.089-16.55 11.089-27.477v-242.87c0-10.927-3.695-20.085-11.089-27.476-7.39-7.391-16.55-11.087-27.477-11.087h-242.869c-10.927 0-20.085 3.696-27.476 11.087s-11.087 16.55-11.087 27.476v242.87c0 10.927 3.696 20.087 11.087 27.477s16.55 11.089 27.476 11.089zM512.073 917.333c-56.064 0-108.757-10.641-158.086-31.915-49.329-21.278-92.238-50.155-128.727-86.626s-65.378-79.364-86.663-128.67c-21.286-49.306-31.929-101.99-31.929-158.050 0-56.064 10.639-108.758 31.915-158.086s50.151-92.238 86.624-128.727c36.474-36.49 79.364-65.378 128.671-86.663s101.99-31.929 158.050-31.929c56.064 0 108.757 10.638 158.084 31.915 49.331 21.277 92.241 50.151 128.73 86.624s65.378 79.364 86.66 128.671c21.286 49.306 31.932 101.991 31.932 158.051s-10.641 108.757-31.915 158.084c-21.278 49.331-50.15 92.237-86.626 128.73-36.471 36.489-79.364 65.374-128.67 86.66s-101.99 31.932-158.050 31.932z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["transcript-stop"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":820,"id":63,"name":"transcript-stop","prevSize":32,"code":59738},"setIdx":0,"setId":1,"iconIdx":64},{"icon":{"paths":["M287.922 413.537c20.512 0 38.097-7.156 52.756-21.467s21.989-32.044 21.989-53.199v-178.872c0-21.156-7.312-38.889-21.937-53.2-14.626-14.31-32.207-21.466-52.744-21.466-21.159 0-38.891 7.155-53.195 21.466s-21.457 32.044-21.457 53.2v178.872c0 21.155 7.148 38.889 21.444 53.199s32.009 21.467 53.142 21.467zM776.205 906.667c21.21 0 39.369-7.556 54.473-22.66s22.656-33.259 22.656-54.468v-656.41c0-21.21-7.552-39.367-22.656-54.471s-33.263-22.657-54.473-22.657h-285.538c-9.067 0-16.666 3.068-22.797 9.204-6.135 6.136-9.199 13.739-9.199 22.81s3.063 16.669 9.199 22.796c6.131 6.126 13.73 9.19 22.797 9.19h285.538c3.827 0 6.976 1.231 9.438 3.693s3.691 5.607 3.691 9.437v656.41c0 3.827-1.229 6.972-3.691 9.434s-5.611 3.695-9.438 3.695h-443.074c-3.829 0-6.975-1.233-9.437-3.695s-3.693-5.606-3.693-9.434v-45.952c0-9.067-3.068-16.666-9.204-22.797-6.135-6.135-13.739-9.199-22.81-9.199s-16.669 3.063-22.795 9.199c-6.127 6.131-9.19 13.73-9.19 22.797v45.952c0 21.21 7.552 39.364 22.657 54.468s33.261 22.66 54.471 22.66h443.074zM450.462 751.59h208.41c9.766 0 17.95-3.302 24.555-9.907 6.605-6.609 9.907-14.793 9.907-24.555 0-9.766-3.302-17.95-9.907-24.555s-14.788-9.907-24.555-9.907h-208.41c-9.762 0-17.946 3.302-24.553 9.907-6.605 6.605-9.907 14.788-9.907 24.555 0 9.762 3.302 17.946 9.907 24.555 6.607 6.605 14.79 9.907 24.553 9.907zM533.333 623.59h128c9.067 0 16.666-3.072 22.801-9.207 6.131-6.135 9.199-13.739 9.199-22.81s-3.068-16.666-9.199-22.793c-6.135-6.127-13.734-9.19-22.801-9.19h-128c-9.067 0-16.666 3.068-22.797 9.203-6.135 6.135-9.199 13.739-9.199 22.81s3.063 16.67 9.199 22.797c6.131 6.127 13.73 9.19 22.797 9.19zM288.151 490.667c-38.222 0-71.665-10.94-100.331-32.819-28.666-21.884-47.156-50.026-55.471-84.433-2.462-9.354-6.884-17.449-13.267-24.287s-14.095-10.256-23.137-10.256c-9.042 0-16.627 3.637-22.753 10.912s-8.178 15.59-6.154 24.943c8.041 46.933 29.497 86.564 64.369 118.893s76.403 51.802 124.594 58.419v77.295c0 9.067 3.068 16.666 9.204 22.797 6.136 6.135 13.74 9.203 22.81 9.203s16.67-3.068 22.796-9.203c6.126-6.131 9.189-13.73 9.189-22.797v-77.295c47.48-6.618 88.766-26.091 123.856-58.419 35.093-32.329 56.794-71.686 65.109-118.071 2.022-9.627 0.038-18.147-5.952-25.558-5.986-7.412-13.589-11.118-22.81-11.118-9.216 0-16.994 3.419-23.343 10.256-6.345 6.837-10.748 14.933-13.21 24.287-8.311 34.407-26.952 62.549-55.917 84.433-28.964 21.879-62.159 32.819-99.584 32.819z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["transcript"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":821,"id":64,"name":"transcript","prevSize":32,"code":59739},"setIdx":0,"setId":1,"iconIdx":65},{"icon":{"paths":["M826.701 870.349l-254.935-254.93c-21.33 15.642-44.723 27.909-70.177 36.797-25.452 8.888-52.536 13.332-81.251 13.332-71.136 0-131.593-24.888-181.37-74.665s-74.667-110.223-74.667-181.334c0-71.111 24.889-131.555 74.667-181.333s110.222-74.667 181.333-74.667c71.111 0 131.554 24.889 181.336 74.667 49.777 49.778 74.665 110.235 74.665 181.37 0 28.716-4.444 55.799-13.332 81.251s-21.156 48.845-36.803 70.18l254.935 254.93-54.4 54.4zM420.3 588.749c49.778 0 92.089-17.423 126.936-52.265 34.842-34.845 52.265-77.156 52.265-126.934s-17.423-92.089-52.265-126.933c-34.847-34.845-77.158-52.267-126.936-52.267s-92.089 17.422-126.933 52.267c-34.845 34.844-52.267 77.155-52.267 126.933s17.422 92.089 52.267 126.934c34.844 34.842 77.155 52.265 126.933 52.265z"],"width":1075,"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["search"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":58,"id":65,"name":"search","prevSize":32,"code":59717},"setIdx":0,"setId":1,"iconIdx":66},{"icon":{"paths":["M564.029 905.856c-15.754 0-27.080-5.125-33.971-15.375s-7.629-22.769-2.217-37.555l145.316-383.48c3.994-10.57 11.566-19.424 22.728-26.562 11.162-7.139 22.446-10.708 33.864-10.708 10.778 0 21.514 3.589 32.21 10.766 10.691 7.177 18.012 16.032 21.965 26.566l146.191 383.967c5.484 14.792 4.618 27.218-2.596 37.284-7.219 10.066-18.749 15.099-34.596 15.099-6.968 0-13.665-2.355-20.081-7.055-6.42-4.705-11.008-10.337-13.763-16.891l-33.659-97.408h-192.061l-33.060 96.937c-2.519 6.595-7.276 12.314-14.259 17.157-6.989 4.838-14.326 7.26-22.011 7.26zM658.386 715.581h140.227l-68.925-193.725h-2.381l-68.92 193.725zM320.665 372.935c10.536 19.097 21.731 36.712 33.584 52.846 11.853 16.133 25.766 33.252 41.738 51.359 29.812-31.671 54.013-64.027 72.602-97.066s34.128-68.184 46.616-105.435h-396.265c-11.146 0-20.489-3.774-28.030-11.323-7.54-7.549-11.31-16.903-11.31-28.062s3.774-20.512 11.322-28.061c7.549-7.549 16.903-11.323 28.062-11.323h236.306v-39.385c0-11.159 3.774-20.512 11.323-28.061 7.549-7.549 16.903-11.324 28.062-11.324s20.512 3.774 28.061 11.324c7.549 7.548 11.324 16.902 11.324 28.061v39.385h236.307c11.156 0 20.511 3.774 28.058 11.323 7.552 7.549 11.325 16.902 11.325 28.061s-3.773 20.513-11.325 28.062c-7.547 7.548-16.901 11.323-28.058 11.323h-76.349c-13.153 46.386-31.406 91.549-54.753 135.491-23.347 43.941-52.207 85.259-86.579 123.953l93.128 96.328-29.537 80.901-120.534-120.53-169.107 169.103c-7.548 7.552-16.82 11.325-27.815 11.325s-20.266-4.132-27.814-12.39c-8.261-7.547-12.39-16.819-12.39-27.817 0-10.993 4.13-20.618 12.39-28.882l170.172-170.665c-17.832-20.063-34.174-40.37-49.025-60.922s-28.13-42.142-39.836-64.766c-7.275-13.793-7.385-25.968-0.328-36.525s18.789-15.836 35.2-15.836c6.171 0 12.506 1.933 19.003 5.797s11.322 8.442 14.473 13.73z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["lang-select"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":822,"id":66,"name":"lang-select","prevSize":32,"code":59714},"setIdx":0,"setId":1,"iconIdx":67},{"icon":{"paths":["M511.982 917.315c-55.576 0-108.020-10.655-157.332-31.959-49.313-21.304-92.321-50.309-129.026-87.014s-65.709-79.713-87.014-129.024c-21.306-49.316-31.96-101.76-31.96-157.335 0-56.013 10.653-108.567 31.96-157.661 21.305-49.094 50.31-91.993 87.014-128.697s79.713-65.709 129.026-87.014c49.312-21.306 101.756-31.96 157.332-31.96 56.015 0 108.567 10.653 157.663 31.96 49.091 21.305 91.991 50.31 128.696 87.014s65.71 79.604 87.014 128.697c21.304 49.094 31.959 101.647 31.959 157.661 0 55.575-10.655 108.019-31.959 157.335-21.304 49.311-50.309 92.319-87.014 129.024s-79.606 65.71-128.696 87.014c-49.096 21.304-101.647 31.959-157.663 31.959zM511.982 852.413c21.773-28.882 40.097-58.010 54.976-87.388 14.879-29.373 26.993-61.481 36.347-96.328h-182.646c9.901 35.942 22.154 68.598 36.76 97.971 14.605 29.373 32.793 57.958 54.564 85.745zM429.358 840.678c-16.355-23.465-31.043-50.145-44.062-80.041s-23.139-60.539-30.36-91.94h-144.74c22.537 44.308 52.76 81.536 90.668 111.677 37.908 30.136 80.74 50.243 128.494 60.303zM594.606 840.678c47.754-10.061 90.583-30.167 128.497-60.303 37.903-30.141 68.127-67.369 90.665-111.677h-144.742c-8.586 31.672-19.389 62.459-32.41 92.349-13.020 29.896-27.023 56.438-42.010 79.631zM183.367 604.703h158.606c-2.68-15.867-4.622-31.411-5.825-46.648-1.203-15.232-1.805-30.592-1.805-46.072s0.602-30.837 1.805-46.072c1.203-15.234 3.145-30.783 5.825-46.646h-158.606c-4.102 14.496-7.247 29.566-9.436 45.211s-3.282 31.48-3.282 47.508c0 16.028 1.094 31.864 3.282 47.511 2.188 15.642 5.334 30.71 9.436 45.21zM405.971 604.703h212.023c2.678-15.867 4.623-31.278 5.827-46.239s1.802-30.454 1.802-46.482c0-16.028-0.599-31.522-1.802-46.482-1.203-14.961-3.149-30.373-5.827-46.237h-212.023c-2.68 15.863-4.622 31.276-5.826 46.237-1.203 14.96-1.805 30.454-1.805 46.482s0.602 31.521 1.805 46.482c1.204 14.961 3.146 30.372 5.826 46.239zM681.994 604.703h158.602c4.106-14.5 7.25-29.568 9.436-45.21 2.191-15.647 3.282-31.483 3.282-47.511s-1.091-31.864-3.282-47.508c-2.186-15.645-5.33-30.715-9.436-45.211h-158.602c2.678 15.863 4.618 31.412 5.821 46.646 1.203 15.235 1.807 30.593 1.807 46.072s-0.604 30.84-1.807 46.072c-1.203 15.237-3.144 30.781-5.821 46.648zM669.025 355.266h144.742c-22.81-44.855-52.828-82.080-90.051-111.674-37.228-29.593-80.261-49.832-129.111-60.718 16.353 24.834 30.904 51.993 43.653 81.477 12.744 29.484 22.999 59.789 30.766 90.914zM420.659 355.266h182.646c-9.902-35.665-22.359-68.527-37.376-98.586-15.012-30.058-32.998-58.434-53.947-85.129-20.951 26.695-38.934 55.071-53.948 85.129-15.016 30.058-27.474 62.921-37.375 98.586zM210.196 355.266h144.74c7.768-31.126 18.024-61.43 30.77-90.914s27.296-56.643 43.651-81.477c-49.121 10.886-92.226 31.194-129.314 60.923s-67.037 66.886-89.848 111.468z"],"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["globe"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":60,"id":67,"name":"globe","prevSize":32,"code":59716},"setIdx":0,"setId":1,"iconIdx":68},{"icon":{"paths":["M484.27 661.171v-428.639c0-7.858 2.637-14.445 7.912-19.76s11.811-7.972 19.61-7.972c7.796 0 14.406 2.657 19.818 7.972 5.417 5.316 8.125 11.902 8.125 19.76v429.048l103.388-103.798c5.837-6.18 12.483-9.272 19.937-9.272s14.019 3.062 19.692 9.185c5.903 5.729 8.858 12.237 8.858 19.517 0 7.286-2.954 13.88-8.858 19.789l-146.954 146.954c-6.743 7.004-14.612 10.501-23.603 10.501-8.989 0-16.986-3.497-23.987-10.501l-147.105-147.103c-5.716-5.74-8.351-12.278-7.904-19.615 0.447-7.337 3.488-13.819 9.122-19.456 5.907-6.18 12.512-9.201 19.814-9.068 7.303 0.138 13.907 3.159 19.814 9.068l102.32 103.388z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["down-arrow"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":823,"id":68,"name":"down-arrow","prevSize":32,"code":59711},"setIdx":0,"setId":1,"iconIdx":69},{"icon":{"paths":["M192 853.315c-17.066 0-32-6.4-44.8-19.2s-19.2-27.73-19.2-44.8v-554.664c0-17.067 6.4-32 19.2-44.8s27.734-19.2 44.8-19.2h640c17.065 0 32 6.4 44.8 19.2s19.2 27.733 19.2 44.8v554.664c0 17.070-6.4 32-19.2 44.8s-27.735 19.2-44.8 19.2h-640zM192 789.315h640v-554.664h-640v554.664zM288 638.915h151.466c9.067 0 16.667-3.067 22.8-9.196 6.133-6.134 9.2-13.737 9.2-22.804v-44.8h-53.333v23.47h-108.8v-147.202h108.8v23.467h53.333v-44.8c0-9.067-3.066-16.667-9.2-22.8s-13.733-9.2-22.8-9.2h-151.466c-9.066 0-16.667 3.066-22.8 9.2s-9.2 13.733-9.2 22.8v189.864c0 9.068 3.067 16.671 9.2 22.804 6.133 6.129 13.733 9.196 22.8 9.196zM585.6 638.915h151.465c8.535 0 16-3.2 22.4-9.6s9.6-13.865 9.6-22.4v-44.8h-53.33v23.47h-108.8v-147.202h108.8v23.467h53.33v-44.8c0-8.534-3.2-16-9.6-22.4s-13.865-9.6-22.4-9.6h-151.465c-8.535 0-16 3.2-22.4 9.6s-9.6 13.866-9.6 22.4v189.864c0 8.535 3.2 16 9.6 22.4s13.865 9.6 22.4 9.6z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["caption-mode"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":824,"id":69,"name":"caption-mode","prevSize":32,"code":59708},"setIdx":0,"setId":1,"iconIdx":70},{"icon":{"paths":["M714.634 424.534c-20.623 0-38.216-7.282-52.782-21.846-14.592-14.592-21.888-32.199-21.888-52.822v-179.2c0-21.333 7.296-39.111 21.888-53.333 14.566-14.222 32.159-21.334 52.782-21.334 21.33 0 39.112 7.111 53.33 21.334 14.223 14.222 21.335 32 21.335 53.333v179.2c0 20.622-7.112 38.23-21.335 52.822-14.218 14.563-32 21.846-53.33 21.846zM226.1 917.335c-21.334 0-39.467-7.47-54.4-22.4-14.934-14.935-22.4-33.070-22.4-54.4v-657.069c0-21.333 7.466-39.466 22.4-54.4s33.066-22.4 54.4-22.4h317.864v64h-317.864c-3.556 0-6.571 1.238-9.046 3.712-2.503 2.503-3.754 5.533-3.754 9.088v657.069c0 3.553 1.251 6.584 3.754 9.088 2.474 2.473 5.49 3.712 9.046 3.712h443.734c3.553 0 6.584-1.239 9.088-3.712 2.473-2.504 3.712-5.535 3.712-9.088v-77.87h64v77.87c0 21.33-7.47 39.465-22.4 54.4-14.935 14.93-33.070 22.4-54.4 22.4h-443.734zM309.3 762.665v-69.33h277.334v69.33h-277.334zM309.3 634.665v-64h192v64h-192zM746.634 672h-64v-108.8c-53.335-7.823-98.673-31.647-136.023-71.466-37.318-39.822-55.978-87.111-55.978-141.867h64.001c0 42.667 15.826 78.578 47.488 107.734 31.631 29.155 69.135 43.734 112.512 43.734 44.088 0 81.777-14.578 113.065-43.734s46.935-65.066 46.935-107.734h64c0 54.756-18.488 102.045-55.47 141.867-36.977 39.82-82.488 63.643-136.53 71.466v108.8z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["stt"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":825,"id":70,"name":"stt","prevSize":32,"code":59709},"setIdx":0,"setId":1,"iconIdx":71},{"icon":{"paths":["M309.35 714.629h277.33v-63.995h-277.33v63.995zM309.35 543.964h405.33v-63.997h-405.33v63.997zM309.35 373.298h405.33v-63.997h-405.33v63.997zM226.479 874.629c-21.553 0-39.795-7.465-54.729-22.4-14.933-14.93-22.4-33.172-22.4-54.728v-571.073c0-21.552 7.467-39.795 22.4-54.728s33.176-22.4 54.729-22.4h571.074c21.55 0 39.798 7.466 54.728 22.4 14.935 14.933 22.4 33.176 22.4 54.728v571.073c0 21.555-7.465 39.798-22.4 54.728-14.93 14.935-33.178 22.4-54.728 22.4h-571.074zM226.479 810.634h571.074c3.282 0 6.292-1.367 9.027-4.106 2.734-2.734 4.101-5.745 4.101-9.027v-571.073c0-3.282-1.367-6.291-4.101-9.027s-5.745-4.103-9.027-4.103h-571.074c-3.282 0-6.291 1.368-9.027 4.103s-4.103 5.745-4.103 9.027v571.073c0 3.282 1.368 6.292 4.103 9.027 2.736 2.739 5.745 4.106 9.027 4.106z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["transcript-mode"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":811,"id":71,"name":"transcript-mode","prevSize":32,"code":59710},"setIdx":0,"setId":1,"iconIdx":72},{"icon":{"paths":["M916.178 808.381l-724.878-724.881-36.204 36.204 724.878 724.881 36.204-36.204zM520.996 342.147l-181.969-181.97c13.932-4.385 28.922-6.577 44.973-6.577 40.558 0 74.35 13.999 101.378 41.995 27.062 27.997 40.589 63.001 40.589 105.013 0 14.702-1.659 28.549-4.972 41.539zM624.307 445.457l159.237 159.235c17.167-16.927 25.748-37.212 25.748-60.861v-201.139c0-24.131-8.934-44.612-26.808-61.444-17.869-16.832-39.613-25.248-65.234-25.248s-47.508 8.416-65.664 25.248c-18.186 16.832-27.279 37.314-27.279 61.444v102.765zM242.032 300.609c0-3.318 0.084-6.593 0.253-9.823l156.257 156.257c-4.746 0.417-9.594 0.625-14.542 0.625-40.558 0-74.35-13.999-101.378-41.995-27.060-28.030-40.59-63.052-40.59-105.063zM551.286 599.788c9.718 26.045 26.312 48.922 49.777 68.628 14.618 12.252 30.423 21.775 47.416 28.564l97.377 97.377v49.096c0 7.301-2.826 13.614-8.489 18.949-5.663 5.33-12.37 7.997-20.116 7.997s-14.454-2.668-20.116-7.997c-5.663-5.335-8.489-11.648-8.489-18.949v-80.794c-53.617-5.601-99.036-26.081-136.264-61.445-37.228-35.333-59.118-77.696-65.666-127.089-1.201-8.402 1.029-15.549 6.691-21.448 1.809-1.889 3.816-3.471 6.019-4.751l51.86 51.86zM840.335 661.484l40.73 40.73 1.055-0.998c37.228-35.333 58.819-77.696 64.763-127.089 1.203-8.402-1.029-15.549-6.687-21.448-5.663-5.898-13.256-8.847-22.779-8.847-7.148 0-13.107 2.115-17.884 6.339-4.772 4.204-7.762 9.953-8.965 17.254-5.453 36.541-22.2 67.896-50.232 94.060zM142.698 768c-18.949 0-34.669-6.472-47.16-19.41-12.492-12.974-18.738-28.902-18.738-47.79v-29.394c0-21.007 5.564-40.095 16.694-57.272 11.161-17.142 25.875-30.264 44.143-39.373 39.195-18.888 79.753-33.423 121.673-43.607 41.888-10.153 83.451-15.227 124.69-15.227 3.533 0 7.071 0.036 10.612 0.113 24.093 0.507 40.588 23.060 40.588 47.16 0 83.743 40.209 158.095 102.374 204.8h-394.876z"],"attrs":[{"fill":"rgb(250, 250, 250)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["demote-filled"],"grid":0},"attrs":[{"fill":"rgb(250, 250, 250)"}],"properties":{"order":812,"id":72,"name":"demote-filled","prevSize":32,"code":59666},"setIdx":0,"setId":1,"iconIdx":73},{"icon":{"paths":["M242.255 290.755c-0.17 3.241-0.255 6.525-0.255 9.854 0 42.012 13.53 77.033 40.59 105.063 27.027 27.997 60.82 41.995 101.378 41.995 4.958 0 9.815-0.209 14.571-0.628l-156.284-156.285z","M783.544 604.692l-159.237-159.235v-102.765c0-24.131 9.093-44.612 27.279-61.444 18.156-16.832 40.044-25.248 65.664-25.248s47.365 8.416 65.234 25.248c17.874 16.832 26.808 37.314 26.808 61.444v201.139c0 23.649-8.581 43.935-25.748 60.861z","M551.286 599.788c9.718 26.045 26.312 48.922 49.777 68.628 14.618 12.252 30.423 21.775 47.416 28.564l97.377 97.377v49.096c0 7.301-2.826 13.614-8.489 18.949-5.663 5.33-12.37 7.997-20.116 7.997s-14.454-2.668-20.116-7.997c-5.663-5.335-8.489-11.648-8.489-18.949v-80.794c-53.617-5.601-99.036-26.081-136.264-61.445-37.228-35.333-59.118-77.696-65.666-127.089-1.201-8.402 1.029-15.549 6.691-21.448 1.809-1.889 3.816-3.471 6.019-4.751l51.86 51.86z","M881.065 702.213l-40.73-40.73c28.032-26.163 44.78-57.518 50.232-94.060 1.203-7.301 4.193-13.051 8.965-17.254 4.777-4.224 10.737-6.339 17.884-6.339 9.523 0 17.116 2.949 22.779 8.847 5.658 5.898 7.89 13.046 6.687 21.448-5.944 49.393-27.535 91.756-64.763 127.089-0.348 0.338-0.701 0.666-1.055 0.998z","M520.97 342.121l-181.951-181.952c13.925-4.38 28.908-6.569 44.949-6.569 40.558 0 74.35 13.999 101.378 41.995 27.059 27.997 40.591 63.001 40.591 105.013 0 14.692-1.654 28.53-4.966 41.513z","M472.767 721.577c12.172 15.985 26.406 30.285 42.299 42.496h-370.662c-18.74 0-34.701-6.641-47.882-19.917-13.148-13.307-19.722-29.071-19.722-47.288v-29.389c0-21.007 5.709-40.1 17.126-57.272 11.45-17.142 26.196-30.264 44.237-39.373 40.909-18.888 82.867-33.423 125.873-43.607 42.973-10.153 85.613-15.227 127.92-15.227 2.838 0 5.677 0.020 8.519 0.067 18.754 0.302 31.166 18.514 28.382 37.064-2.504 16.676-16.772 30.259-33.638 30.136l-3.262-0.015c-37.447 0-75.41 4.89-113.889 14.674-38.513 9.81-75.094 22.415-109.745 37.811-7.623 3.492-13.698 8.233-18.225 14.213-4.493 5.949-6.74 13.128-6.74 21.53v29.389h283.354c18.351 0 34.94 10.112 46.058 24.709z","M191.3 83.5l724.877 724.877-36.204 36.204-724.877-724.877 36.204-36.204z"],"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["demote-outlined"],"grid":0},"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"properties":{"order":813,"id":73,"name":"demote-outlined","prevSize":32,"code":59682},"setIdx":0,"setId":1,"iconIdx":74},{"icon":{"paths":["M524.8 204.8c35.346 0 64 28.654 64 64v486.4c0 35.346-28.654 64-64 64s-64-28.654-64-64v-486.4c0-35.346 28.654-64 64-64z","M780.8 358.4c35.346 0 64 28.654 64 64v179.2c0 35.346-28.654 64-64 64s-64-28.654-64-64v-179.2c0-35.346 28.654-64 64-64z","M268.8 358.4c35.346 0 64 28.654 64 64v179.2c0 35.346-28.654 64-64 64s-64-28.654-64-64v-179.2c0-35.346 28.654-64 64-64z"],"attrs":[{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["active-speaker"],"grid":0},"attrs":[{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"}],"properties":{"order":67,"id":74,"name":"active-speaker","prevSize":32,"code":59651},"setIdx":0,"setId":1,"iconIdx":75},{"icon":{"paths":["M491.52 705.536c9.556 0 17.408-3.072 23.552-9.216s9.216-13.996 9.216-23.552c0-9.556-3.072-17.408-9.216-23.552s-13.996-9.216-23.552-9.216c-9.556 0-17.408 3.072-23.552 9.216s-9.216 13.996-9.216 23.552c0 9.556 3.072 17.408 9.216 23.552s13.996 9.216 23.552 9.216zM460.8 556.032h61.44v-245.76h-61.44v245.76zM491.52 901.12c-53.932 0-104.612-10.24-152.044-30.72-47.459-20.48-88.596-48.128-123.412-82.944s-62.464-75.952-82.944-123.412c-20.48-47.432-30.72-98.111-30.72-152.044s10.24-104.625 30.72-152.084c20.48-47.432 48.128-88.556 82.944-123.372s75.953-62.464 123.412-82.944c47.432-20.48 98.111-30.72 152.044-30.72s104.624 10.24 152.084 30.72c47.432 20.48 88.556 48.128 123.372 82.944s62.464 75.94 82.944 123.372c20.48 47.459 30.72 98.152 30.72 152.084s-10.24 104.612-30.72 152.044c-20.48 47.46-48.128 88.596-82.944 123.412s-75.94 62.464-123.372 82.944c-47.46 20.48-98.152 30.72-152.084 30.72z"],"width":983,"attrs":[{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["alert"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"}],"properties":{"order":68,"id":75,"name":"alert","prevSize":32,"code":59684},"setIdx":0,"setId":1,"iconIdx":76},{"icon":{"paths":["M512 625.050q-7.475 0-14.95-2.662t-13.824-10.138l-188.826-187.699q-9.626-10.701-9.062-25.088 0.512-14.387 10.138-24.013 11.725-10.65 24.525-10.138 12.8 0.563 23.45 10.138l168.55 169.626 169.574-168.55q9.626-10.65 22.426-10.65t24.525 10.65q10.701 10.65 10.701 24.525t-10.701 23.501l-187.75 187.699q-6.349 7.475-13.824 10.138t-14.95 2.662z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-down"],"grid":0},"attrs":[{}],"properties":{"order":69,"id":76,"name":"arrow-down","prevSize":32,"code":59648},"setIdx":0,"setId":1,"iconIdx":77},{"icon":{"paths":["M295.475 616.55q-10.701-9.626-10.701-23.501t10.701-24.525l187.75-186.675q6.349-7.475 13.824-10.65t14.95-3.174 14.95 3.174 13.824 10.65l188.826 187.75q9.626 10.65 10.138 23.45t-10.138 23.501q-10.65 10.65-24.013 10.65-13.312 0-24.013-10.65l-169.574-167.475-169.574 168.499q-9.626 10.701-22.938 10.701-13.363 0-24.013-11.725z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["arrow-up"],"grid":0},"attrs":[{}],"properties":{"order":70,"id":77,"name":"arrow-up","prevSize":32,"code":59649},"setIdx":0,"setId":1,"iconIdx":78},{"icon":{"paths":["M512 837.333l-325.333-325.333 325.333-325.333 45.867 45.867-248.533 247.467h528v64h-528l248.533 247.467-45.867 45.867z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["back-btn"],"defaultCode":59650,"grid":0},"attrs":[{}],"properties":{"order":71,"id":78,"name":"back-btn","prevSize":32,"code":59650},"setIdx":0,"setId":1,"iconIdx":79},{"icon":{"paths":["M106.6 187.751c0-22.767 7.834-41.967 23.501-57.6 15.633-15.667 34.833-23.501 57.6-23.501h648.498c22.769 0 41.969 7.834 57.6 23.501 15.667 15.633 23.501 34.833 23.501 57.6v477.849c0 22.769-7.834 41.969-23.501 57.6-15.631 15.631-34.831 23.45-57.6 23.45h-578.098l-82.125 81.101c-12.834 12.8-27.597 15.821-44.288 9.062-16.726-6.758-25.088-19.031-25.088-36.813v-612.249zM266.6 386.151h490.699v-68.301h-490.699v68.301zM266.6 535.45h322.149v-68.249h-322.149v68.249z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":72,"id":79,"name":"chat-filled","prevSize":32,"code":59652},"setIdx":0,"setId":1,"iconIdx":80},{"icon":{"paths":["M298.667 586.667h257.067q12.8 0 21.888-9.088 9.045-9.045 9.045-22.912t-9.045-22.955q-9.088-9.045-22.955-9.045h-257.067q-12.8 0-21.845 9.045-9.088 9.088-9.088 22.955t9.088 22.912q9.045 9.088 22.912 9.088zM298.667 458.667h427.733q12.8 0 21.888-9.088 9.045-9.045 9.045-22.912t-9.045-22.955q-9.088-9.045-22.955-9.045h-427.733q-12.8 0-21.845 9.045-9.088 9.088-9.088 22.955t9.088 22.912q9.045 9.088 22.912 9.088zM298.667 330.667h427.733q12.8 0 21.888-9.088 9.045-9.045 9.045-22.912t-9.045-22.912q-9.088-9.088-22.955-9.088h-427.733q-12.8 0-21.845 9.088-9.088 9.045-9.088 22.912t9.088 22.912q9.045 9.088 22.912 9.088zM106.667 804.267v-620.8q0-32 22.4-54.4t54.4-22.4h657.067q32 0 54.4 22.4t22.4 54.4v486.4q0 32-22.4 54.4t-54.4 22.4h-582.4l-86.4 85.333q-18.133 18.133-41.6 8.021-23.467-10.155-23.467-35.755z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat-nav"],"grid":0},"attrs":[{}],"properties":{"order":73,"id":80,"name":"chat-nav","prevSize":32,"code":59655},"setIdx":0,"setId":1,"iconIdx":81},{"icon":{"paths":["M106.6 800v-612.249c0-22.767 7.834-41.967 23.501-57.6 15.633-15.667 34.833-23.501 57.6-23.501h648.498c22.769 0 41.969 7.834 57.6 23.501 15.667 15.633 23.501 34.833 23.501 57.6v477.849c0 22.769-7.834 41.969-23.501 57.6-15.631 15.631-34.831 23.45-57.6 23.45h-578.098l-82.125 81.101c-12.834 12.8-27.597 15.821-44.288 9.062-16.726-6.758-25.088-19.031-25.088-36.813zM174.901 732.774l54.374-54.374h606.923c3.584 0 6.605-1.244 9.062-3.738 2.493-2.493 3.738-5.514 3.738-9.062v-477.849c0-3.584-1.244-6.605-3.738-9.062-2.458-2.492-5.478-3.738-9.062-3.738h-648.498c-3.584 0-6.605 1.246-9.062 3.738-2.492 2.458-3.738 5.478-3.738 9.062v545.023zM174.901 187.751v-12.8 12.8z","M588.749 535.45h-322.149v-68.249h322.149v68.249z","M757.299 386.151h-490.699v-68.301h490.699v68.301z"],"attrs":[{"fill":"rgb(197, 197, 197)"},{"fill":"rgb(197, 197, 197)"},{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["chat-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"},{"fill":"rgb(197, 197, 197)"},{"fill":"rgb(197, 197, 197)"}],"properties":{"order":74,"id":81,"name":"chat-outlined","prevSize":32,"code":59664},"setIdx":0,"setId":1,"iconIdx":82},{"icon":{"paths":["M456.6 650.65l239.976-238.9-39.475-39.475-200.501 199.476-90.675-89.601-39.475 39.476 130.15 129.024zM512.051 900.25c-53.318 0-103.63-10.122-150.939-30.362-47.274-20.275-88.525-48.010-123.75-83.2-35.191-35.226-62.925-76.477-83.2-123.75-20.241-47.309-30.362-97.623-30.362-150.938 0-54.033 10.121-104.516 30.362-151.45 20.275-46.933 48.009-88.013 83.2-123.238 35.226-35.191 76.476-62.925 123.75-83.2 47.309-20.241 97.622-30.362 150.939-30.362 54.031 0 104.515 10.121 151.45 30.362 46.93 20.275 88.013 48.009 123.238 83.2 35.19 35.226 62.925 76.305 83.2 123.238 20.239 46.934 30.362 97.417 30.362 151.45 0 53.315-10.122 103.629-30.362 150.938-20.275 47.273-48.010 88.525-83.2 123.75-35.226 35.19-76.308 62.925-123.238 83.2-46.935 20.239-97.418 30.362-151.45 30.362z"],"attrs":[{"fill":"rgb(41, 193, 87)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["check"],"defaultCode":59653,"grid":0},"attrs":[{"fill":"rgb(41, 193, 87)"}],"properties":{"order":75,"id":82,"name":"check","prevSize":32,"code":59653},"setIdx":0,"setId":1,"iconIdx":83},{"icon":{"paths":["M213.333 938.667c-23.467 0-43.563-8.35-60.288-25.045-16.697-16.725-25.045-36.821-25.045-60.288v-597.333h85.333v597.333h469.333v85.333h-469.333zM384 768c-23.467 0-43.549-8.35-60.245-25.045-16.725-16.725-25.088-36.821-25.088-60.288v-512c0-23.467 8.363-43.563 25.088-60.288 16.697-16.697 36.779-25.045 60.245-25.045h384c23.467 0 43.563 8.349 60.288 25.045 16.695 16.725 25.045 36.821 25.045 60.288v512c0 23.467-8.35 43.563-25.045 60.288-16.725 16.695-36.821 25.045-60.288 25.045h-384z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["clipboard"],"defaultCode":59654,"grid":0},"attrs":[{}],"properties":{"order":76,"id":83,"name":"clipboard","prevSize":32,"code":59654},"setIdx":0,"setId":1,"iconIdx":84},{"icon":{"paths":["M512 556.8l-216.533 216.533q-8.533 8.533-21.845 9.045-13.355 0.555-22.955-9.045t-9.6-22.4 9.6-22.4l216.533-216.533-216.533-216.533q-8.533-8.533-9.045-21.888-0.555-13.312 9.045-22.912t22.4-9.6 22.4 9.6l216.533 216.533 216.533-216.533q8.533-8.533 21.888-9.088 13.312-0.512 22.912 9.088t9.6 22.4-9.6 22.4l-216.533 216.533 216.533 216.533q8.533 8.533 9.045 21.845 0.555 13.355-9.045 22.955t-22.4 9.6-22.4-9.6z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["close"],"grid":0},"attrs":[{}],"properties":{"order":77,"id":84,"name":"close","prevSize":32,"code":59656},"setIdx":0,"setId":1,"iconIdx":85},{"icon":{"paths":["M42.667 560.213v-53.76h362.246l-130.885-126.72 39.662-38.4 198.31 192-198.31 192-39.662-38.4 130.885-126.72h-362.246zM512 533.333l198.31-192 39.663 38.4-130.884 126.72h362.244v53.76h-362.244l130.884 126.72-39.663 38.4-198.31-192z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["collapse"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":810,"id":85,"name":"collapse","prevSize":32,"code":59667},"setIdx":0,"setId":1,"iconIdx":86},{"icon":{"paths":["M323.091 562.97c27.378-20.864 56.734-37.257 88.067-49.182 31.334-11.921 64.947-17.882 100.843-17.882s69.508 5.961 100.843 17.882c31.334 11.925 60.689 28.318 88.068 49.182l168.832-165.428c-52.932-40.538-109.060-72.431-168.375-95.681-59.319-23.249-122.445-34.874-189.367-34.874s-130.046 11.625-189.365 34.874c-59.319 23.249-115.444 55.143-168.375 95.681l168.832 165.428zM512 810.667c-3.652 0-6.997-0.597-10.039-1.788-3.042-1.195-6.084-3.281-9.126-6.259l-399.72-391.665c-5.475-5.365-8.061-11.625-7.757-18.778s3.194-13.115 8.67-17.885c57.798-50.076 122.137-89.421 193.016-118.036s145.866-42.922 224.957-42.922c79.091 0 154.078 14.307 224.956 42.922 70.882 28.615 135.219 67.96 193.015 118.036 5.478 4.769 8.367 10.731 8.67 17.885 0.307 7.153-2.278 13.413-7.757 18.778l-399.718 391.665c-3.042 2.978-6.084 5.065-9.126 6.259-3.042 1.19-6.387 1.788-10.039 1.788z"],"attrs":[{"fill":"rgb(255, 171, 0)","opacity":0.6}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-bad"],"defaultCode":59657,"grid":0},"attrs":[{"fill":"rgb(255, 171, 0)","opacity":0.6}],"properties":{"order":79,"id":86,"name":"connection-bad","prevSize":32,"code":59657},"setIdx":0,"setId":1,"iconIdx":87},{"icon":{"paths":["M512 810.667c-4.561 0-8.951-0.649-13.175-1.941-4.25-1.293-8-3.878-11.26-7.757l-392.905-388.847c-6.516-7.111-9.617-15.192-9.305-24.242 0.339-9.050 3.766-16.808 10.282-23.273 58.643-49.131 123.475-86.626 194.498-112.485s144.98-38.788 221.865-38.788c76.885 0 150.844 12.929 221.867 38.788s135.855 63.354 194.496 112.485c6.515 6.465 9.946 14.223 10.283 23.273 0.311 9.050-2.79 17.131-9.306 24.242l-392.905 388.847c-3.26 3.878-6.997 6.464-11.221 7.757-4.25 1.293-8.653 1.941-13.214 1.941z"],"attrs":[{"fill":"rgb(41, 193, 87)","opacity":0.6}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-good"],"defaultCode":59658,"grid":0},"attrs":[{"fill":"rgb(41, 193, 87)","opacity":0.6}],"properties":{"order":80,"id":87,"name":"connection-good","prevSize":32,"code":59658},"setIdx":0,"setId":1,"iconIdx":88},{"icon":{"paths":["M490.667 298.667c58.91 0 106.667-47.756 106.667-106.667s-47.757-106.667-106.667-106.667c-58.91 0-106.667 47.756-106.667 106.667s47.757 106.667 106.667 106.667zM341.333 277.333c0 58.91-47.756 106.667-106.667 106.667s-106.667-47.756-106.667-106.667c0-58.91 47.756-106.667 106.667-106.667s106.667 47.756 106.667 106.667zM725.333 341.333c23.565 0 42.667-19.103 42.667-42.667s-19.102-42.667-42.667-42.667c-23.565 0-42.667 19.103-42.667 42.667s19.102 42.667 42.667 42.667zM832 597.333c35.345 0 64-28.655 64-64s-28.655-64-64-64c-35.345 0-64 28.655-64 64s28.655 64 64 64zM810.667 789.333c0 35.345-28.655 64-64 64s-64-28.655-64-64c0-35.345 28.655-64 64-64s64 28.655 64 64zM490.667 938.667c35.345 0 64-28.655 64-64s-28.655-64-64-64c-35.345 0-64 28.655-64 64s28.655 64 64 64zM341.333 768c0 47.13-38.205 85.333-85.333 85.333s-85.333-38.204-85.333-85.333c0-47.13 38.205-85.333 85.333-85.333s85.333 38.204 85.333 85.333zM170.667 640c47.128 0 85.333-38.204 85.333-85.333s-38.205-85.333-85.333-85.333c-47.128 0-85.333 38.204-85.333 85.333s38.205 85.333 85.333 85.333z"],"attrs":[{"fill":"rgb(179, 179, 179)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-loading"],"defaultCode":59659,"grid":0},"attrs":[{"fill":"rgb(179, 179, 179)"}],"properties":{"order":81,"id":88,"name":"connection-loading","prevSize":32,"code":59659},"setIdx":0,"setId":1,"iconIdx":89},{"icon":{"paths":["M511.735 810.667c-4.565 0-8.96-0.649-13.184-1.941-4.254-1.293-8.009-3.878-11.268-7.757l-392.17-387.878c-6.52-7.111-9.78-15.515-9.78-25.212s3.26-17.455 9.78-23.273c58.027-49.777 122.731-87.441 194.11-112.989 71.406-25.522 145.576-38.284 222.512-38.284 76.932 0 151.104 12.929 222.511 38.788 71.377 25.859 136.081 63.354 194.108 112.485 6.519 6.465 9.95 14.223 10.287 23.273 0.316 9.050-2.462 16.808-8.329 23.273l-6.848 5.818c-16.951-18.101-37.645-32.646-62.084-43.636-24.461-10.99-49.732-16.485-75.81-16.485-52.16 0-96.495 18.101-133.005 54.303-36.514 36.2-54.767 80.164-54.767 131.88 0 28.442 5.709 54.455 17.131 78.042 11.396 23.607 26.223 43.17 44.48 58.684l-123.226 121.212c-3.26 3.878-7.002 6.464-11.226 7.757-4.254 1.293-8.661 1.941-13.222 1.941zM785.57 690.423c-7.172 0-13.039-2.415-17.604-7.253-4.565-4.86-6.195-10.202-4.89-16.017 1.301-16.162 5.385-29.739 12.245-40.73 6.831-10.991 18.722-25.212 35.674-42.667 13.692-13.577 22.822-24.243 27.383-32 4.565-7.757 6.848-16.482 6.848-26.18 0-12.928-4.723-24.41-14.161-34.445-9.468-10.005-23.33-15.010-41.583-15.010-11.085 0-21.359 2.419-30.827 7.253-9.442 4.86-17.421 11.494-23.94 19.9-4.565 5.815-9.783 9.694-15.65 11.635-5.867 1.937-11.409 1.617-16.627-0.969-6.519-2.59-10.756-6.788-12.71-12.608-1.958-5.82-1.63-11.315 0.977-16.486 10.432-15.514 24.124-28.288 41.075-38.319 16.951-10.010 36.186-15.014 57.702-15.014 29.99 0 54.438 8.883 73.348 26.65 18.906 17.792 28.361 40.585 28.361 68.382 0 14.869-3.26 27.955-9.779 39.253-6.519 11.328-18.91 26.364-37.163 45.111-11.738 10.991-19.887 20.365-24.452 28.122-4.561 7.757-7.497 17.131-8.802 28.122-1.301 6.464-4.237 11.959-8.802 16.482-4.561 4.527-10.103 6.788-16.623 6.788zM784.593 797.090c-9.131 0-16.785-3.23-22.963-9.698-6.208-6.464-9.314-14.221-9.314-23.27s3.106-16.806 9.314-23.275c6.178-6.464 13.833-9.694 22.963-9.694 9.126 0 16.951 3.23 23.471 9.694 6.519 6.468 9.779 14.225 9.779 23.275s-3.26 16.806-9.779 23.27c-6.519 6.468-14.345 9.698-23.471 9.698z"],"attrs":[{"fill":"rgb(179, 179, 179)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-unpublished"],"defaultCode":59660,"grid":0},"attrs":[{"fill":"rgb(179, 179, 179)"}],"properties":{"order":82,"id":89,"name":"connection-unpublished","prevSize":32,"code":59660},"setIdx":0,"setId":1,"iconIdx":90},{"icon":{"paths":["M512 801.126c-4.561 0-8.951-0.64-13.175-1.911-4.25-1.271-8-3.814-11.26-7.633l-392.905-382.636c-6.516-6.997-9.617-15.102-9.305-24.313 0.339-9.236 3.766-16.718 10.282-22.443 57.991-48.346 122.329-85.242 193.012-110.688 70.71-25.446 145.16-38.168 223.351-38.168s152.627 12.723 223.313 38.168c70.707 25.446 135.057 62.342 193.050 110.688 6.515 5.725 9.946 13.206 10.283 22.443 0.311 9.211-2.79 17.316-9.306 24.313l-92.851 90.651h-254.118c-19.546 0-36.16 6.677-49.843 20.036s-20.527 29.581-20.527 48.666v232.828zM616.58 802.078c-5.214-5.090-7.821-11.601-7.821-19.541 0-7.966 2.607-14.81 7.821-20.535l61.572-60.113-61.572-60.117c-5.214-5.086-7.821-11.601-7.821-19.541 0-7.966 2.607-14.81 7.821-20.535 5.862-5.086 12.873-7.633 21.030-7.633 8.132 0 14.805 2.547 20.019 7.633l61.572 60.117 61.577-60.117c5.867-5.086 12.877-7.633 21.035-7.633 8.132 0 14.801 2.547 20.015 7.633 5.867 5.726 8.798 12.57 8.798 20.535 0 7.94-2.931 14.455-8.798 19.541l-61.572 60.117 61.572 60.113c5.867 5.726 8.798 12.57 8.798 20.535 0 7.94-2.931 14.451-8.798 19.541-5.214 5.726-11.883 8.589-20.015 8.589-8.158 0-15.168-2.863-21.035-8.589l-61.577-60.113-61.572 60.113c-5.214 5.726-11.887 8.589-20.019 8.589-8.158 0-15.168-2.863-21.030-8.589z"],"attrs":[{"fill":"rgb(179, 179, 179)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-unsupported"],"defaultCode":59661,"grid":0},"attrs":[{"fill":"rgb(179, 179, 179)"}],"properties":{"order":83,"id":90,"name":"connection-unsupported","prevSize":32,"code":59661},"setIdx":0,"setId":1,"iconIdx":91},{"icon":{"paths":["M387.886 626.458c17.644-13.709 36.96-24.439 57.951-32.192 20.988-7.748 43.042-11.622 66.163-11.622s45.175 3.874 66.163 11.622c20.992 7.753 40.307 18.483 57.95 32.192l233.63-228.917c-52.932-40.538-109.060-72.431-168.375-95.68-59.319-23.249-122.445-34.874-189.367-34.874s-130.046 11.625-189.365 34.874c-59.319 23.249-115.444 55.143-168.375 95.68l233.626 228.917zM512 810.667c-3.652 0-6.997-0.597-10.039-1.788-3.042-1.195-6.084-3.281-9.126-6.259l-399.72-391.665c-5.475-5.365-8.061-11.625-7.757-18.778s3.194-13.115 8.67-17.885c57.798-50.076 122.137-89.421 193.016-118.036s145.866-42.922 224.957-42.922c79.091 0 154.078 14.307 224.956 42.922 70.882 28.615 135.219 67.96 193.015 118.036 5.478 4.769 8.367 10.731 8.67 17.885 0.307 7.153-2.278 13.413-7.757 18.778l-399.718 391.665c-3.042 2.978-6.084 5.065-9.126 6.259-3.042 1.19-6.387 1.788-10.039 1.788z"],"attrs":[{"fill":"rgb(255, 65, 77)","opacity":0.6}],"isMulticolor":false,"isMulticolor2":false,"tags":["connection-very-bad"],"defaultCode":59662,"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)","opacity":0.6}],"properties":{"order":84,"id":91,"name":"connection-very-bad","prevSize":32,"code":59662},"setIdx":0,"setId":1,"iconIdx":92},{"icon":{"paths":["M814.249 946.069c-70.816 102.289-222.025 102.289-292.846 0l-461.498-666.614c-81.771-118.113 2.766-279.455 146.422-279.455l922.998 0c143.658 0 228.196 161.343 146.423 279.456l-461.499 666.613z"],"width":1336,"attrs":[{"fill":"rgb(85, 85, 85)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["downside-triangle"],"defaultCode":59702,"grid":0},"attrs":[{"fill":"rgb(85, 85, 85)"}],"properties":{"order":85,"id":92,"name":"downside-triangle","prevSize":32,"code":59702},"setIdx":0,"setId":1,"iconIdx":93},{"icon":{"paths":["M533.325 341.333c83.913 0 166.571 16.896 247.979 50.688 81.438 33.763 153.801 84.425 217.088 151.979 8.533 8.533 12.8 18.487 12.8 29.867s-4.267 21.333-12.8 29.867l-98.133 96c-7.821 7.821-16.883 12.087-27.179 12.8-10.325 0.713-19.755-2.133-28.288-8.533l-123.733-93.867c-5.687-4.267-9.954-9.246-12.8-14.933s-4.267-12.087-4.267-19.2v-121.6c-27.021-8.533-54.754-15.287-83.2-20.267s-57.6-7.467-87.467-7.467c-29.867 0-59.021 2.487-87.467 7.467-28.444 4.979-56.177 11.733-83.2 20.267v121.6c0 7.113-1.422 13.513-4.267 19.2s-7.111 10.667-12.8 14.933l-123.733 93.867c-8.533 6.4-17.948 9.246-28.245 8.533-10.325-0.713-19.399-4.979-27.221-12.8l-98.133-96c-8.533-8.533-12.8-18.487-12.8-29.867s4.267-21.333 12.8-29.867c62.578-67.554 134.756-118.215 216.533-151.979 81.778-33.792 164.621-50.688 248.533-50.688z"],"width":1067,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["end-call"],"defaultCode":59663,"grid":0},"attrs":[{}],"properties":{"order":86,"id":93,"name":"end-call","prevSize":32,"code":59663},"setIdx":0,"setId":1,"iconIdx":94},{"icon":{"paths":["M42.667 499.823l201.155-201.156 40.231 40.231-132.762 132.761h721.474l-132.762-132.761 40.23-40.231 201.156 201.156-201.156 201.152-40.23-40.23 132.762-132.762h-721.474l132.762 132.762-40.231 40.23-201.155-201.152z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["expand"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":809,"id":94,"name":"expand","prevSize":32,"code":59683},"setIdx":0,"setId":1,"iconIdx":95},{"icon":{"paths":["M170.667 85.333c-47.128 0-85.333 38.205-85.333 85.333v213.333c0 47.13 38.205 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.128-38.204-85.333-85.333-85.333h-213.333z","M170.667 554.667c-47.128 0-85.333 38.204-85.333 85.333v213.333c0 47.13 38.205 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.13-38.204-85.333-85.333-85.333h-213.333z","M554.667 170.667c0-47.128 38.204-85.333 85.333-85.333h213.333c47.13 0 85.333 38.205 85.333 85.333v213.333c0 47.13-38.204 85.333-85.333 85.333h-213.333c-47.13 0-85.333-38.204-85.333-85.333v-213.333z","M640 554.667c-47.13 0-85.333 38.204-85.333 85.333v213.333c0 47.13 38.204 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.13-38.204-85.333-85.333-85.333h-213.333z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["grid"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":808,"id":95,"name":"grid","prevSize":32,"code":59719},"setIdx":0,"setId":1,"iconIdx":96},{"icon":{"paths":["M484.299 706.15h55.502v-245.351h-55.502v245.351zM512.051 396.799c8.53 0 15.647-3.021 21.35-9.062 5.663-6.042 8.499-13.329 8.499-21.862 0-7.816-2.836-14.746-8.499-20.787-5.704-6.075-12.82-9.114-21.35-9.114-8.535 0-15.652 2.85-21.352 8.55-5.666 5.701-8.499 12.817-8.499 21.35s2.833 15.821 8.499 21.862c5.7 6.042 12.817 9.062 21.352 9.062zM512.051 900.25c-53.318 0-103.63-10.122-150.939-30.362-47.274-20.275-88.525-48.010-123.75-83.2-35.191-35.226-62.925-76.477-83.2-123.75-20.241-47.309-30.362-97.623-30.362-150.938 0-54.033 10.121-104.516 30.362-151.45 20.275-46.933 48.009-88.013 83.2-123.238 35.226-35.191 76.476-62.925 123.75-83.2 47.309-20.241 97.622-30.362 150.939-30.362 54.031 0 104.515 10.121 151.45 30.362 46.93 20.275 88.013 48.009 123.238 83.2 35.19 35.226 62.925 76.476 83.2 123.75 20.239 47.309 30.362 97.622 30.362 150.938 0 53.315-10.122 103.629-30.362 150.938-20.275 47.273-48.010 88.525-83.2 123.75-35.226 35.19-76.477 62.925-123.75 83.2-47.309 20.239-97.623 30.362-150.938 30.362zM512.051 844.8c92.431 0 171.008-32.358 235.725-97.075s97.075-143.293 97.075-235.725c0-92.433-32.358-171.008-97.075-235.725s-143.293-97.075-235.725-97.075c-92.434 0-171.010 32.358-235.726 97.075s-97.075 143.292-97.075 235.725c0 92.432 32.358 171.009 97.075 235.725s143.292 97.075 235.726 97.075z"],"attrs":[{"opacity":0.5}],"isMulticolor":false,"isMulticolor2":false,"tags":["info"],"defaultCode":59665,"grid":0},"attrs":[{"opacity":0.5}],"properties":{"order":89,"id":96,"name":"info","prevSize":32,"code":59665},"setIdx":0,"setId":1,"iconIdx":97},{"icon":{"paths":["M739.554 512c-15.642 0-28.442 12.8-28.442 28.446v170.667h-398.224v-398.224h170.665c15.646 0 28.446-12.8 28.446-28.444s-12.8-28.445-28.446-28.445h-170.665c-31.289 0-56.889 25.6-56.889 56.889v398.224c0 31.287 25.6 56.887 56.889 56.887h398.224c31.287 0 56.887-25.6 56.887-56.887v-170.667c0-15.646-12.8-28.446-28.446-28.446z","M753.792 256h-150.473c-12.8 0-19.055 15.36-9.954 24.178l55.181 55.182-221.867 221.867c-2.632 2.633-4.721 5.76-6.146 9.199-1.425 3.443-2.159 7.13-2.159 10.854s0.734 7.411 2.159 10.854c1.425 3.439 3.514 6.566 6.146 9.199 2.637 2.633 5.76 4.723 9.203 6.148 3.439 1.425 7.13 2.159 10.85 2.159 3.725 0 7.415-0.734 10.854-2.159s6.566-3.516 9.199-6.148l221.585-221.582 55.181 55.182c9.101 8.819 24.461 2.56 24.461-10.24v-150.471c0-7.964-6.255-14.222-14.221-14.222z"],"attrs":[{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["link-share"],"defaultCode":59707,"grid":0},"attrs":[{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(9, 157, 253)"}],"properties":{"order":90,"id":97,"name":"link-share","prevSize":32,"code":59707},"setIdx":0,"setId":1,"iconIdx":98},{"icon":{"paths":["M256 85.495c23.564 0 42.667 19.103 42.667 42.667v128c0 23.564-19.103 42.667-42.667 42.667h-128c-23.564 0-42.667-19.102-42.667-42.667v-128c0-23.564 19.103-42.667 42.667-42.667h128zM640 128.161c0-23.564-19.102-42.667-42.667-42.667h-170.667c-23.564 0-42.667 19.103-42.667 42.667v128c0 23.564 19.103 42.667 42.667 42.667h170.667c23.565 0 42.667-19.102 42.667-42.667v-128zM938.667 128.161c0-23.564-19.102-42.667-42.667-42.667h-128c-23.565 0-42.667 19.103-42.667 42.667v128c0 23.564 19.102 42.667 42.667 42.667h128c23.565 0 42.667-19.102 42.667-42.667v-128zM128 384.161c-23.564 0-42.667 19.103-42.667 42.668v469.333c0 23.565 19.103 42.667 42.667 42.667h768c23.565 0 42.667-19.102 42.667-42.667v-469.333c0-23.565-19.102-42.668-42.667-42.668h-768z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["list-view"],"defaultCode":59668,"grid":0},"attrs":[{}],"properties":{"order":91,"id":98,"name":"list-view","prevSize":32,"code":59668},"setIdx":0,"setId":1,"iconIdx":99},{"icon":{"paths":["M800.676 751.514c12.416-33.055 18.621-69.868 18.621-110.423v-381.694c0-14.127-4.413-25.524-13.245-34.191-8.858-8.698-20.485-13.047-34.872-13.047-14.392 0-26.015 4.349-34.877 13.047-8.827 8.667-13.24 20.064-13.24 34.191v212.083c0 4.472-1.48 8.158-4.434 11.058-2.918 2.899-6.676 4.349-11.264 4.349s-8.182-1.45-10.793-4.349c-2.637-2.899-3.958-6.585-3.958-11.058v-324.833c0-13.51-4.572-24.752-13.711-33.728-9.175-9.006-20.628-13.51-34.36-13.51-14.418 0-26.045 4.504-34.872 13.51-8.832 8.976-13.245 20.218-13.245 33.728v324.833c0 4.472-1.475 8.158-4.429 11.058s-6.723 4.349-11.31 4.349c-4.557 0-8.31-1.45-11.264-4.349s-4.434-6.585-4.434-11.058v-374.013c0-14.127-4.255-25.369-12.769-33.728s-19.968-12.538-34.355-12.538c-14.392 0-26.017 4.179-34.878 12.538-8.828 8.359-13.243 19.601-13.243 33.728v329.177l330.934 324.871zM439.296 396.755l-95.247-93.503v-123.849c0-14.127 4.257-25.523 12.772-34.19 8.515-8.698 19.967-13.047 34.357-13.047 14.422 0 26.047 4.349 34.875 13.047 8.829 8.667 13.243 20.064 13.243 34.19v217.353zM533.555 921.6c-64.128 0-121.076-17.193-170.844-51.584-49.737-34.36-86.717-84.316-110.942-149.857l-93.268-239.056c-5.876-14.126-6.363-26.664-1.461-37.614 4.901-10.918 13.573-16.378 26.015-16.378 18.977 0 36.321 6.585 52.030 19.756s26.188 26.834 31.435 40.991l44.16 107.937c1.32 2.591 7.211 6.124 17.673 10.598h15.694v-105.644l-292.849-292.848 54.306-54.306 814.635 814.639-54.303 54.303-154.163-154.158c-48.251 35.482-107.628 53.222-178.12 53.222z"],"attrs":[{"fill":"rgb(9, 157, 253)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["lower-hand"],"grid":0},"attrs":[{"fill":"rgb(9, 157, 253)"}],"properties":{"order":92,"id":99,"name":"lower-hand","prevSize":32,"code":59669},"setIdx":0,"setId":1,"iconIdx":100},{"icon":{"paths":["M735.991 604.804l-48-48c7.113-12.087 12.629-25.6 16.555-40.533 3.895-14.933 5.845-30.579 5.845-46.933h64c0 26.313-3.371 50.658-10.112 73.045-6.771 22.413-16.201 43.221-28.288 62.421zM598.391 466.138l-58.667-58.667v-194.133c0-12.089-4.083-22.229-12.245-30.421-8.192-8.164-18.334-12.245-30.421-12.245s-22.217 4.081-30.379 12.245c-8.192 8.192-12.288 18.332-12.288 30.421v107.733l-64-64v-43.733c0-29.867 10.311-55.111 30.933-75.733 20.621-20.623 45.867-30.933 75.733-30.933s55.113 10.311 75.733 30.933c20.621 20.622 30.933 45.867 30.933 75.733v228.267c0 4.979-0.525 9.417-1.579 13.312-1.079 3.925-2.334 7.667-3.755 11.221zM465.058 885.338v-140.8c-70.4-7.821-128.881-37.858-175.445-90.112-46.592-52.279-69.888-113.975-69.888-185.088h64c0 59.021 20.622 109.325 61.866 150.912 41.245 41.613 91.733 62.421 151.467 62.421 27.021 0 52.621-4.979 76.8-14.933s45.513-23.467 64-40.533l45.867 45.867c-20.621 19.2-44.087 35.2-70.4 48s-54.4 20.621-84.267 23.467v140.8h-64zM847.991 926.938l-776.533-776.533 44.8-44.8 776.533 775.467-44.8 45.867z","M857.617 935.467l-787.2-786.135 45.867-44.8 786.134 786.135-44.8 44.8z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-off"],"defaultCode":59670,"grid":0},"attrs":[{},{}],"properties":{"order":93,"id":100,"name":"mic-off","prevSize":32,"code":59670},"setIdx":0,"setId":1,"iconIdx":101},{"icon":{"paths":["M741.325 609.075l-52.275-51.2c4.982-7.818 9.078-16.538 12.288-26.163 3.21-9.59 5.514-19.712 6.912-30.361 1.434-9.25 5.002-16.538 10.701-21.862 5.668-5.359 12.764-8.038 21.299-8.038 10.685 0 19.574 3.738 26.675 11.213 7.132 7.475 9.984 16.538 8.55 27.187-2.867 18.498-6.963 36.111-12.288 52.838-5.325 16.691-12.611 32.154-21.862 46.387zM605.85 474.676l-215.45-216.525v-40.55c0-30.584 10.838-56.713 32.512-78.387 21.709-21.709 47.855-32.563 78.438-32.563 30.582 0 56.525 10.854 77.824 32.563 21.366 21.674 32.051 47.803 32.051 78.387v225.075c0 5.666-0.358 11.537-1.075 17.613-0.717 6.042-2.15 10.837-4.301 14.387zM817.050 907.725l-717.824-716.799c-6.417-6.383-9.626-14.2-9.626-23.45s3.209-17.425 9.626-24.525c7.1-7.134 15.274-10.701 24.525-10.701s17.066 3.567 23.45 10.701l717.875 716.799c6.385 7.101 9.574 15.089 9.574 23.962 0 8.909-3.19 16.911-9.574 24.013-7.101 6.415-15.273 9.626-24.525 9.626s-17.085-3.21-23.501-9.626zM467.2 851.2v-102.4c-64-7.101-118.221-33.060-162.662-77.875-44.442-44.785-70.57-98.473-78.387-161.075-1.434-10.65 1.399-19.712 8.499-27.187 7.134-7.475 16.026-11.213 26.675-11.213 7.817 0 14.575 2.679 20.275 8.038 5.7 5.325 9.609 12.612 11.725 21.862 7.134 51.199 30.242 94.039 69.325 128.511 39.117 34.473 85.35 51.712 138.701 51.712 25.6 0 49.766-4.608 72.499-13.824 22.769-9.252 43.044-21.709 60.826-37.376l50.125 50.176c-19.901 18.468-42.481 33.572-67.738 45.312s-52.465 19.389-81.613 22.938v102.4c0 9.252-3.379 17.254-10.138 24.013s-14.746 10.138-23.962 10.138c-9.25 0-17.254-3.379-24.013-10.138s-10.138-14.761-10.138-24.013z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-off-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":94,"id":101,"name":"mic-off-filled","prevSize":32,"code":59720},"setIdx":0,"setId":1,"iconIdx":102},{"icon":{"paths":["M741.325 609.075l-52.275-51.2c4.265-7.818 8.192-16.538 11.776-26.163 3.548-9.59 6.026-19.712 7.424-30.361 1.434-9.25 5.002-16.538 10.701-21.862 5.668-5.359 12.764-8.038 21.299-8.038 10.685 0 19.574 3.738 26.675 11.213 7.132 7.475 9.984 16.538 8.55 27.187-2.867 18.498-6.963 36.111-12.288 52.838-5.325 16.691-12.611 32.154-21.862 46.387zM605.85 474.676l-62.925-62.925v-194.15c0-12.083-4.081-22.221-12.237-30.413-8.192-8.158-17.971-12.237-29.338-12.237-12.083 0-22.221 4.079-30.413 12.237-8.192 8.192-12.288 18.33-12.288 30.413v108.8l-68.25-68.25v-40.55c0-30.584 10.838-56.713 32.512-78.387 21.709-21.709 47.855-32.563 78.438-32.563 30.582 0 56.525 10.854 77.824 32.563 21.366 21.674 32.051 47.803 32.051 78.387v225.075c0 5.666-0.358 11.537-1.075 17.613-0.717 6.042-2.15 10.837-4.301 14.387zM817.050 907.725l-717.824-716.799c-6.417-6.383-9.626-14.2-9.626-23.45s3.209-17.425 9.626-24.525c7.1-7.134 15.274-10.701 24.525-10.701s17.066 3.567 23.45 10.701l717.875 716.799c6.385 7.101 9.574 15.089 9.574 23.962 0 8.909-3.19 16.911-9.574 24.013-7.101 6.415-15.273 9.626-24.525 9.626s-17.085-3.21-23.501-9.626zM467.2 851.2v-103.475c-64-7.101-118.221-32.87-162.662-77.312s-70.57-97.961-78.387-160.563c-1.434-10.65 1.229-19.712 7.987-27.187s15.821-11.213 27.187-11.213c7.817 0 14.575 2.679 20.275 8.038 5.7 5.325 9.609 12.612 11.725 21.862 6.417 50.483 29.354 93.148 68.813 127.999 39.458 34.816 85.862 52.224 139.213 52.224 25.6 0 49.956-4.439 73.062-13.312s43.197-21.504 60.262-37.888l50.125 50.176c-19.901 18.468-42.481 33.572-67.738 45.312s-52.465 19.031-81.613 21.862v103.475c0 9.252-3.379 17.254-10.138 24.013s-14.746 10.138-23.962 10.138c-9.25 0-17.254-3.379-24.013-10.138s-10.138-14.761-10.138-24.013z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-off-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":95,"id":102,"name":"mic-off-outlined","prevSize":32,"code":59721},"setIdx":0,"setId":1,"iconIdx":103},{"icon":{"paths":["M512 599.671c-32.367 0-59.725-11.298-82.074-33.899-22.351-22.601-33.526-50.266-33.526-82.995v-280.549c0-32.731 11.175-60.396 33.526-82.996 22.349-22.6 49.707-33.9 82.074-33.9s59.725 11.3 82.074 33.9c22.353 22.6 33.527 50.265 33.527 82.996v280.549c0 32.73-11.174 60.395-33.527 82.995-22.349 22.601-49.707 33.899-82.074 33.899zM512 938.667c-10.018 0-18.295-3.302-24.832-9.911-6.566-6.639-9.847-15.027-9.847-25.156v-119.232c-70.131-7.795-129.657-36.442-178.578-85.943-48.952-49.468-77.282-108.881-84.989-178.24-1.541-10.133 1.156-18.893 8.092-26.278 6.936-7.42 15.799-11.127 26.588-11.127 8.477 0 15.983 3.115 22.519 9.348 6.566 6.238 10.62 14.029 12.161 23.381 7.706 56.887 33.138 104.614 76.295 143.172 43.157 38.592 94.023 57.886 152.591 57.886s109.436-19.294 152.593-57.886c43.153-38.558 68.587-86.285 76.292-143.172 1.545-9.353 5.581-17.143 12.117-23.381 6.566-6.234 14.089-9.348 22.562-9.348 10.79 0 19.652 3.708 26.59 11.127 6.938 7.386 9.634 16.145 8.090 26.278-7.706 69.359-36.019 128.772-84.941 178.24-48.951 49.502-108.493 78.148-178.624 85.943v119.232c0 10.129-3.268 18.517-9.801 25.156-6.566 6.609-14.861 9.911-24.879 9.911z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-on"],"defaultCode":59671,"grid":0},"attrs":[{}],"properties":{"order":96,"id":103,"name":"mic-on","prevSize":32,"code":59671},"setIdx":0,"setId":1,"iconIdx":104},{"icon":{"paths":["M513.075 582.4c-30.582 0-56.712-10.839-78.386-32.512-21.709-21.709-32.563-47.855-32.563-78.438v-254.925c0-30.584 10.854-56.542 32.563-77.875 21.674-21.334 47.804-32 78.386-32 30.587 0 56.545 10.666 77.875 32 21.335 21.333 32 47.291 32 77.875v254.925c0 30.583-10.665 56.729-32 78.438-21.33 21.673-47.288 32.512-77.875 32.512zM513.075 885.35c-9.249 0-17.253-3.379-24.012-10.138s-10.138-14.761-10.138-24.013v-102.4c-64-7.101-118.221-33.060-162.662-77.875-44.442-44.785-70.57-98.473-78.387-161.075-1.434-10.65 1.229-19.712 7.987-27.187s15.821-11.213 27.187-11.213c7.817 0 14.763 2.679 20.838 8.038 6.042 5.325 9.762 12.612 11.162 21.862 7.134 50.483 30.242 93.148 69.325 127.999 39.117 34.816 85.35 52.224 138.7 52.224 52.603 0 98.458-17.408 137.574-52.224 39.117-34.852 62.228-77.517 69.325-127.999 1.434-9.25 5.002-16.538 10.701-21.862 5.704-5.359 12.82-8.038 21.35-8.038 11.366 0 20.429 3.738 27.187 11.213s9.421 16.538 7.987 27.187c-7.096 62.602-32.87 116.29-77.312 161.075-44.472 44.815-98.693 70.774-162.662 77.875v102.4c0 9.252-3.379 17.254-10.138 24.013s-14.761 10.138-24.013 10.138z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-on-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":97,"id":104,"name":"mic-on-filled","prevSize":32,"code":59722},"setIdx":0,"setId":1,"iconIdx":105},{"icon":{"paths":["M513.075 582.4c-30.582 0-56.712-10.839-78.386-32.512-21.709-21.709-32.563-47.855-32.563-78.438v-254.925c0-30.584 10.854-56.542 32.563-77.875 21.674-21.334 47.804-32 78.386-32 30.587 0 56.545 10.666 77.875 32 21.335 21.333 32 47.291 32 77.875v254.925c0 30.583-10.665 56.729-32 78.438-21.33 21.673-47.288 32.512-77.875 32.512zM513.075 885.35c-9.249 0-17.253-3.379-24.012-10.138s-10.138-14.761-10.138-24.013v-103.475c-64-7.101-118.221-32.87-162.662-77.312s-70.57-97.961-78.387-160.563c-1.434-10.65 1.229-19.712 7.987-27.187s15.821-11.213 27.187-11.213c7.817 0 14.763 2.679 20.838 8.038 6.042 5.325 9.762 12.612 11.162 21.862 7.134 50.483 30.242 93.148 69.325 127.999 39.117 34.816 85.35 52.224 138.7 52.224 52.603 0 98.458-17.408 137.574-52.224 39.117-34.852 62.228-77.517 69.325-127.999 1.434-9.25 5.002-16.538 10.701-21.862 5.704-5.359 12.82-8.038 21.35-8.038 11.366 0 20.429 3.738 27.187 11.213s9.421 16.538 7.987 27.187c-7.096 62.602-32.87 116.121-77.312 160.563-44.472 44.442-98.693 70.211-162.662 77.312v103.475c0 9.252-3.379 17.254-10.138 24.013s-14.761 10.138-24.013 10.138zM513.075 514.15c12.083 0 22.036-4.095 29.85-12.287 7.818-8.192 11.725-18.33 11.725-30.413v-254.925c0-12.083-3.907-22.033-11.725-29.85-7.813-7.817-17.766-11.725-29.85-11.725-12.082 0-22.22 3.908-30.412 11.725-8.158 7.816-12.237 17.766-12.237 29.85v254.925c0 12.083 4.079 22.221 12.237 30.413 8.192 8.192 18.33 12.287 30.412 12.287z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["mic-on-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":98,"id":105,"name":"mic-on-outlined","prevSize":32,"code":59723},"setIdx":0,"setId":1,"iconIdx":106},{"icon":{"paths":["M441.617 226.089c0-19.549 6.852-36.159 20.553-49.828 13.67-13.7 30.276-20.55 49.826-20.55s36.16 6.85 49.83 20.55c13.7 13.669 20.548 30.278 20.548 49.828s-6.848 36.175-20.548 49.874c-13.67 13.669-30.281 20.503-49.83 20.503s-36.156-6.834-49.826-20.503c-13.7-13.7-20.553-30.325-20.553-49.874zM441.617 497.045c0-19.55 6.852-36.16 20.553-49.83 13.67-13.7 30.276-20.549 49.826-20.549s36.16 6.848 49.83 20.549c13.7 13.67 20.548 30.281 20.548 49.83s-6.848 36.156-20.548 49.826c-13.67 13.7-30.281 20.553-49.83 20.553s-36.156-6.852-49.826-20.553c-13.7-13.67-20.553-30.276-20.553-49.826zM441.617 768c0-19.55 6.852-36.16 20.553-49.826 13.67-13.7 30.276-20.553 49.826-20.553s36.16 6.852 49.83 20.553c13.7 13.666 20.548 30.276 20.548 49.826s-6.848 36.173-20.548 49.873c-13.67 13.67-30.281 20.506-49.83 20.506s-36.156-6.835-49.826-20.506c-13.7-13.7-20.553-30.323-20.553-49.873z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["more-menu"],"defaultCode":59672,"grid":0},"attrs":[{}],"properties":{"order":99,"id":106,"name":"more-menu","prevSize":32,"code":59672},"setIdx":0,"setId":1,"iconIdx":107},{"icon":{"paths":["M857.604 928.495l-763.508-786.65c-5.842-6.028-8.763-13.563-8.763-22.605s3.286-16.954 9.859-23.735c6.572-6.781 14.43-10.172 23.573-10.172 9.114 0 16.957 3.391 23.529 10.172l762.413 786.649c5.841 6.029 8.764 13.564 8.764 22.605s-3.285 16.951-9.86 23.735c-6.571 6.78-14.238 10.172-23.002 10.172s-16.431-3.392-23.006-10.172zM784.213 628.979l-461.173-474.701h381.207c22.639 0 41.626 7.912 56.96 23.735 15.339 15.823 23.006 35.037 23.006 57.642v206.836l119.398-123.198c6.575-6.781 14.067-8.484 22.481-5.108 8.384 3.406 12.574 10.006 12.574 19.802v318.728c0 9.796-4.19 16.38-12.574 19.759-8.414 3.405-15.906 1.715-22.481-5.065l-119.398-123.196v84.766zM200.352 155.408l582.764 601.289c-0.73 20.343-9.126 38.050-25.195 53.12s-34.325 22.605-54.771 22.605h-498.417c-21.908 0-40.531-7.91-55.866-23.735-15.336-15.821-23.004-35.034-23.004-57.643v-515.389c0-20.344 7.303-38.805 21.908-55.382s32.133-24.866 52.58-24.866z","M298.667 1024c-40.818 0-79.395-7.842-115.733-23.522s-68.071-37.082-95.2-64.213c-27.129-27.127-48.533-58.859-64.213-95.198s-23.52-75.166-23.52-116.48c0-40.819 7.84-79.394 23.52-115.733s37.085-67.947 64.213-94.827c27.129-26.88 58.862-48.162 95.2-63.842s75.165-23.518 116.48-23.518c40.818 0 79.395 7.838 115.733 23.518 36.339 15.68 67.947 36.962 94.827 63.842s48.158 58.487 63.838 94.827c15.68 36.339 23.522 75.166 23.522 116.48 0 40.819-7.842 79.394-23.522 115.733s-36.958 68.070-63.838 95.198c-26.88 27.132-58.487 48.533-94.827 64.213-36.338 15.68-75.165 23.522-116.48 23.522z","M256 533.333v256h85.333v-256h-85.333z","M298.667 938.667c23.564 0 42.667-19.102 42.667-42.667s-19.103-42.667-42.667-42.667c-23.564 0-42.667 19.102-42.667 42.667s19.103 42.667 42.667 42.667z"],"attrs":[{"fill":"rgb(143, 143, 143)"},{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":false,"tags":["no-cam"],"grid":0},"attrs":[{"fill":"rgb(143, 143, 143)"},{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":100,"id":107,"name":"no-cam","prevSize":32,"code":59673,"codes":[59673,59674,59675,59676]},"setIdx":0,"setId":1,"iconIdx":108},{"icon":{"paths":["M742.306 612.459l-51.61-50.795c5.35-8.277 9.937-17.681 13.764-28.22 3.823-10.534 6.498-21.444 8.026-32.734 1.532-9.028 5.551-16.555 12.066-22.575 6.485-6.016 13.931-9.028 22.345-9.028 10.705 0 19.499 3.584 26.381 10.748 6.878 7.134 9.557 15.59 8.026 25.374-3.059 20.318-7.829 39.309-14.315 56.977-6.511 17.698-14.741 34.449-24.683 50.253zM594.351 465.719l-223.656-221.233v-46.279c0-31.605 11.087-58.319 33.262-80.141s49.321-32.734 81.432-32.734c32.115 0 59.26 10.911 81.434 32.734 22.178 21.822 33.263 48.536 33.263 80.141v241.553c0 5.265-0.563 9.963-1.698 14.084-1.161 4.156-2.509 8.115-4.036 11.874zM838.652 929.638l-786.81-774.323c-6.117-6.020-9.175-13.74-9.175-23.162 0-9.391 3.058-17.473 9.175-24.245 6.882-6.772 15.11-10.159 24.683-10.159 9.542 0 17.373 3.386 23.49 10.159l786.808 773.193c6.118 6.771 9.178 14.869 9.178 24.29 0 9.391-3.059 17.472-9.178 24.247-6.878 6.020-15.091 9.028-24.636 9.028-9.57 0-17.417-3.008-23.535-9.028zM450.982 875.456v-115.132c-69.582-7.526-128.658-35.187-177.228-82.987-48.539-47.765-76.632-105.139-84.278-172.109-1.529-9.783 1.147-18.24 8.029-25.374 6.882-7.164 15.675-10.748 26.38-10.748 8.411 0 15.675 3.012 21.792 9.028 6.117 6.020 10.323 13.547 12.617 22.575 7.646 54.933 32.879 101.018 75.699 138.253 42.819 37.261 93.284 55.893 151.396 55.893 29.056 0 56.585-5.265 82.581-15.804 25.997-10.534 48.939-24.832 68.817-42.889l49.318 48.533c-22.174 20.318-47.407 37.052-75.695 50.206-28.292 13.184-58.496 21.658-90.611 25.421v115.132c0 9.783-3.243 17.882-9.728 24.29-6.515 6.383-14.741 9.574-24.683 9.574-9.937 0-18.15-3.191-24.636-9.574-6.515-6.409-9.771-14.507-9.771-24.29z","M298.667 1024c-40.818 0-79.395-7.842-115.733-23.522s-68.071-37.082-95.2-64.213c-27.129-27.127-48.533-58.859-64.213-95.198s-23.52-75.166-23.52-116.48c0-40.819 7.84-79.394 23.52-115.733s37.085-67.947 64.213-94.827c27.129-26.88 58.862-48.162 95.2-63.842s75.165-23.518 116.48-23.518c40.818 0 79.395 7.838 115.733 23.518 36.339 15.68 67.947 36.962 94.827 63.842s48.158 58.487 63.838 94.827c15.68 36.339 23.522 75.166 23.522 116.48 0 40.819-7.842 79.394-23.522 115.733s-36.958 68.070-63.838 95.198c-26.88 27.132-58.487 48.533-94.827 64.213-36.338 15.68-75.165 23.522-116.48 23.522z","M256 533.333v256h85.333v-256h-85.333z","M298.667 938.667c23.564 0 42.667-19.102 42.667-42.667s-19.103-42.667-42.667-42.667c-23.564 0-42.667 19.102-42.667 42.667s19.103 42.667 42.667 42.667z"],"attrs":[{"fill":"rgb(143, 143, 143)"},{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":false,"tags":["no-mic"],"defaultCode":59677,"grid":0},"attrs":[{"fill":"rgb(143, 143, 143)"},{"fill":"rgb(255, 171, 0)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":101,"id":108,"name":"no-mic","prevSize":32,"codes":[59677,59678,59679,59680],"code":59677},"setIdx":0,"setId":1,"iconIdx":109},{"icon":{"paths":["M-4.25 772.271v-71.467c0-32.713 16.185-58.85 48.555-78.421 32.341-19.541 74.467-29.312 126.379-29.312 7.822 0 15.118 0.354 21.888 1.067l20.779 2.133c-10.667 14.933-18.489 31.117-23.467 48.555-4.978 17.408-7.467 35.712-7.467 54.912v72.533h-186.667zM251.75 772.271v-72.533c0-47.646 24.007-86.217 72.021-115.712 47.986-29.525 110.733-44.288 188.246-44.288 78.221 0 141.154 14.763 188.8 44.288 47.646 29.495 71.467 68.066 71.467 115.712v72.533h-520.534zM841.617 772.271v-72.533c0-19.2-2.658-37.504-7.979-54.912-5.346-17.438-13.001-33.621-22.955-48.555l20.821-2.133c6.741-0.713 14.025-1.067 21.845-1.067 51.913 0 94.050 9.771 126.421 29.312 32.341 19.571 48.512 45.709 48.512 78.421v71.467h-186.667zM170.683 551.471c-24.178 0-44.8-8.533-61.867-25.6s-25.6-37.687-25.6-61.867c0-24.179 8.533-44.985 25.6-62.421 17.067-17.408 37.689-26.112 61.867-26.112s44.8 8.704 61.867 26.112c17.067 17.436 25.6 38.242 25.6 62.421s-8.533 44.8-25.6 61.867c-17.067 17.067-37.689 25.6-61.867 25.6zM853.35 551.471c-24.179 0-44.8-8.533-61.867-25.6s-25.6-37.687-25.6-61.867c0-24.179 8.533-44.985 25.6-62.421 17.067-17.408 37.687-26.112 61.867-26.112s44.8 8.704 61.867 26.112c17.067 17.436 25.6 38.242 25.6 62.421s-8.533 44.8-25.6 61.867c-17.067 17.067-37.687 25.6-61.867 25.6zM512.017 507.738c-36.979 0-68.267-12.8-93.867-38.4s-38.4-56.533-38.4-92.8c0-36.978 12.8-68.267 38.4-93.867s56.888-38.4 93.867-38.4c36.979 0 68.267 12.8 93.867 38.4s38.4 56.889 38.4 93.867c0 36.267-12.8 67.2-38.4 92.8s-56.887 38.4-93.867 38.4z"],"attrs":[{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["participants"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"}],"properties":{"order":807,"id":109,"name":"participants","prevSize":32,"code":59686},"setIdx":0,"setId":1,"iconIdx":110},{"icon":{"paths":["M809.574 374.374l-153.6-154.624 49.101-48.026q20.275-20.275 48.538-20.275t48.538 20.275l56.525 56.525q19.2 20.275 19.2 49.613t-20.275 48.538zM196.25 874.65q-15.974 0-28.262-12.237-12.237-12.288-12.237-28.262v-97.075q0-7.475 2.662-14.387 2.662-6.963 9.062-13.363l441.6-441.6 153.6 153.6-441.6 441.6q-6.4 6.4-13.363 9.062-6.912 2.662-14.387 2.662z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["pencil-filled"],"grid":0},"attrs":[{}],"properties":{"order":103,"id":110,"name":"pencil-filled","prevSize":32,"code":59712},"setIdx":0,"setId":1,"iconIdx":111},{"icon":{"paths":["M224 806.4h56.525l385.075-385.075-55.45-56.525-386.15 386.15zM809.574 374.374l-153.6-154.624 49.101-48.026q20.275-20.275 48.538-20.275t48.538 20.275l56.525 56.525q19.2 20.275 19.2 49.613t-20.275 48.538zM196.25 874.65q-15.974 0-28.262-12.237-12.237-12.288-12.237-28.262v-97.075q0-7.475 2.662-14.387 2.662-6.963 9.062-13.363l441.6-441.6 153.6 153.6-441.6 441.6q-6.4 6.4-13.363 9.062-6.912 2.662-14.387 2.662zM637.85 393.626l-27.699-28.826 55.45 56.525z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["pencil-outlined"],"grid":0},"attrs":[{}],"properties":{"order":104,"id":111,"name":"pencil-outlined","prevSize":32,"code":59735},"setIdx":0,"setId":1,"iconIdx":112},{"icon":{"paths":["M136.6 789.35v-73.626c0-16.348 3.908-30.566 11.725-42.65s18.842-22.39 33.075-30.925c34.816-19.932 71.441-35.942 109.875-48.026 38.366-12.083 82.090-18.125 131.174-18.125s92.807 6.042 131.176 18.125c38.431 12.083 75.059 28.093 109.875 48.026 14.234 8.535 25.257 18.842 33.075 30.925 7.813 12.083 11.725 26.301 11.725 42.65v73.626h-571.701zM785.101 789.35v-70.4c0-26.317-5.156-50.499-15.462-72.55-10.312-22.052-23.639-40.192-39.987-54.426 19.896 5.699 39.629 12.646 59.187 20.838 19.558 8.156 38.943 17.935 58.163 29.338 12.083 7.101 21.862 17.577 29.338 31.437 7.439 13.86 11.162 28.979 11.162 45.363v70.4h-102.4zM422.45 499.201c-36.966 0-68.25-12.971-93.85-38.912-25.6-25.976-38.4-57.088-38.4-93.338 0-37.001 12.8-68.301 38.4-93.901s56.883-38.4 93.85-38.4c36.966 0 68.25 12.8 93.851 38.4 25.6 25.6 38.4 56.9 38.4 93.901 0 36.25-12.8 67.362-38.4 93.338-25.602 25.941-56.885 38.912-93.851 38.912zM722.176 366.951c0 36.25-12.8 67.362-38.4 93.338-25.6 25.941-56.883 38.912-93.85 38.912-2.15 0-3.4 0.17-3.738 0.512-0.343 0.375-1.946 0.205-4.813-0.512 15.631-18.501 27.904-38.776 36.813-60.826 8.873-22.016 13.312-45.824 13.312-71.424 0-26.317-4.628-50.33-13.875-72.038-9.216-21.675-21.299-41.762-36.25-60.262h8.55c36.966 0 68.25 12.8 93.85 38.4s38.4 56.9 38.4 93.901z"],"attrs":[{"fill":"rgb(119, 119, 119)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["people"],"grid":0},"attrs":[{"fill":"rgb(119, 119, 119)"}],"properties":{"order":800,"id":112,"name":"people","prevSize":32,"code":59718},"setIdx":0,"setId":1,"iconIdx":113},{"icon":{"paths":["M487.619 499.756c-39.281 0-72.804-14.061-100.571-42.181-27.767-28.092-41.651-61.44-41.651-100.043 0-39.28 13.884-72.804 41.651-100.571s61.291-41.65 100.571-41.65c39.283 0 72.806 13.883 100.571 41.65s41.652 61.291 41.652 100.571c0 38.603-13.887 71.951-41.652 100.043-27.765 28.12-61.289 42.181-100.571 42.181zM182.857 808.58v-90.414c0-19.641 5.418-37.927 16.254-54.857s25.397-30.135 43.683-39.619c39.957-19.641 80.43-34.377 121.417-44.208 40.96-9.811 82.096-14.711 123.408-14.711s82.461 4.901 123.451 14.711c40.96 9.83 81.418 24.566 121.373 44.208 18.286 9.484 32.846 22.689 43.686 39.619 10.835 16.93 16.252 35.216 16.252 54.857v90.414h-609.524z"],"width":975,"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["person"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":106,"id":113,"name":"person","prevSize":32,"code":59681},"setIdx":0,"setId":1,"iconIdx":114},{"icon":{"paths":["M572.252 651.402l0.036 132.762-39.209 39.209-140.291-140.293-156.908 156.908-39.209-0.036-0.036-39.209 156.908-156.908-140.29-140.289 39.208-39.208 132.76 0.036 205.166-205.167-36.204-36.204 39.209-39.208 259.441 259.437-39.209 39.209-36.204-36.204-205.169 205.167z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["pin-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":107,"id":114,"name":"pin-filled","prevSize":32,"code":59724},"setIdx":0,"setId":1,"iconIdx":115},{"icon":{"paths":["M572.252 651.402l0.036 132.762-39.209 39.209-140.291-140.293-156.908 156.908-39.209-0.036-0.036-39.209 156.908-156.908-140.29-140.289 39.208-39.208 132.76 0.036 205.166-205.167-36.204-36.204 39.209-39.208 259.441 259.437-39.209 39.209-36.204-36.204-205.169 205.167zM308.289 520.166l208.171 208.169v-99.558l221.752-221.75-108.616-108.612-221.747 221.751h-99.56z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["pin-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":108,"id":115,"name":"pin-outlined","prevSize":32,"code":59725},"setIdx":0,"setId":1,"iconIdx":116},{"icon":{"paths":["M170.667 85.333c-47.128 0-85.333 38.205-85.333 85.333v213.333c0 47.13 38.205 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.128-38.204-85.333-85.333-85.333h-213.333z","M170.667 554.667c-47.128 0-85.333 38.204-85.333 85.333v213.333c0 47.13 38.205 85.333 85.333 85.333h213.333c47.13 0 85.333-38.204 85.333-85.333v-213.333c0-47.13-38.204-85.333-85.333-85.333h-213.333z","M554.667 170.667c0-47.128 38.204-85.333 85.333-85.333h213.333c47.13 0 85.333 38.205 85.333 85.333v682.667c0 47.13-38.204 85.333-85.333 85.333h-213.333c-47.13 0-85.333-38.204-85.333-85.333v-682.667z"],"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["pinned"],"grid":0},"attrs":[{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":804,"id":116,"name":"pinned","prevSize":32,"code":59726},"setIdx":0,"setId":1,"iconIdx":117},{"icon":{"paths":["M1024 512c0 282.77-229.23 512-512 512s-512-229.23-512-512c0-282.77 229.23-512 512-512s512 229.23 512 512z","M1024 512c0 282.77-229.23 512-512 512s-512-229.23-512-512c0-282.77 229.23-512 512-512s512 229.23 512 512z","M237.52 772.135v-37.445c0-68.645 55.647-124.293 124.292-124.293h290.016c68.645 0 124.292 55.648 124.292 124.293v26.929c-68.901 72.72-166.393 118.078-274.479 118.078-102.777 0-195.974-41.012-264.121-107.563z","M672.547 392.888c0 91.526-74.197 165.723-165.723 165.723s-165.723-74.197-165.723-165.723c0-91.526 74.197-165.723 165.723-165.723s165.723 74.197 165.723 165.723z"],"attrs":[{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"isMulticolor":true,"isMulticolor2":false,"tags":["profile"],"defaultCode":59703,"grid":0},"attrs":[{"fill":"rgb(0, 0, 0)"},{"fill":"rgb(9, 157, 253)"},{"fill":"rgb(255, 255, 255)"},{"fill":"rgb(255, 255, 255)"}],"properties":{"order":110,"id":117,"name":"profile","prevSize":32,"codes":[59703,59704,59705,59706],"code":59703},"setIdx":0,"setId":1,"iconIdx":118},{"icon":{"paths":["M282.622 405.672c27.028 27.997 60.82 41.995 101.378 41.995s74.35-13.999 101.378-41.995c27.062-28.030 40.589-63.052 40.589-105.063s-13.527-77.017-40.589-105.013c-27.028-27.997-60.82-41.995-101.378-41.995s-74.35 13.999-101.378 41.995c-27.060 27.997-40.59 63.001-40.59 105.013s13.53 77.033 40.59 105.063z","M95.538 748.59c12.492 12.938 28.212 19.41 47.16 19.41h394.876c-62.165-46.705-102.374-121.057-102.374-204.8 0-24.1-16.496-46.653-40.588-47.16-3.541-0.077-7.078-0.113-10.612-0.113-41.239 0-82.803 5.074-124.69 15.227-41.921 10.184-82.478 24.719-121.673 43.607-18.267 9.108-32.982 22.231-44.143 39.373-11.129 17.178-16.694 36.265-16.694 57.272v29.394c0 18.888 6.246 34.816 18.738 47.79z","M717.251 631.368c-25.62 0-47.508-8.55-65.664-25.651-18.186-17.126-27.279-37.755-27.279-61.885v-201.139c0-24.131 9.093-44.612 27.279-61.444 18.156-16.832 40.044-25.248 65.664-25.248s47.365 8.416 65.234 25.248c17.874 16.832 26.808 37.314 26.808 61.444v201.139c0 24.131-8.934 44.759-26.808 61.885-17.869 17.101-39.613 25.651-65.234 25.651zM717.251 870.4c-7.747 0-14.454-2.668-20.116-7.997-5.663-5.335-8.494-11.648-8.494-18.949v-80.794c-53.612-5.601-99.031-26.081-136.259-61.445-37.228-35.333-59.118-77.696-65.666-127.089-1.201-8.402 1.029-15.549 6.691-21.448s13.253-8.847 22.777-8.847c6.543 0 12.365 2.115 17.454 6.339 5.059 4.204 8.177 9.953 9.349 17.254 5.975 39.828 25.334 73.492 58.076 100.992 32.768 27.469 71.496 41.206 116.188 41.206 44.063 0 82.478-13.737 115.246-41.206 32.768-27.5 52.127-61.164 58.071-100.992 1.203-7.301 4.193-13.051 8.965-17.254 4.777-4.224 10.737-6.339 17.884-6.339 9.523 0 17.116 2.949 22.779 8.847 5.658 5.898 7.89 13.046 6.687 21.448-5.944 49.393-27.535 91.756-64.763 127.089-37.258 35.364-82.678 55.844-136.264 61.445v80.794c0 7.301-2.826 13.614-8.489 18.949-5.663 5.33-12.37 7.997-20.116 7.997z"],"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["promote-filled"],"grid":0},"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"properties":{"order":805,"id":118,"name":"promote-filled","prevSize":32,"code":59727},"setIdx":0,"setId":1,"iconIdx":119},{"icon":{"paths":["M287.952 405.117c27.727 28.366 62.395 42.55 104.003 42.55s76.276-14.183 104.003-42.55c27.761-28.333 41.641-63.17 41.641-104.509 0-42.012-13.88-77.017-41.641-105.013-27.727-27.997-62.395-41.995-104.003-41.995s-76.276 13.999-104.003 41.995c-27.761 27.997-41.641 63.001-41.641 105.013 0 41.339 13.88 76.176 41.641 104.509zM448.626 357.879c-14.879 15.057-33.769 22.585-56.67 22.585s-41.791-7.528-56.67-22.585c-14.912-15.057-22.368-34.147-22.368-57.271 0-23.090 7.456-42.163 22.368-57.221 14.879-15.057 33.769-22.585 56.67-22.585s41.791 7.528 56.67 22.585c14.912 15.057 22.368 34.13 22.368 57.221 0 23.123-7.456 42.213-22.368 57.271z","M515.067 768c-15.893-12.211-30.127-26.511-42.299-42.496-11.118-14.597-27.707-24.704-46.058-24.704h-283.354v-29.394c0-8.402 2.247-15.58 6.74-21.524 4.527-5.985 10.602-10.726 18.225-14.218 34.651-15.396 71.233-27.996 109.745-37.811 38.479-9.779 76.442-14.669 113.889-14.669 1.087 0 2.174 0 3.262 0.010 16.866 0.123 31.134-13.455 33.638-30.136 2.784-18.55-9.628-36.762-28.382-37.059-2.842-0.046-5.682-0.072-8.519-0.072-42.307 0-84.947 5.074-127.92 15.227-43.006 10.184-84.963 24.719-125.873 43.607-18.041 9.108-32.787 22.231-44.237 39.373-11.417 17.178-17.126 36.265-17.126 57.272v29.394c0 18.212 6.574 33.976 19.722 47.288 13.181 13.276 29.142 19.912 47.882 19.912h370.662z","M717.251 631.368c-25.62 0-47.508-8.55-65.664-25.651-18.186-17.126-27.279-37.755-27.279-61.885v-201.139c0-24.131 9.093-44.612 27.279-61.444 18.156-16.832 40.044-25.248 65.664-25.248s47.365 8.416 65.234 25.248c17.874 16.832 26.808 37.314 26.808 61.444v201.139c0 24.131-8.934 44.759-26.808 61.885-17.869 17.101-39.613 25.651-65.234 25.651zM717.251 870.4c-7.747 0-14.454-2.668-20.116-7.997-5.663-5.335-8.494-11.648-8.494-18.949v-81.638c-53.612-5.606-99.031-25.938-136.259-61-37.228-35.067-59.118-77.297-65.666-126.689-1.201-8.402 1.029-15.549 6.691-21.448s13.253-8.847 22.777-8.847c6.543 0 12.365 2.115 17.454 6.339 5.059 4.204 8.177 9.953 9.349 17.254 5.975 39.828 25.334 73.492 58.076 100.992 32.768 27.469 71.496 41.206 116.188 41.206 44.063 0 82.478-13.737 115.246-41.206 32.768-27.5 52.127-61.164 58.071-100.992 1.203-7.301 4.193-13.051 8.965-17.254 4.777-4.224 10.737-6.339 17.884-6.339 9.523 0 17.116 2.949 22.779 8.847 5.658 5.898 7.89 13.046 6.687 21.448-5.944 49.393-27.535 91.622-64.763 126.689-37.258 35.062-82.678 55.393-136.264 61v81.638c0 7.301-2.826 13.614-8.489 18.949-5.663 5.33-12.37 7.997-20.116 7.997zM717.251 577.521c10.122 0 18.458-3.231 25.006-9.697 6.548-6.461 9.82-14.459 9.82-23.992v-201.139c0-9.534-3.272-17.384-9.82-23.551s-14.884-9.251-25.006-9.251c-10.122 0-18.616 3.084-25.477 9.251-6.835 6.167-10.25 14.018-10.25 23.551v201.139c0 9.533 3.415 17.531 10.25 23.992 6.861 6.467 15.355 9.697 25.477 9.697z","M716.8 307.2c28.277 0 51.2 22.923 51.2 51.2v204.8c0 28.277-22.923 51.2-51.2 51.2s-51.2-22.923-51.2-51.2v-204.8c0-28.277 22.923-51.2 51.2-51.2z"],"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["promote-outlined"],"grid":0},"attrs":[{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"},{"fill":"rgb(250, 250, 250)"}],"properties":{"order":806,"id":119,"name":"promote-outlined","prevSize":32,"code":59728},"setIdx":0,"setId":1,"iconIdx":120},{"icon":{"paths":["M512.491 938.667c43.157 0 82.889-7.561 119.194-22.682 36.275-15.117 67.665-35.443 94.161-60.971 26.466-25.502 47.223-55.262 62.263-89.284 15.040-34.018 22.558-69.926 22.558-107.729v-219.238c0-12.6-4.407-23.159-13.222-31.676-8.841-8.492-19.797-12.739-32.879-12.739-13.077 0-24.034 4.246-32.875 12.739-8.815 8.518-13.222 19.076-13.222 31.676v170.099h-11.772c-39.232 4.412-70.293 18.436-93.18 42.074-22.886 23.612-35.635 51.166-38.251 82.667h-35.311c3.268-39.061 17.655-72.448 43.157-100.169s59.174-45.675 101.026-53.867v-370.439c0-12.6-4.407-23.159-13.222-31.676-8.841-8.492-19.802-12.739-32.879-12.739s-23.868 4.246-32.367 12.739c-8.499 8.518-12.749 19.076-12.749 31.676v274.993h-35.311v-355.318c0-12.6-4.42-22.995-13.261-31.185-8.815-8.19-19.435-12.285-31.859-12.285-13.077 0-24.026 4.095-32.841 12.285-8.841 8.19-13.261 18.585-13.261 31.185v355.318h-34.33v-307.124c0-11.97-4.407-22.365-13.22-31.185-8.841-8.82-19.8-13.23-32.878-13.23s-24.024 4.41-32.839 13.23c-8.841 8.82-13.261 19.215-13.261 31.185v330.748h-35.311v-220.183c0-12.6-4.407-23.146-13.222-31.639-8.841-8.518-19.473-12.777-31.897-12.777-13.078 0-24.024 4.259-32.839 12.777-8.841 8.492-13.261 19.039-13.261 31.639v370.438c0 37.803 7.52 73.711 22.56 107.729 15.040 34.022 35.964 63.782 62.774 89.284 26.81 25.527 58.367 45.854 94.671 60.971 36.277 15.121 75.996 22.682 119.153 22.682z"],"attrs":[{"fill":"rgb(9, 157, 253)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["raise-hand"],"defaultCode":59687,"grid":0},"attrs":[{"fill":"rgb(9, 157, 253)"}],"properties":{"order":113,"id":120,"name":"raise-hand","prevSize":32,"code":59687},"setIdx":0,"setId":1,"iconIdx":121},{"icon":{"paths":["M917.333 512c0-212.077-171.921-384-384-384-212.077 0-384 171.923-384 384 0 212.079 171.923 384 384 384 212.079 0 384-171.921 384-384zM960 512c0 235.639-191.027 426.667-426.667 426.667-235.642 0-426.667-191.027-426.667-426.667 0-235.642 191.025-426.667 426.667-426.667 235.639 0 426.667 191.025 426.667 426.667zM533.333 768c-141.385 0-256-114.615-256-256s114.615-256 256-256c141.385 0 256 114.615 256 256s-114.615 256-256 256z"],"width":1067,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["recording"],"defaultCode":59688,"grid":0},"attrs":[{}],"properties":{"order":114,"id":121,"name":"recording","prevSize":32,"code":59688},"setIdx":0,"setId":1,"iconIdx":122},{"icon":{"paths":["M361.624 701.85l150.427-150.374 150.426 150.374 39.424-39.424-150.374-150.426 150.374-150.426-39.424-39.424-150.426 150.374-150.427-150.374-39.424 39.424 150.374 150.426-150.374 150.426 39.424 39.424zM512.051 900.25c-53.318 0-103.63-10.122-150.939-30.362-47.274-20.275-88.525-48.010-123.75-83.2-35.191-35.226-62.925-76.477-83.2-123.75-20.241-47.309-30.362-97.623-30.362-150.938 0-54.033 10.121-104.516 30.362-151.45 20.275-46.933 48.009-88.013 83.2-123.238 35.226-35.191 76.476-62.925 123.75-83.2 47.309-20.241 97.622-30.362 150.939-30.362 54.031 0 104.515 10.121 151.45 30.362 46.93 20.275 88.013 48.009 123.238 83.2 35.19 35.226 62.925 76.305 83.2 123.238 20.239 46.934 30.362 97.417 30.362 151.45 0 53.315-10.122 103.629-30.362 150.938-20.275 47.273-48.010 88.525-83.2 123.75-35.226 35.19-76.308 62.925-123.238 83.2-46.935 20.239-97.418 30.362-151.45 30.362z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["remove"],"defaultCode":59689,"grid":0},"attrs":[{}],"properties":{"order":115,"id":122,"name":"remove1","prevSize":32,"code":59689},"setIdx":0,"setId":1,"iconIdx":123},{"icon":{"paths":["M512.051 900.25c-53.318 0-103.63-10.122-150.939-30.362-47.274-20.275-88.525-48.010-123.75-83.2-35.191-35.226-62.925-76.477-83.2-123.75-20.241-47.309-30.362-97.623-30.362-150.938 0-54.033 10.121-104.516 30.362-151.45 20.275-46.933 48.009-88.013 83.2-123.238 35.226-35.191 76.476-62.925 123.75-83.2 47.309-20.241 97.622-30.362 150.939-30.362 54.031 0 104.515 10.121 151.45 30.362 46.93 20.275 88.013 48.009 123.238 83.2 35.19 35.226 62.925 76.305 83.2 123.238 20.239 46.934 30.362 97.417 30.362 151.45 0 53.315-10.122 103.629-30.362 150.938-20.275 47.273-48.010 88.525-83.2 123.75-35.226 35.19-76.308 62.925-123.238 83.2-46.935 20.239-97.418 30.362-151.45 30.362zM512.051 844.8c92.431 0 171.008-32.358 235.725-97.075s97.075-143.293 97.075-235.725c0-40.55-6.932-79.121-20.787-115.712-13.86-36.625-33.249-69.53-58.163-98.714l-468.277 468.276c29.184 24.919 62.089 44.303 98.714 58.163 36.591 13.86 75.162 20.787 115.714 20.787zM258.2 726.426l468.277-468.276c-29.184-24.917-62.090-44.305-98.714-58.163-36.593-13.858-75.162-20.787-115.712-20.787-92.434 0-171.010 32.358-235.726 97.075s-97.075 143.292-97.075 235.725c0 40.551 6.929 79.12 20.787 115.713 13.858 36.623 33.246 69.53 58.163 98.714z"],"attrs":[{"fill":"rgb(231, 65, 76)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["remove-meeting"],"grid":0},"attrs":[{"fill":"rgb(231, 65, 76)"}],"properties":{"order":116,"id":123,"name":"remove-meeting","prevSize":32,"code":59715},"setIdx":0,"setId":1,"iconIdx":124},{"icon":{"paths":["M490.667 682.667h85.333v-177.067l68.267 67.2 60.8-60.8-171.733-170.667-170.667 170.667 60.8 59.733 67.2-67.2v178.133zM192 853.333c-23.467 0-43.549-8.35-60.245-25.045-16.725-16.725-25.088-36.821-25.088-60.288v-512c0-23.467 8.363-43.549 25.088-60.245 16.697-16.725 36.779-25.088 60.245-25.088h682.667c23.467 0 43.563 8.363 60.288 25.088 16.695 16.697 25.045 36.779 25.045 60.245v512c0 23.467-8.35 43.563-25.045 60.288-16.725 16.695-36.821 25.045-60.288 25.045h-682.667z"],"width":1067,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["screen-share"],"defaultCode":59690,"grid":0},"attrs":[{}],"properties":{"order":117,"id":124,"name":"screen-share","prevSize":32,"code":59690},"setIdx":0,"setId":1,"iconIdx":125},{"icon":{"paths":["M190.073 849.715c-14.605 5.858-28.594 4.604-41.966-3.767-13.405-8.367-20.107-20.924-20.107-37.662v-177.020c0-10.044 3.051-19.247 9.153-27.618 6.069-8.371 14.378-13.811 24.926-16.32l303.060-75.328-303.060-75.328c-10.548-2.509-18.857-7.731-24.926-15.667-6.102-7.968-9.153-17.392-9.153-28.273v-177.018c0-16.739 6.702-29.294 20.107-37.663 13.372-8.37 27.361-9.625 41.966-3.767l679.15 295.031c17.852 8.367 26.778 22.596 26.778 42.684s-8.926 34.317-26.778 42.684l-679.15 295.031z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["send"],"defaultCode":59691,"grid":0},"attrs":[{}],"properties":{"order":118,"id":125,"name":"send","prevSize":32,"code":59691},"setIdx":0,"setId":1,"iconIdx":126},{"icon":{"paths":["M599.983 938.667h-134.409c-9.626 0-18.142-3.366-25.549-10.103s-11.849-15.347-13.329-25.826l-13.33-101.052c-11.849-3.742-23.875-9.357-36.079-16.845-12.234-7.484-23.535-15.343-33.902-23.578l-92.197 40.422c-9.627 3.742-19.061 4.117-28.303 1.122-9.271-2.995-16.87-8.981-22.794-17.967l-66.648-117.892c-5.184-8.981-6.665-18.534-4.443-28.655 2.222-10.091 7.035-18.129 14.441-24.119l81.089-61.751c-1.481-6.737-2.399-13.474-2.755-20.211-0.385-6.737-0.577-13.474-0.577-20.211 0-5.99 0.192-12.352 0.577-19.089 0.356-6.737 1.274-13.845 2.755-21.333l-81.089-61.753c-7.405-5.988-12.219-14.028-14.441-24.118-2.222-10.12-0.741-19.671 4.443-28.654l66.648-116.772c5.184-8.983 12.589-14.971 22.216-17.965s18.884-2.62 27.77 1.123l93.308 39.298c10.368-8.234 21.668-15.899 33.902-22.995 12.204-7.126 24.23-12.935 36.079-17.426l13.33-101.053c1.481-10.48 5.922-19.088 13.329-25.825s15.923-10.105 25.549-10.105h134.409c10.368 0 19.251 3.369 26.658 10.105s11.849 15.345 13.329 25.825l13.329 101.053c13.329 5.239 25.357 11.033 36.079 17.381 10.752 6.378 21.683 14.058 32.794 23.040l94.417-39.298c8.887-3.743 17.967-4.117 27.238-1.123 9.242 2.994 16.823 8.982 22.75 17.965l66.645 116.772c5.184 8.983 6.665 18.534 4.446 28.654-2.223 10.090-7.036 18.129-14.443 24.118l-82.197 62.875c1.481 7.488 2.219 14.225 2.219 20.211v19.089c0 5.99-0.192 12.156-0.576 18.505-0.354 6.379-1.276 13.683-2.756 21.918l81.092 61.751c8.145 5.99 13.329 14.029 15.548 24.119 2.223 10.121 0.371 19.674-5.551 28.655l-66.65 116.77c-5.184 8.981-12.591 14.972-22.217 17.967-9.626 2.991-19.255 2.62-28.881-1.122l-91.085-39.3c-11.11 8.981-22.396 16.841-33.86 23.578-11.49 6.737-23.164 12.352-35.012 16.845l-13.329 101.052c-1.481 10.479-5.922 19.089-13.329 25.826s-16.29 10.103-26.658 10.103zM533.333 646.737c37.026 0 68.501-13.099 94.417-39.3 25.92-26.197 38.878-58.010 38.878-95.437s-12.958-69.239-38.878-95.439c-25.916-26.199-57.391-39.298-94.417-39.298s-68.501 13.099-94.417 39.298c-25.92 26.199-38.88 58.011-38.88 95.439s12.959 69.239 38.88 95.437c25.916 26.202 57.391 39.3 94.417 39.3z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["settings"],"defaultCode":59692,"grid":0},"attrs":[{}],"properties":{"order":802,"id":126,"name":"settings","prevSize":32,"code":59692},"setIdx":0,"setId":1,"iconIdx":127},{"icon":{"paths":["M775.313 938.667c-33.643 0-62.17-11.669-85.577-35.008s-35.106-51.785-35.106-85.333c0-5.107 0.363-10.765 1.097-16.981 0.73-6.182 2.193-11.465 4.386-15.838l-324.753-190.362c-11.703 11.669-24.869 20.787-39.497 27.349-14.629 6.566-30.354 9.847-47.177 9.847-33.646 0-62.171-11.669-85.577-35.008s-35.109-51.785-35.109-85.333c0-33.549 11.703-61.995 35.109-85.333s51.931-35.008 85.577-35.008c16.823 0 32.549 3.282 47.177 9.846s27.794 15.681 39.497 27.351l324.753-190.36c-2.193-4.376-3.657-9.671-4.386-15.885-0.734-6.185-1.097-11.83-1.097-16.936 0-33.55 11.699-61.994 35.106-85.333s51.934-35.008 85.577-35.008c33.647 0 62.174 11.669 85.577 35.008 23.407 23.339 35.11 51.784 35.11 85.333s-11.703 61.994-35.11 85.333c-23.403 23.339-51.93 35.009-85.577 35.009-16.823 0-32.546-3.282-47.177-9.846-14.626-6.564-27.793-15.681-39.497-27.351l-324.754 190.36c2.194 4.373 3.657 9.481 4.389 15.313 0.731 5.837 1.097 11.669 1.097 17.506 0 5.107-0.366 10.752-1.097 16.934-0.731 6.217-2.194 11.511-4.389 15.885l324.754 190.362c11.703-11.669 24.87-20.787 39.497-27.354 14.63-6.562 30.353-9.843 47.177-9.843 33.647 0 62.174 11.669 85.577 35.008 23.407 23.339 35.11 51.785 35.11 85.333s-11.703 61.995-35.11 85.333c-23.403 23.339-51.93 35.008-85.577 35.008z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["share"],"defaultCode":59693,"grid":0},"attrs":[{}],"properties":{"order":120,"id":127,"name":"share","prevSize":32,"code":59693},"setIdx":0,"setId":1,"iconIdx":128},{"icon":{"paths":["M599.532 857.62v-66.135c61.158-19.2 110.582-54.584 148.27-106.153 37.688-51.543 56.53-109.665 56.53-174.378 0-64.711-18.842-122.851-56.53-174.421-37.688-51.542-87.112-86.912-148.27-106.112v-66.134c78.935 21.334 143.462 64 193.582 128 50.145 64 75.218 136.889 75.218 218.666s-25.073 154.666-75.218 218.666c-50.12 64-114.647 106.665-193.582 128zM155.8 618.685v-213.331h158.934l182.4-183.466v580.268l-182.4-183.47h-158.934zM599.532 667.756v-313.602c28.447 15.645 50.488 37.689 66.135 66.134s23.465 59.022 23.465 91.733c0 32.712-7.818 62.935-23.465 90.665-15.647 27.735-37.688 49.423-66.135 65.070z"],"attrs":[{"fill":"rgb(128, 128, 128)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["speaker"],"grid":0},"attrs":[{"fill":"rgb(128, 128, 128)"}],"properties":{"order":121,"id":128,"name":"speaker","prevSize":32,"code":59685},"setIdx":0,"setId":1,"iconIdx":129},{"icon":{"paths":["M937.685 502.549c0 90.628-28.373 174.511-76.565 242.931l-29.623-29.628c41.126-60.582 65.216-134.076 65.216-213.303 0-207.401-165.090-375.532-368.742-375.532-80.205 0-154.432 26.079-214.947 70.365l-29.627-29.627c68.26-51.802 152.9-82.464 244.574-82.464 226.278 0 409.715 186.813 409.715 417.258z","M754.79 850.086c-64.947 44.049-142.938 69.717-226.82 69.717-226.279 0-409.715-186.812-409.715-417.254 0-82.7 23.624-159.78 64.374-224.623l-115.962-115.963 45.255-45.255 784.416 784.416-45.252 45.257-96.294-96.294zM724.838 820.139l-91.345-91.349c-31.945 15.462-67.686 24.115-105.408 24.115-135.769 0-245.83-112.085-245.83-250.351 0-36.796 7.795-71.74 21.794-103.208l-91.47-91.47c-33.857 56.768-53.353 123.404-53.353 194.674 0 207.398 165.092 375.531 368.744 375.531 72.388 0 139.908-21.244 196.868-57.941z","M773.914 502.554c0 44.983-11.648 87.194-32.043 123.678l-339.105-339.106c36.697-22.188 79.547-34.927 125.32-34.927 135.765 0 245.828 112.087 245.828 250.356z"],"attrs":[{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["stop-recording"],"grid":0},"attrs":[{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"},{"fill":"rgb(255, 65, 77)"}],"properties":{"order":122,"id":129,"name":"stop-recording","prevSize":32,"code":59694},"setIdx":0,"setId":1,"iconIdx":130},{"icon":{"paths":["M359.467 664.533c6.4 6.4 13.867 9.6 22.4 9.6s16-3.2 22.4-9.6l107.733-107.733 108.8 108.8c5.687 5.687 12.8 8.533 21.333 8.533s16-3.2 22.4-9.6c6.4-6.4 9.6-13.867 9.6-22.4s-3.2-16-9.6-22.4l-107.733-107.733 108.8-108.8c5.687-5.689 8.533-12.8 8.533-21.333s-3.2-16-9.6-22.4c-6.4-6.4-13.867-9.6-22.4-9.6s-16 3.2-22.4 9.6l-107.733 107.733-108.8-108.8c-5.689-5.689-12.8-8.533-21.333-8.533s-16 3.2-22.4 9.6c-6.4 6.4-9.6 13.867-9.6 22.4s3.2 16 9.6 22.4l107.733 107.733-108.8 108.8c-5.689 5.687-8.533 12.8-8.533 21.333s3.2 16 9.6 22.4zM149.333 853.333c-17.067 0-32-6.4-44.8-19.2s-19.2-27.733-19.2-44.8v-554.667c0-17.067 6.4-32 19.2-44.8s27.733-19.2 44.8-19.2h725.333c17.779 0 32.887 6.4 45.333 19.2s18.667 27.733 18.667 44.8v554.667c0 17.067-6.221 32-18.667 44.8s-27.554 19.2-45.333 19.2h-725.333z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["stop-screen-share"],"defaultCode":59695,"grid":0},"attrs":[{}],"properties":{"order":123,"id":130,"name":"stop-screen-share","prevSize":32,"code":59695},"setIdx":0,"setId":1,"iconIdx":131},{"icon":{"paths":["M257.123 586.543l112.28 112.939c5.989 6.775 13.669 10.167 23.040 10.167 9.341 0 17.381-3.392 24.118-10.167s10.105-14.878 10.105-24.303c0-9.399-3.369-17.109-10.105-23.13l-58.386-58.731h307.651l-58.389 58.731c-6.737 6.020-10.103 13.73-10.103 23.13 0 9.425 3.366 17.527 10.103 24.303s14.793 10.167 24.166 10.167c9.34 0 17.007-3.392 22.993-10.167l112.282-112.939c7.484-7.531 11.226-16.943 11.226-28.237s-3.742-21.082-11.226-29.363l-112.282-111.814c-5.986-6.776-13.653-10.164-22.993-10.164-9.374 0-17.429 3.388-24.166 10.164s-10.103 14.683-10.103 23.72c0 9.033 2.995 16.939 8.981 23.714l60.634 60.988h-309.896l60.631-60.988c5.988-6.775 8.983-14.682 8.983-23.714 0-9.037-3.369-16.943-10.105-23.72s-14.776-10.164-24.118-10.164c-9.372 0-17.052 3.388-23.040 10.164l-112.28 111.814c-7.485 8.282-11.228 18.069-11.228 29.363s3.743 20.706 11.228 28.237zM166.176 896c-22.456 0-41.544-7.906-57.263-23.718s-23.579-35.012-23.579-57.6v-515.011c0-22.588 7.86-41.788 23.579-57.6s34.807-23.718 57.263-23.718h136.982l59.509-64.377c7.485-8.282 16.468-14.682 26.947-19.2s21.333-6.776 32.561-6.776h179.651c11.226 0 22.080 2.259 32.559 6.776s19.465 10.918 26.948 19.2l59.507 64.377h136.986c22.455 0 41.54 7.906 57.263 23.718 15.718 15.812 23.578 35.012 23.578 57.6v515.011c0 22.588-7.859 41.788-23.578 57.6-15.723 15.812-34.807 23.718-57.263 23.718h-691.651z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["switch-camera"],"defaultCode":59696,"grid":0},"attrs":[{}],"properties":{"order":124,"id":131,"name":"switch-camera","prevSize":32,"code":59696},"setIdx":0,"setId":1,"iconIdx":132},{"icon":{"paths":["M1341.235 162.025c35.499-40.678 31.289-102.43-9.387-137.925s-102.434-31.294-137.921 9.384l-620.733 711.364-414.268-332.33c-42.111-33.782-103.637-27.030-137.419 15.082-33.783 42.111-27.030 103.637 15.082 137.419l487.421 391.014c19.833 15.91 43.969 22.83 67.487 21.299 25.074-1.621 49.522-12.851 67.347-33.279l682.391-782.029z"],"width":1365,"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["tick"],"defaultCode":59698,"grid":0},"attrs":[{}],"properties":{"order":125,"id":132,"name":"tick","prevSize":32,"code":59698},"setIdx":0,"setId":1,"iconIdx":133},{"icon":{"paths":["M433.152 686.080l275.456-275.456-43.008-43.008-232.448 232.448-116.736-116.736-43.008 43.008 159.744 159.744zM491.52 901.12c-53.932 0-104.612-10.24-152.044-30.72-47.459-20.48-88.596-48.128-123.412-82.944s-62.464-75.952-82.944-123.412c-20.48-47.432-30.72-98.111-30.72-152.044s10.24-104.625 30.72-152.084c20.48-47.432 48.128-88.556 82.944-123.372s75.953-62.464 123.412-82.944c47.432-20.48 98.111-30.72 152.044-30.72s104.624 10.24 152.084 30.72c47.432 20.48 88.556 48.128 123.372 82.944s62.464 75.94 82.944 123.372c20.48 47.459 30.72 98.152 30.72 152.084s-10.24 104.612-30.72 152.044c-20.48 47.46-48.128 88.596-82.944 123.412s-75.94 62.464-123.372 82.944c-47.46 20.48-98.152 30.72-152.084 30.72z"],"width":983,"attrs":[{"fill":"rgb(54, 179, 126)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["tick-fill"],"defaultCode":59697,"grid":0},"attrs":[{"fill":"rgb(54, 179, 126)"}],"properties":{"order":126,"id":133,"name":"tick-fill","prevSize":32,"code":59697},"setIdx":0,"setId":1,"iconIdx":134},{"icon":{"paths":["M259.713 446.842l105.819 105.821-141.124 141.164c-9.997 9.999-9.998 26.209 0 36.209 9.997 9.994 26.206 9.994 36.204 0l470.652-470.652c9.994-9.998 9.994-26.207 0-36.204-9.999-9.997-26.209-9.997-36.204 0l-142.5 142.453-105.819-105.819 36.204-36.204-39.208-39.209-259.437 259.437 39.209 39.209 36.204-36.204z","M823.834 504.059l-39.209-39.209-127.473 0.035-192.317 192.318-0.035 127.473 39.253 39.301 140.289-140.288 156.908 156.908 39.209-0.036 0.036-39.209-156.882-156.933 140.221-140.36z"],"attrs":[{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["unpin-filled"],"grid":0},"attrs":[{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"}],"properties":{"order":127,"id":134,"name":"unpin-filled","prevSize":32,"code":59729},"setIdx":0,"setId":1,"iconIdx":135},{"icon":{"paths":["M259.713 446.842l105.819 105.821 39.208-39.209-105.819-105.821 108.612-108.612 105.819 105.819 39.209-39.209-105.819-105.819 36.204-36.204-39.208-39.209-259.437 259.437 39.209 39.209 36.204-36.204z","M632.023 520.771l55.798-55.8 96.85-0.027 39.209 39.209-140.293 140.292 156.908 156.908-0.036 39.209-39.209 0.036-156.908-156.908-140.289 140.288-39.209-39.209 0.027-96.845 55.798-55.798v96.819l208.174-208.174h-96.819z","M224.408 693.827c-9.998 9.999-9.998 26.209 0 36.209 9.997 9.994 26.206 9.994 36.204 0l470.652-470.652c9.994-9.998 9.994-26.207 0-36.204-9.999-9.997-26.209-9.997-36.204 0l-470.652 470.648z"],"attrs":[{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["unpin-outlined"],"grid":0},"attrs":[{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"},{"fill":"rgb(217, 217, 217)"}],"properties":{"order":128,"id":135,"name":"unpin-outlined","prevSize":32,"code":59730},"setIdx":0,"setId":1,"iconIdx":136},{"icon":{"paths":["M908.817 709.333l-150.4-149.333v80l-64-64v-307.198c0-3.555-1.237-6.571-3.712-9.045-2.505-2.503-5.888-3.755-10.155-3.755h-307.2l-64-64h371.2c22.046 0 40.533 7.467 55.467 22.4s22.4 33.067 22.4 54.4v195.198l150.4-149.331v394.665zM852.284 945.067l-787.2-786.131 45.867-44.8 786.134 786.131-44.8 44.8zM189.883 193.069l62.933 62.933h-57.6c-3.555 0-6.571 1.251-9.045 3.755-2.503 2.475-3.755 5.49-3.755 9.045v486.398c0 3.558 1.251 6.588 3.755 9.088 2.475 2.475 5.49 3.712 9.045 3.712h485.334c4.267 0 7.65-1.237 10.155-3.712 2.475-2.5 3.712-5.53 3.712-9.088v-57.6l62.933 62.933c-1.421 19.913-9.417 36.796-23.979 50.645-14.592 13.884-32.201 20.821-52.821 20.821h-485.334c-21.333 0-39.467-7.467-54.4-22.4s-22.4-33.067-22.4-54.4v-486.398c0-19.911 6.941-37.163 20.821-51.755 13.853-14.563 30.734-22.557 50.645-23.979z","M851.2 943.996l-787.2-786.133 45.867-44.8 786.133 786.133-44.8 44.8z"],"attrs":[{},{}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-off"],"defaultCode":59699,"grid":0},"attrs":[{},{}],"properties":{"order":129,"id":136,"name":"video-off","prevSize":32,"code":59699},"setIdx":0,"setId":1,"iconIdx":137},{"icon":{"paths":["M823.475 919.424l-736.001-733.849c-6.383-6.383-9.574-14.2-9.574-23.45s3.55-17.425 10.65-24.525c7.134-7.134 15.138-10.701 24.013-10.701s17.238 3.567 25.088 10.701l733.851 734.924c6.385 6.385 9.574 14.198 9.574 23.45s-3.19 17.065-9.574 23.45c-7.818 7.849-15.99 11.776-24.525 11.776s-16.369-3.927-23.501-11.776zM758.4 631.424l-440.526-439.449h358.401c23.485 0 43.044 7.816 58.675 23.45 15.631 15.667 23.45 34.867 23.45 57.6v185.6l112.026-111.974c7.818-7.817 16.179-9.592 25.088-5.325 8.873 4.266 13.312 11.008 13.312 20.224v300.85c0 9.216-4.439 15.959-13.312 20.224-8.909 4.265-17.27 2.493-25.088-5.325l-112.026-111.974v66.099zM193.1 193.051l564.276 565.298c0 19.215-8.535 36.285-25.6 51.2-17.065 14.95-35.569 22.426-55.501 22.426h-476.775c-22.767 0-41.967-7.818-57.6-23.45-15.667-15.667-23.501-34.867-23.501-57.6v-477.9c0-19.183 7.305-37.308 21.914-54.374 14.575-17.067 32.171-25.6 52.787-25.6z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-off-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":130,"id":137,"name":"video-off-filled","prevSize":32,"code":59731},"setIdx":0,"setId":1,"iconIdx":138},{"icon":{"paths":["M870.426 677.325l-112.026-111.974v66.099l-68.25-68.25v-290.15c0-3.55-1.244-6.57-3.738-9.062s-5.873-3.738-10.138-3.738h-290.1l-68.301-68.25h358.401c22.769 0 42.153 7.817 58.163 23.45 15.974 15.667 23.962 34.867 23.962 57.6v185.6l112.026-111.974c7.101-7.134 15.273-8.909 24.525-5.325 9.252 3.55 13.875 10.291 13.875 20.224v300.851c0 9.933-4.623 16.676-13.875 20.224-9.252 3.584-17.423 1.807-24.525-5.325zM823.475 919.45l-736.001-733.85c-6.383-6.383-9.574-14.029-9.574-22.938 0-8.874 3.909-16.879 11.725-24.013 6.417-7.1 14.234-10.65 23.45-10.65 9.25 0 17.442 3.55 24.576 10.65l733.851 733.901c6.385 6.385 9.764 14.029 10.138 22.938 0.343 8.873-3.036 16.86-10.138 23.962-7.132 7.132-15.309 10.701-24.525 10.701-9.252 0-17.085-3.569-23.501-10.701zM192.025 193.075l67.174 67.174h-60.774c-3.55 0-6.57 1.246-9.062 3.738s-3.738 5.513-3.738 9.062l1.075 477.901c0 3.548 1.246 6.569 3.738 9.062s5.513 3.738 9.062 3.738h476.775c4.265 0 7.644-1.244 10.138-3.738s3.738-5.514 3.738-9.062v-59.75l67.226 67.174c-2.15 20.649-10.87 38.077-26.163 52.275-15.293 14.234-33.603 21.35-54.938 21.35h-476.775c-22.767 0-41.967-7.818-57.6-23.45-15.667-15.667-23.501-34.867-23.501-57.6v-477.901c0-21.334 7.117-39.646 21.35-54.938s31.659-23.638 52.275-25.037z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-off-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":131,"id":138,"name":"video-off-outlined","prevSize":32,"code":59732},"setIdx":0,"setId":1,"iconIdx":139},{"icon":{"paths":["M194.133 832c-21.333 0-39.467-7.467-54.4-22.4s-22.4-33.067-22.4-54.4v-486.4c0-21.333 7.467-39.467 22.4-54.4s33.067-22.4 54.4-22.4h485.333c22.046 0 40.533 7.467 55.467 22.4s22.4 33.067 22.4 54.4v195.2l149.333-149.333v394.667l-149.333-149.333v195.2c0 21.333-7.467 39.467-22.4 54.4s-33.421 22.4-55.467 22.4h-485.333z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-on"],"defaultCode":59700,"grid":0},"attrs":[{}],"properties":{"order":132,"id":139,"name":"video-on","prevSize":32,"code":59700},"setIdx":0,"setId":1,"iconIdx":140},{"icon":{"paths":["M198.45 833.101c-22.767 0-41.967-7.834-57.6-23.501-15.633-15.631-23.45-34.816-23.45-57.549v-477.902c0-22.733 7.817-41.933 23.45-57.6 15.633-15.633 34.833-23.45 57.6-23.45h476.827c23.45 0 42.988 7.817 58.624 23.45 15.667 15.667 23.501 34.867 23.501 57.6v184.525l111.974-111.974c7.813-7.816 15.99-9.591 24.525-5.325 8.53 4.267 12.8 11.008 12.8 20.224v300.852c0 9.216-4.27 15.959-12.8 20.224-8.535 4.265-16.712 2.493-24.525-5.325l-111.974-111.974v186.675c0 22.733-7.834 41.917-23.501 57.549-15.636 15.667-35.174 23.501-58.624 23.501h-476.827z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-on-filled"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":133,"id":140,"name":"video-on-filled","prevSize":32,"code":59733},"setIdx":0,"setId":1,"iconIdx":141},{"icon":{"paths":["M198.45 832c-22.767 0-41.967-7.818-57.6-23.45-15.633-15.667-23.45-34.867-23.45-57.6v-477.901c0-22.733 7.817-41.933 23.45-57.6 15.633-15.633 34.833-23.45 57.6-23.45h476.827c22.733 0 42.102 7.817 58.112 23.45 16.005 15.667 24.013 34.867 24.013 57.6v185.6l111.974-111.974c7.132-7.134 15.135-8.909 24.013-5.325 8.873 3.55 13.312 10.291 13.312 20.224v300.851c0 9.933-4.439 16.676-13.312 20.224-8.878 3.584-16.881 1.807-24.013-5.325l-111.974-111.974v185.6c0 22.733-8.008 41.933-24.013 57.6-16.010 15.631-35.379 23.45-58.112 23.45h-476.827zM198.45 763.75h476.827c4.229 0 7.593-1.244 10.086-3.738 2.488-2.493 3.738-5.514 3.738-9.062v-477.901c0-3.55-1.249-6.57-3.738-9.062-2.493-2.492-5.857-3.738-10.086-3.738h-476.827c-3.55 0-6.57 1.246-9.062 3.738s-3.738 5.513-3.738 9.062v477.901c0 3.548 1.246 6.569 3.738 9.062s5.513 3.738 9.062 3.738zM185.65 763.75v0z"],"attrs":[{"fill":"rgb(197, 197, 197)"}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-on-outlined"],"grid":0},"attrs":[{"fill":"rgb(197, 197, 197)"}],"properties":{"order":134,"id":141,"name":"video-on-outlined","prevSize":32,"code":59734},"setIdx":0,"setId":1,"iconIdx":142},{"icon":{"paths":["M384 682.667h85.333v-128h128v-85.333h-128v-128h-85.333v128h-128v85.333h128v128zM170.667 853.333c-23.467 0-43.549-8.35-60.245-25.045-16.725-16.725-25.088-36.821-25.088-60.288v-512c0-23.467 8.363-43.549 25.088-60.245 16.697-16.725 36.779-25.088 60.245-25.088h512c23.467 0 43.563 8.363 60.288 25.088 16.695 16.697 25.045 36.779 25.045 60.245v192l170.667-170.667v469.333l-170.667-170.667v192c0 23.467-8.35 43.563-25.045 60.288-16.725 16.695-36.821 25.045-60.288 25.045h-512z"],"attrs":[{}],"isMulticolor":false,"isMulticolor2":false,"tags":["video-plus"],"defaultCode":59701,"grid":0},"attrs":[{}],"properties":{"order":135,"id":142,"name":"video-plus","prevSize":32,"code":59701},"setIdx":0,"setId":1,"iconIdx":143}],"height":1024,"metadata":{"name":"icomoon"},"preferences":{"showGlyphs":true,"showQuickUse":true,"showQuickUse2":true,"showSVGs":true,"fontPref":{"prefix":"icon-","metadata":{"fontFamily":"icomoon"},"metrics":{"emSize":1024,"baseline":6.25,"whitespace":50},"embed":false},"imagePref":{"prefix":"icon-","png":true,"useClassSelector":true,"color":0,"bgColor":16777215,"classSelector":".icon","name":"icomoon"},"historySize":50,"showCodes":true,"gridSize":16}} \ No newline at end of file diff --git a/template/src/atoms/CustomIcon.tsx b/template/src/atoms/CustomIcon.tsx index b8e126f82..bb7f16bf4 100644 --- a/template/src/atoms/CustomIcon.tsx +++ b/template/src/atoms/CustomIcon.tsx @@ -157,6 +157,7 @@ export interface IconsInterface { 'double-up-arrow': string; 'move-up': string; 'close-room': string; + 'open-room': string; announcement: string; 'settings-outlined': string; 'breakout-room': string; diff --git a/template/src/components/Controls.tsx b/template/src/components/Controls.tsx index 944472896..556dbcc86 100644 --- a/template/src/components/Controls.tsx +++ b/template/src/components/Controls.tsx @@ -683,7 +683,7 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { // 8. Screenshare const {permissions} = useBreakoutRoom(); const canAccessBreakoutRoom = useControlPermissionMatrix('breakoutRoom'); - const canScreenshareInBreakoutRoom = permissions.canScreenshare; + const canScreenshareInBreakoutRoom = permissions?.canScreenshare; const canAccessScreenshare = useControlPermissionMatrix('screenshareControl'); if (canAccessScreenshare) { @@ -851,7 +851,7 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { icon: 'breakout-room', iconColor: $config.SECONDARY_ACTION_COLOR, textColor: $config.FONT_COLOR, - title: breakoutRoomLabel, + title: isHost ? breakoutRoomLabel : 'View Breakout Rooms', onPress: () => { setActionMenuVisible(false); setSidePanel(SidePanelType.BreakoutRoom); @@ -1312,11 +1312,7 @@ const Controls = (props: ControlsProps) => { const canAccessInvite = useControlPermissionMatrix('inviteControl'); const canAccessScreenshare = useControlPermissionMatrix('screenshareControl'); - const canAccessExitBreakoutRoomBtn = permissions.canExitRoom; - console.log( - 'supriya-exit canAccessExitBreakoutRoomBtn: ', - canAccessExitBreakoutRoomBtn, - ); + const canAccessExitBreakoutRoomBtn = permissions?.canExitRoom; const defaultItems: ToolbarPresetProps['items'] = React.useMemo(() => { return { diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 2662b1e0a..0cc7297ad 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -14,6 +14,7 @@ import StorageContext from '../../StorageContext'; import getUniqueID from '../../../utils/getUniqueID'; import {logger} from '../../../logger/AppBuilderLogger'; import {useRoomInfo} from 'customization-api'; +import {useLocation} from '../../Router'; import { BreakoutGroupActionTypes, BreakoutGroup, @@ -59,6 +60,7 @@ interface BreakoutRoomPermissions { // Media controls canScreenshare: boolean; canRaiseHands: boolean; + canSeeRaisedHands: boolean; // Room management (host only) canAssignParticipants: boolean; canCreateRooms: boolean; @@ -88,7 +90,6 @@ interface BreakoutRoomContextValue { getAllRooms: () => BreakoutGroup[]; getRoomMemberDropdownOptions: (memberUid: UidType) => MemberDropdownOption[]; upsertBreakoutRoomAPI: (type: 'START' | 'UPDATE') => Promise; - closeBreakoutRoomAPI: () => void; checkIfBreakoutRoomSessionExistsAPI: () => Promise; handleAssignParticipants: (strategy: RoomAssignmentStrategy) => void; sendAnnouncement: (announcement: string) => void; @@ -96,6 +97,11 @@ interface BreakoutRoomContextValue { onMakeMePresenter: (action: 'start' | 'stop') => void; presenters: {uid: UidType; timestamp: number}[]; clearAllPresenters: () => void; + // Raised hands + raisedHands: {uid: UidType; timestamp: number}[]; + sendRaiseHandEvent: (action: 'raise' | 'lower') => void; + onRaiseHand: (action: 'raise' | 'lower', uid: UidType) => void; + clearAllRaisedHands: () => void; // State sync handleBreakoutRoomSyncState: ( data: BreakoutRoomSyncStateEventPayload['data']['data'], @@ -127,11 +133,14 @@ const BreakoutRoomContext = React.createContext({ getRoomMemberDropdownOptions: () => [], sendAnnouncement: () => {}, upsertBreakoutRoomAPI: async () => {}, - closeBreakoutRoomAPI: () => {}, checkIfBreakoutRoomSessionExistsAPI: async () => false, onMakeMePresenter: () => {}, presenters: [], clearAllPresenters: () => {}, + raisedHands: [], + sendRaiseHandEvent: () => {}, + onRaiseHand: () => {}, + clearAllRaisedHands: () => {}, handleBreakoutRoomSyncState: () => {}, permissions: null, }); @@ -139,9 +148,11 @@ const BreakoutRoomContext = React.createContext({ const BreakoutRoomProvider = ({ children, mainChannel, + handleLeaveBreakout, }: { children: React.ReactNode; mainChannel: string; + handleLeaveBreakout: () => void; }) => { const {store} = useContext(StorageContext); const {defaultContent, activeUids} = useContent(); @@ -154,10 +165,11 @@ const BreakoutRoomProvider = ({ data: {isHost, roomId}, } = useRoomInfo(); - const breakoutRoomExit = useBreakoutRoomExit(); + const location = useLocation(); + const isInBreakoutRoute = location.pathname.includes('breakout'); + + const breakoutRoomExit = useBreakoutRoomExit(handleLeaveBreakout); // Sync state - const lastSyncTimeRef = useRef(Date.now()); - console.log('supriya-exit', state.breakoutGroups); // Join Room const [selfJoinRoomId, setSelfJoinRoomId] = useState(null); @@ -174,17 +186,17 @@ const BreakoutRoomProvider = ({ {uid: UidType; timestamp: number}[] >([]); - // Fetch the data when the context loads. Dont wait till they open panel - useEffect(() => { - const loadData = async () => { - try { - await checkIfBreakoutRoomSessionExistsAPI(); - } catch (error) { - console.error('Failed to load breakout session:', error); - } - }; - loadData(); - }, []); + // Raised hands + const [raisedHands, setRaisedHands] = useState< + {uid: UidType; timestamp: number}[] + >([]); + + // Polling control + const [isPollingPaused, setIsPollingPaused] = useState(false); + + // Note: No initial data loading needed here as host polls every 2s and sends RTM sync events + // All participants (including attendees) receive current data via RTM sync events + // BreakoutRoomView handles data loading only when panel is opened // Update unassigned participants whenever defaultContent or activeUids change useEffect(() => { @@ -292,33 +304,50 @@ const BreakoutRoomProvider = ({ // Polling for sync event const pollBreakoutGetAPI = useCallback(async () => { - const now = Date.now(); - const timeSinceLastAPICall = now - lastSyncTimeRef.current; - - // If no UPDATE API call in last 30 seconds and we have an active session - if (timeSinceLastAPICall > 30000 && isHost && state.breakoutSessionId) { + if (isHost && state.breakoutSessionId) { console.log( - 'Fallback: Calling breakout session to sync events due to no recent updates', + 'supriya-polling-check Polling: Calling breakout session to sync events', ); await checkIfBreakoutRoomSessionExistsAPI(); - lastSyncTimeRef.current = Date.now(); } }, [isHost, state.breakoutSessionId]); - // Automatic interval management with cleanup + // Automatic interval management with cleanup only host will poll useEffect(() => { - if (isHost && state.breakoutSessionId) { - // Check every 15 seconds - const interval = setInterval(pollBreakoutGetAPI, 15000); + console.log( + 'supriya-polling-check for automatic interval', + isHost, + state.breakoutSessionId, + isPollingPaused, + ); + if ( + isHost && + !isPollingPaused && + (state.breakoutSessionId || isInBreakoutRoute) + ) { + console.log('supriya inside automatic interval'); + + // Check every 2 seconds + const interval = setInterval(pollBreakoutGetAPI, 2000); // React will automatically call this cleanup function return () => clearInterval(interval); } - }, [isHost, state.breakoutSessionId, pollBreakoutGetAPI]); + }, [ + isHost, + state.breakoutSessionId, + isPollingPaused, + isInBreakoutRoute, + pollBreakoutGetAPI, + ]); const upsertBreakoutRoomAPI = useCallback( async (type: 'START' | 'UPDATE' = 'START') => { const startReqTs = Date.now(); const requestId = getUniqueID(); + + // Pause polling during API call to prevent conflicts + setIsPollingPaused(true); + try { const payload = { passphrase: roomId.host, @@ -371,12 +400,13 @@ const BreakoutRoomProvider = ({ payload: data.breakout_room, }); } - if (type === 'UPDATE') { - lastSyncTimeRef.current = Date.now(); - } + // API update completed successfully } } catch (err) { console.log('debugging err', err); + } finally { + // Resume polling after API call completes (success or error) + setIsPollingPaused(false); } }, [ @@ -390,10 +420,6 @@ const BreakoutRoomProvider = ({ ], ); - const closeBreakoutRoomAPI = () => { - console.log('supriya close breakout room API not yet implemented'); - }; - const setStrategy = (strategy: RoomAssignmentStrategy) => { dispatch({ type: BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY, @@ -433,6 +459,7 @@ const BreakoutRoomProvider = ({ }; const handleAssignParticipants = (strategy: RoomAssignmentStrategy) => { + console.log('supriya participant assign strategy strategy: ', strategy); if (strategy === RoomAssignmentStrategy.AUTO_ASSIGN) { dispatch({ type: BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, @@ -443,6 +470,14 @@ const BreakoutRoomProvider = ({ type: BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, }); } + if (strategy === RoomAssignmentStrategy.NO_ASSIGN) { + dispatch({ + type: BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, + payload: { + canUserSwitchRoom: true, + }, + }); + } }; const moveUserToMainRoom = (user: ContentInterface) => { @@ -463,14 +498,14 @@ const BreakoutRoomProvider = ({ }, }); } - console.log(`supriya User ${user.name} (${user.uid}) moved to main room`); + console.log(`User ${user.name} (${user.uid}) moved to main room`); } catch (error) { - console.error('supriya Error moving user to main room:', error); + console.error('Error moving user to main room:', error); } }; const moveUserIntoGroup = (user: ContentInterface, toGroupId: string) => { - console.log('supriya move user to another room', user, toGroupId); + console.log('move user to another room', user, toGroupId); try { // Find user's current breakout group const currentGroup = state.breakoutGroups.find( @@ -497,10 +532,10 @@ const BreakoutRoomProvider = ({ }); console.log( - `supriya User ${user.name} (${user.uid}) moved to ${targetGroup.name}`, + `User ${user.name} (${user.uid}) moved to ${targetGroup.name}`, ); } catch (error) { - console.error('supriya Error moving user to breakout room:', error); + console.error('Error moving user to breakout room:', error); } }; @@ -719,8 +754,20 @@ const BreakoutRoomProvider = ({ const onMakeMePresenter = (action: 'start' | 'stop') => { if (action === 'start') { setICanPresent(true); + // Show toast notification when presenter permission is granted + Toast.show({ + type: 'success', + text1: 'You can now present in this breakout room', + visibilityTime: 3000, + }); } else if (action === 'stop') { setICanPresent(false); + // Show toast notification when presenter permission is removed + Toast.show({ + type: 'info', + text1: 'Your presenter access has been removed', + visibilityTime: 3000, + }); } }; @@ -728,13 +775,103 @@ const BreakoutRoomProvider = ({ setPresenters([]); }, []); + // Raised hand management functions + const addRaisedHand = useCallback( + (uid: UidType) => { + setRaisedHands(prev => { + // Check if already raised to avoid duplicates + const exists = prev.find(hand => hand.uid === uid); + if (exists) { + return prev; + } + return [...prev, {uid, timestamp: Date.now()}]; + }); + if (isHost) { + const userName = defaultContent[uid]?.name || `User ${uid}`; + Toast.show({ + leadingIconName: 'raise-hand', + type: 'info', + text1: `${userName} raised their hand`, + visibilityTime: 3000, + primaryBtn: null, + secondaryBtn: null, + leadingIcon: null, + }); + } + }, + [defaultContent, isHost], + ); + + const removeRaisedHand = useCallback( + (uid: UidType) => { + if (uid) { + setRaisedHands(prev => prev.filter(hand => hand.uid !== uid)); + } + if (isHost) { + const userName = defaultContent[uid]?.name || `User ${uid}`; + Toast.show({ + leadingIconName: 'raise-hand', + type: 'info', + text1: `${userName} lowered their hand`, + visibilityTime: 3000, + primaryBtn: null, + secondaryBtn: null, + leadingIcon: null, + }); + } + }, + [defaultContent, isHost], + ); + + const clearAllRaisedHands = useCallback(() => { + setRaisedHands([]); + }, []); + + // Send raise hand event via RTM + const sendRaiseHandEvent = useCallback( + (action: 'raise' | 'lower') => { + const payload = { + action, + uid: localUid, + timestamp: Date.now(), + }; + + events.send( + BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, + JSON.stringify(payload), + ); + }, + [localUid], + ); + + // Handle incoming raise hand events (only host sees notifications) + const onRaiseHand = (action: 'raise' | 'lower', uid: UidType) => { + try { + if (action === 'raise') { + addRaisedHand(uid); + // Show toast notification only to host + } else if (action === 'lower') { + removeRaisedHand(uid); + // Show toast notification only to host + } + } catch (error) { + console.error('Error handling raise hand event:', error); + } + }; + // Calculate permissions dynamically const permissions = useMemo((): BreakoutRoomPermissions => { const currentlyInRoom = isUserInRoom(); - console.log('supriya-exit currentlyInRoom: ', currentlyInRoom); + console.log('supriya-let currentlyInRoom: ', currentlyInRoom); const hasAvailableRooms = state.breakoutGroups.length > 0; + console.log('supriya-let hasAvailableRooms: ', hasAvailableRooms); const canUserSwitchRoom = state.canUserSwitchRoom; + console.log('supriya-let canUserSwitchRoom: ', canUserSwitchRoom); + console.log( + 'supriya-let canJoinRoom', + !currentlyInRoom && hasAvailableRooms && (isHost || canUserSwitchRoom), + ); if (true) { return { // Room navigation @@ -747,7 +884,8 @@ const BreakoutRoomProvider = ({ currentlyInRoom && hasAvailableRooms && (isHost || canUserSwitchRoom), // Media controls canScreenshare: currentlyInRoom ? canIPresent : isHost, - canRaiseHands: $config.RAISE_HAND && !isHost, + canRaiseHands: !isHost && !!state.breakoutSessionId, + canSeeRaisedHands: isHost, // Room management (host only) canAssignParticipants: isHost, canCreateRooms: isHost, @@ -775,18 +913,18 @@ const BreakoutRoomProvider = ({ state.canUserSwitchRoom, state.breakoutGroups, state.breakoutSessionId, + state.assignmentStrategy, canIPresent, ]); const handleBreakoutRoomSyncState = useCallback( (data: BreakoutRoomSyncStateEventPayload['data']['data']) => { const {switch_room, breakout_room} = data; - // Store previous state to compare changes const prevGroups = state.breakoutGroups; const prevSwitchRoom = state.canUserSwitchRoom; const userCurrentRoom = getCurrentRoom(); - const userCurrentRoomId = userCurrentRoom.id; + const userCurrentRoomId = userCurrentRoom?.id || null; dispatch({ type: BreakoutGroupActionTypes.SYNC_STATE, @@ -863,6 +1001,8 @@ const BreakoutRoomProvider = ({ text1: "You've returned to the main room.", visibilityTime: 3000, }); + // Exit breakout room and return to main room + exitRoom(); return; } } @@ -875,6 +1015,8 @@ const BreakoutRoomProvider = ({ text1: 'Breakout rooms are now closed. Returning to the main room...', visibilityTime: 4000, }); + // Exit breakout room and return to main room + exitRoom(); return; } @@ -896,6 +1038,8 @@ const BreakoutRoomProvider = ({ contact the host.`, visibilityTime: 5000, }); + // Exit breakout room and return to main room + exitRoom(); return; } } @@ -926,7 +1070,6 @@ const BreakoutRoomProvider = ({ if (!lastAction || !lastAction.type) { return; } - console.log('supriya-exit 1 lastAction: ', lastAction.type); // Actions that should trigger API calls const API_TRIGGERING_ACTIONS = [ @@ -942,15 +1085,26 @@ const BreakoutRoomProvider = ({ BreakoutGroupActionTypes.EXIT_GROUP, ]; - const shouldCallAPI = - API_TRIGGERING_ACTIONS.includes(lastAction.type as any) && isHost; + // Host can always trigger API calls for any action + // Attendees can only trigger API when they self-join a room and switch_room is enabled + console.log('supriya-allow-people lastAction', lastAction); + console.log('supriya-allow-people isHost', isHost); + const shouldCallAPI = + API_TRIGGERING_ACTIONS.includes(lastAction.type as any) && + (isHost || + (!isHost && + state.canUserSwitchRoom && + lastAction.type === + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP)); + console.log('supriya-allow-people shouldCallAPI', shouldCallAPI); if (shouldCallAPI) { + console.log('supriya-allow-people calling update api'); upsertBreakoutRoomAPI('UPDATE').finally(() => {}); } else { console.log(`Action ${lastAction.type} - skipping API call`); } - }, [lastAction, upsertBreakoutRoomAPI, isHost]); + }, [lastAction, upsertBreakoutRoomAPI, isHost, state.canUserSwitchRoom]); return ( diff --git a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx index 06dd9839e..a6b7c3745 100644 --- a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx +++ b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx @@ -14,36 +14,37 @@ const BreakoutRoomEventsConfigure: React.FC = ({ children, mainChannelName, }) => { - const {onMakeMePresenter, handleBreakoutRoomSyncState} = useBreakoutRoom(); + const {onMakeMePresenter, handleBreakoutRoomSyncState, onRaiseHand} = + useBreakoutRoom(); useEffect(() => { - const handleHandRaiseEvent = (evtData: any) => { - console.log('supriya BREAKOUT_ROOM_ATTENDEE_RAISE_HAND data: ', evtData); + const handlePresenterStatusEvent = (evtData: any) => { + console.log('supriya-event BREAKOUT_ROOM_MAKE_PRESENTER data: ', evtData); try { - const {uid, payload} = evtData; + const {payload} = evtData; const data = JSON.parse(payload); - // uid timestamp action - if (data.action === 'raise') { - } else if (data.action === 'lower') { + if (data.action === 'start' || data.action === 'stop') { + onMakeMePresenter(data.action); } } catch (error) {} }; - const handlePresenterStatusEvent = (evtData: any) => { - console.log('supriya BREAKOUT_ROOM_MAKE_PRESENTER data: ', evtData); + const handleRaiseHandEvent = (evtData: any) => { + console.log( + 'supriya-event BREAKOUT_ROOM_ATTENDEE_RAISE_HAND data: ', + evtData, + ); try { const {payload} = evtData; const data = JSON.parse(payload); - if (data.action === 'start') { - onMakeMePresenter('start'); - } else if (data.action === 'stop') { - onMakeMePresenter('stop'); + if (data.action === 'raise' || data.action === 'lower') { + onRaiseHand(data.action, data.uid); } } catch (error) {} }; const handleAnnouncementEvent = (evtData: any) => { - console.log('supriya BREAKOUT_ROOM_ANNOUNCEMENT data: ', evtData); + console.log('supriya-event BREAKOUT_ROOM_ANNOUNCEMENT data: ', evtData); try { const {_, payload} = evtData; const data = JSON.parse(payload); @@ -61,9 +62,9 @@ const BreakoutRoomEventsConfigure: React.FC = ({ } catch (error) {} }; - const handleBreakoutRoomStateSync = (evtData: any) => { + const handleBreakoutRoomSyncStateEvent = (evtData: any) => { const {payload} = evtData; - console.log('supriya BREAKOUT_ROOM_SYNC_STATE data: ', evtData); + console.log('supriya-event BREAKOUT_ROOM_SYNC_STATE data: ', evtData); const data: BreakoutRoomSyncStateEventPayload = JSON.parse(payload); if (data.data.act === 'SYNC_STATE') { handleBreakoutRoomSyncState(data.data.data); @@ -80,11 +81,11 @@ const BreakoutRoomEventsConfigure: React.FC = ({ ); events.on( BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, - handleHandRaiseEvent, + handleRaiseHandEvent, ); events.on( BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, - handleBreakoutRoomStateSync, + handleBreakoutRoomSyncStateEvent, ); return () => { @@ -95,14 +96,14 @@ const BreakoutRoomEventsConfigure: React.FC = ({ ); events.off( BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, - handleHandRaiseEvent, + handleRaiseHandEvent, ); events.off( BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, - handleBreakoutRoomStateSync, + handleBreakoutRoomSyncStateEvent, ); }; - }, [onMakeMePresenter, handleBreakoutRoomSyncState]); + }, [onMakeMePresenter, handleBreakoutRoomSyncState, onRaiseHand]); return <>{children}; }; diff --git a/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx b/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx new file mode 100644 index 000000000..81e105495 --- /dev/null +++ b/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx @@ -0,0 +1,44 @@ +import React, {useEffect} from 'react'; +import events from '../../../rtm-events-api'; +import {BreakoutRoomEventNames} from './constants'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; + +interface Props { + children: React.ReactNode; +} + +const BreakoutRoomMainEventsConfigure: React.FC = ({children}) => { + const {onRaiseHand} = useBreakoutRoom(); + + useEffect(() => { + const handleRaiseHandEvent = (evtData: any) => { + console.log( + 'supriya-event BREAKOUT_ROOM_ATTENDEE_RAISE_HAND data: ', + evtData, + ); + try { + const {payload} = evtData; + const data = JSON.parse(payload); + if (data.action === 'raise' || data.action === 'lower') { + onRaiseHand(data.action, data.uid); + } + } catch (error) {} + }; + + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, + handleRaiseHandEvent, + ); + + return () => { + events.off( + BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, + handleRaiseHandEvent, + ); + }; + }, [onRaiseHand]); + + return <>{children}; +}; + +export default BreakoutRoomMainEventsConfigure; diff --git a/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts b/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts index 3d8b288d7..7653e639f 100644 --- a/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts +++ b/template/src/components/breakout-room/hooks/useBreakoutRoomExit.ts @@ -2,7 +2,7 @@ import {useContext} from 'react'; import {useHistory, useParams} from '../../../components/Router'; import {useCaption, useContent, useSTTAPI} from 'customization-api'; -const useBreakoutRoomExit = () => { +const useBreakoutRoomExit = (handleLeaveBreakout?: () => void) => { const history = useHistory(); const {phrase} = useParams<{phrase: string}>(); const {defaultContent} = useContent(); @@ -24,10 +24,22 @@ const useBreakoutRoomExit = () => { console.log('Error stopping stt', error); }); } - // 2. Navigate back to main room (if not skipped) - history.push(`/${phrase}`); + + // Trigger exit transition if callback provided + if (handleLeaveBreakout) { + console.log('Triggering breakout room exit transition'); + handleLeaveBreakout(); + } else { + // Fallback: Navigate directly if no transition callback + history.push(`/${phrase}`); + } } catch (error) { - history.push(`/${phrase}`); + // Fallback navigation on error + if (handleLeaveBreakout) { + handleLeaveBreakout(); + } else { + history.push(`/${phrase}`); + } throw error; // Re-throw so caller can handle } diff --git a/template/src/components/breakout-room/state/reducer.ts b/template/src/components/breakout-room/state/reducer.ts index d589fe183..6d2e1d230 100644 --- a/template/src/components/breakout-room/state/reducer.ts +++ b/template/src/components/breakout-room/state/reducer.ts @@ -308,7 +308,7 @@ export const breakoutRoomReducer = ( const currentRoomId = roomIds[roomIndex]; const roomAssignment = roomAssignments.get(currentRoomId)!; // Assign participant based on their isHost status (string "true"/"false") - if (participant.user.isHost === 'true') { + if (participant.user?.isHost === 'true') { roomAssignment.hosts.push(participant.uid); } else { roomAssignment.attendees.push(participant.uid); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index fcad2ec56..cbe4e6413 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -16,6 +16,7 @@ import IconButton from '../../../atoms/IconButton'; import ThemeConfig from '../../../theme'; import {UidType} from 'agora-rn-uikit'; import UserAvatar from '../../../atoms/UserAvatar'; +import ImageIcon from '../../../atoms/ImageIcon'; import {BreakoutGroup} from '../state/reducer'; import {useContent} from 'customization-api'; import {videoRoomUserFallbackText} from '../../../language/default-labels/videoCallScreenLabels'; @@ -43,6 +44,8 @@ const BreakoutRoomGroupSettings: React.FC = () => { sendAnnouncement, updateRoomName, canUserSwitchRoom, + raisedHands, + permissions, } = useBreakoutRoom(); const disableJoinBtn = !isHost && !canUserSwitchRoom; @@ -99,7 +102,10 @@ const BreakoutRoomGroupSettings: React.FC = () => { const memberRef = memberMoreMenuRefs.current[memberUId]; const isMenuVisible = actionMenuVisible[memberUId] || false; - const hasRaisedHand = false; + const hasRaisedHand = + permissions?.canSeeRaisedHands && + raisedHands.some(hand => hand.uid === memberUId); + return ( @@ -114,7 +120,18 @@ const BreakoutRoomGroupSettings: React.FC = () => { - {hasRaisedHand ? '✋' : <>} + {hasRaisedHand ? ( + + + + ) : ( + <> + )} {isHost || canUserSwitchRoom ? ( @@ -206,7 +223,6 @@ const BreakoutRoomGroupSettings: React.FC = () => { {isHost ? ( { - console.log('supriya on delete clicked'); closeRoom(room.id); }} onRenameRoom={() => { @@ -283,7 +299,17 @@ const BreakoutRoomGroupSettings: React.FC = () => { <> )} - {breakoutGroups.map(renderRoom)} + + {breakoutGroups.length === 0 ? ( + + + The host hasn't created any breakout rooms yet + + + ) : ( + breakoutGroups.map(renderRoom) + )} + {isAnnoucementModalOpen && ( { + const action = isHandRaised ? 'lower' : 'raise'; + sendRaiseHandEvent(action); + setIsHandRaised(!isHandRaised); + }; return ( - - - - Please wait, the meeting host will assign you to a room shortly. - - + {!isUserInRoom() ? ( + + + + Please wait, the meeting host will assign you to a room shortly. + + + ) : ( + <> + )} {}} + text={isHandRaised ? 'Lower Hand' : 'Raise Hand'} + iconName={isHandRaised ? 'lower-hand' : 'raise-hand'} + iconColor={$config.SEMANTIC_WARNING} + iconSize={15} + onPress={handleRaiseHand} /> diff --git a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx index 4d4455274..c9403a996 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx @@ -31,13 +31,25 @@ export default function BreakoutRoomSettings() { setModalOpen: setManualAssignmentModalOpen, } = useModal(); - useEffect(() => { + // Handle assign participants button click + const handleAssignClick = () => { if (assignmentStrategy === RoomAssignmentStrategy.MANUAL_ASSIGN) { + // Open manual assignment modal setManualAssignmentModalOpen(true); } else { - setManualAssignmentModalOpen(false); + // Handle other assignment strategies + handleAssignParticipants(assignmentStrategy); } - }, [assignmentStrategy, setManualAssignmentModalOpen]); + }; + + // Handle strategy change - automatically trigger assignment for NO_ASSIGN + const handleStrategyChange = (strategy: RoomAssignmentStrategy) => { + setStrategy(strategy); + // NO_ASSIGN needs to be applied immediately to enable switch rooms + if (strategy === RoomAssignmentStrategy.NO_ASSIGN) { + handleAssignParticipants(strategy); + } + }; return ( @@ -49,7 +61,7 @@ export default function BreakoutRoomSettings() { { - handleAssignParticipants(assignmentStrategy); - }} + onPress={handleAssignClick} text={'Assign participants'} /> diff --git a/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx b/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx index 1a2adc447..21323928f 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomTransition.tsx @@ -1,9 +1,16 @@ import React, {useEffect, useState} from 'react'; import Loading from '../../../subComponents/Loading'; -import {View, StyleSheet, Text} from 'react-native'; -import ThemeConfig from '../../../theme'; +import {View, StyleSheet} from 'react-native'; -const BreakoutRoomTransition = ({onTimeout}: {onTimeout: () => void}) => { +interface BreakoutRoomTransitionProps { + onTimeout: () => void; + direction?: 'enter' | 'exit'; +} + +const BreakoutRoomTransition = ({ + onTimeout, + direction = 'enter', +}: BreakoutRoomTransitionProps) => { const [dots, setDots] = useState(''); useEffect(() => { @@ -19,10 +26,13 @@ const BreakoutRoomTransition = ({onTimeout}: {onTimeout: () => void}) => { }; }, [onTimeout]); + const transitionText = + direction === 'exit' ? 'Exiting breakout room' : 'Entering breakout room'; + return ( diff --git a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx index c34ffc905..fdca43eb5 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx @@ -27,6 +27,7 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { upsertBreakoutRoomAPI, closeAllRooms, permissions, + isUserInRoom, } = useBreakoutRoom(); useEffect(() => { @@ -64,14 +65,14 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { ) : ( - {permissions.canRaiseHands ? : <>} - {permissions.canAssignParticipants ? ( + {permissions?.canRaiseHands ? : <>} + {permissions?.canAssignParticipants ? ( ) : ( )} - {permissions.canCreateRooms ? ( + {permissions?.canCreateRooms ? ( )} - {!isInitializing && permissions.canCloseRooms ? ( + {!isInitializing && permissions?.canCloseRooms ? ( void; onSelectionChange: (uid: UidType) => void; }) { - console.log('supriya-manual individual assignment', assignment); - console.log('supriya-manual participant', participant); const selectedValue = assignment?.roomId || 'unassigned'; return ( @@ -90,7 +91,7 @@ export default function ParticipantManualAssignmentModal( unsassignedParticipants, manualAssignments, setManualAssignments, - applyManualAssignments, + handleAssignParticipants, } = useBreakoutRoom(); // Local state for assignments @@ -110,7 +111,6 @@ export default function ParticipantManualAssignmentModal( isSelected: false, })); }); - console.log('supriya-manual localAssignments', localAssignments); // Rooms dropdown options const rooms = [ {label: 'Unassigned', value: 'unassigned'}, @@ -215,6 +215,8 @@ export default function ParticipantManualAssignmentModal( const handleSaveManualAssignments = () => { setManualAssignments(localAssignments); + // Trigger the actual assignment after saving + handleAssignParticipants(RoomAssignmentStrategy.MANUAL_ASSIGN); setModalOpen(false); }; diff --git a/template/src/pages/BreakoutRoomVideoCall.tsx b/template/src/pages/BreakoutRoomVideoCall.tsx deleted file mode 100644 index 273cd2d28..000000000 --- a/template/src/pages/BreakoutRoomVideoCall.tsx +++ /dev/null @@ -1,239 +0,0 @@ -/* -******************************************** - Copyright © 2021 Agora Lab, Inc., all rights reserved. - AppBuilder and all associated components, source code, APIs, services, and documentation - (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be - accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. - Use without a license or in violation of any license terms and conditions (including use for - any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more - information visit https://appbuilder.agora.io. -********************************************* -*/ -import React, {useState, useContext} from 'react'; -import {StyleSheet} from 'react-native'; -import { - RtcConfigure, - PropsProvider, - ClientRoleType, - ChannelProfileType, - LocalUserContext, -} from '../../agora-rn-uikit'; -import RtmConfigure from '../components/RTMConfigure'; -import DeviceConfigure from '../components/DeviceConfigure'; -import {LiveStreamContextProvider} from '../components/livestream'; -import ScreenshareConfigure from '../subComponents/screenshare/ScreenshareConfigure'; -import {isMobileUA} from '../utils/common'; -import {LayoutProvider} from '../utils/useLayout'; -import {RecordingProvider} from '../subComponents/recording/useRecording'; -import {SidePanelProvider} from '../utils/useSidePanel'; -import {NetworkQualityProvider} from '../components/NetworkQualityContext'; -import {ChatNotificationProvider} from '../components/chat-notification/useChatNotification'; -import {ChatUIControlsProvider} from '../components/chat-ui/useChatUIControls'; -import {ScreenShareProvider} from '../components/contexts/ScreenShareContext'; -import {LiveStreamDataProvider} from '../components/contexts/LiveStreamDataContext'; -import {VideoMeetingDataProvider} from '../components/contexts/VideoMeetingDataContext'; -import {UserPreferenceProvider} from '../components/useUserPreference'; -import EventsConfigure from '../components/EventsConfigure'; -import {FocusProvider} from '../utils/useFocus'; -import {VideoCallProvider} from '../components/useVideoCall'; -import {CaptionProvider} from '../subComponents/caption/useCaption'; -import SdkMuteToggleListener from '../components/SdkMuteToggleListener'; -import {NoiseSupressionProvider} from '../app-state/useNoiseSupression'; -import {VideoQualityContextProvider} from '../app-state/useVideoQuality'; -import {VBProvider} from '../components/virtual-background/useVB'; -import {DisableChatProvider} from '../components/disable-chat/useDisableChat'; -import {WaitingRoomProvider} from '../components/contexts/WaitingRoomContext'; -import PermissionHelper from '../components/precall/PermissionHelper'; -import {ChatMessagesProvider} from '../components/chat-messages/useChatMessages'; -import VideoCallScreenWrapper from './video-call/VideoCallScreenWrapper'; -import {BeautyEffectProvider} from '../components/beauty-effect/useBeautyEffects'; -import {UserActionMenuProvider} from '../components/useUserActionMenu'; -import {VideoRoomOrchestratorState} from './VideoCallRoomOrchestrator'; -import StorageContext from '../components/StorageContext'; -import {SdkApiContext} from '../components/SdkApiContext'; -import {RnEncryptionEnum} from './VideoCall'; -import VideoCallStateSetup from './video-call/VideoCallStateSetup'; -import {BreakoutRoomProvider} from '../components/breakout-room/context/BreakoutRoomContext'; - -interface BreakoutVideoCallProps { - setBreakoutRtcEngine?: ( - engine: VideoRoomOrchestratorState['rtcEngine'], - ) => void; - storedBreakoutChannelDetails: VideoRoomOrchestratorState['channelDetails']; -} - -const BreakoutRoomVideoCall = ( - breakoutVideoCallProps: BreakoutVideoCallProps, -) => { - const {storedBreakoutChannelDetails} = breakoutVideoCallProps; - const {store} = useContext(StorageContext); - const [isRecordingActive, setRecordingActive] = useState(false); - const [recordingAutoStarted, setRecordingAutoStarted] = useState(false); - const [sttAutoStarted, setSttAutoStarted] = useState(false); - - const { - join: SdkJoinState, - microphoneDevice: sdkMicrophoneDevice, - cameraDevice: sdkCameraDevice, - } = useContext(SdkApiContext); - - const callActive = true; - - const [rtcProps, setRtcProps] = React.useState({ - appId: $config.APP_ID, - channel: storedBreakoutChannelDetails.channel, - uid: storedBreakoutChannelDetails.uid as number, - token: storedBreakoutChannelDetails.token, - rtm: storedBreakoutChannelDetails.rtmToken, - screenShareUid: storedBreakoutChannelDetails?.screenShareUid as number, - screenShareToken: storedBreakoutChannelDetails?.screenShareToken || '', - profile: $config.PROFILE, - screenShareProfile: $config.SCREEN_SHARE_PROFILE, - dual: true, - encryption: $config.ENCRYPTION_ENABLED - ? {key: null, mode: RnEncryptionEnum.AES128GCM2, screenKey: null} - : false, - role: ClientRoleType.ClientRoleBroadcaster, - geoFencing: $config.GEO_FENCING, - audioRoom: $config.AUDIO_ROOM, - activeSpeaker: $config.ACTIVE_SPEAKER, - preferredCameraId: - sdkCameraDevice.deviceId || store?.activeDeviceId?.videoinput || null, - preferredMicrophoneId: - sdkMicrophoneDevice.deviceId || store?.activeDeviceId?.audioinput || null, - recordingBot: false, - }); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - {!isMobileUA() && ( - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -const styleProps = StyleSheet.create({ - errorContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#1a1a1a', - padding: 20, - }, - errorTitle: { - color: 'white', - fontSize: 24, - fontWeight: 'bold', - marginBottom: 16, - }, - errorMessage: { - color: '#cccccc', - fontSize: 16, - textAlign: 'center', - marginBottom: 24, - }, - returnButton: { - minWidth: 200, - }, - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#1a1a1a', - }, - loadingText: { - color: 'white', - fontSize: 18, - marginBottom: 20, - }, -}); - -export default BreakoutRoomVideoCall; diff --git a/template/src/pages/VideoCall.tsx b/template/src/pages/VideoCall.tsx index 7377a9761..e0c509579 100644 --- a/template/src/pages/VideoCall.tsx +++ b/template/src/pages/VideoCall.tsx @@ -81,9 +81,8 @@ import {BeautyEffectProvider} from '../components/beauty-effect/useBeautyEffects import {UserActionMenuProvider} from '../components/useUserActionMenu'; import Toast from '../../react-native-toast-message'; import {AuthErrorCodes} from '../utils/common'; -import {VideoRoomOrchestratorState} from './VideoCallRoomOrchestrator'; -import VideoCallStateSetup from './video-call/VideoCallStateSetup'; import {BreakoutRoomProvider} from '../components/breakout-room/context/BreakoutRoomContext'; +import BreakoutRoomMainEventsConfigure from '../components/breakout-room/events/BreakoutRoomMainEventsConfigure'; interface VideoCallProps { callActive: boolean; @@ -210,8 +209,13 @@ const VideoCall = (videoCallProps: VideoCallProps) => { - + + + diff --git a/template/src/pages/VideoCallRoomOrchestrator.tsx b/template/src/pages/VideoCallRoomOrchestrator.tsx deleted file mode 100644 index ef224046b..000000000 --- a/template/src/pages/VideoCallRoomOrchestrator.tsx +++ /dev/null @@ -1,151 +0,0 @@ -/* -******************************************** - Copyright © 2021 Agora Lab, Inc., all rights reserved. - AppBuilder and all associated components, source code, APIs, services, and documentation - (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be - accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. - Use without a license or in violation of any license terms and conditions (including use for - any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more - information visit https://appbuilder.agora.io. -********************************************* -*/ -import React, {useState, useEffect, useCallback} from 'react'; -import VideoCall from '../pages/VideoCall'; -import BreakoutRoomVideoCall from './BreakoutRoomVideoCall'; -import {RTMCoreProvider} from '../rtm/RTMCoreProvider'; -import {useParams, useHistory, useLocation} from '../components/Router'; -import events from '../rtm-events-api'; -import {EventNames} from '../rtm-events'; -import {BreakoutChannelJoinEventPayload} from '../components/breakout-room/state/types'; - -export interface VideoRoomOrchestratorState { - channel: string; - uid: number | string; - token: string; - screenShareUid: number | string; - screenShareToken: string; - rtmToken: string; -} - -const VideoCallRoomOrchestrator: React.FC = () => { - const {phrase} = useParams<{phrase: string}>(); - const history = useHistory(); - const location = useLocation(); - - // Parse query parameters - const searchParams = new URLSearchParams(location.search); - const isBreakoutRoomActive = searchParams.get('breakout') === 'true'; - const breakoutChannelName = searchParams.get('channelName'); - - // Main room state - const [mainRoomState, setMainRoomState] = - useState({ - channel: phrase, // Use phrase as main channel - uid: null, - token: null, - screenShareUid: null, - screenShareToken: null, - rtmToken: null, - }); - - // Breakout room state - const [breakoutRoomState, setBreakoutRoomState] = - useState({ - channel: breakoutChannelName || null, - uid: null, - token: null, - screenShareUid: null, - screenShareToken: null, - rtmToken: null, - }); - - // Listen for breakout room join events - useEffect(() => { - const handleBreakoutJoin = (evtData: any) => { - const {payload} = evtData; - const data: BreakoutChannelJoinEventPayload = JSON.parse(payload); - console.log('[VideoCallRoomOrchestrator] Breakout join data:', data); - - const {channel_name, mainUser, screenShare} = data.data.data; - - try { - // Update breakout room state - setBreakoutRoomState({ - channel: channel_name, - token: mainUser.rtc, - uid: mainUser?.uid, - screenShareToken: screenShare.rtc, - screenShareUid: screenShare.uid, - rtmToken: mainUser.rtm, - }); - - // Navigate to breakout room with proper URL - history.push(`/${phrase}?breakout=true&channelName=${channel_name}`); - } catch (error) { - console.error( - '[VideoCallRoomOrchestrator] Breakout join error:', - error, - ); - } - }; - - events.on(EventNames.BREAKOUT_ROOM_JOIN_DETAILS, handleBreakoutJoin); - - return () => { - events.off(EventNames.BREAKOUT_ROOM_JOIN_DETAILS, handleBreakoutJoin); - }; - }, [history, phrase]); - - // Handle leaving breakout room - const handleLeaveBreakout = useCallback(() => { - console.log('[VideoCallRoomOrchestrator] Leaving breakout room'); - - // Clear breakout state - setBreakoutRoomState({ - channel: null, - uid: null, - token: null, - screenShareUid: null, - screenShareToken: null, - rtmToken: null, - }); - - // Return to main room - history.push(`/${phrase}`); - }, [history, phrase]); - - // Update main room details - const setMainChannelDetails = useCallback( - (channelInfo: VideoRoomOrchestratorState) => { - console.log('[VideoCallRoomOrchestrator] Setting main channel details'); - setMainRoomState(prev => ({ - ...prev, - ...channelInfo, - })); - }, - [], - ); - - console.log('[VideoCallRoomOrchestrator] Rendering:', { - isBreakoutRoomActive, - phrase, - mainChannel: mainRoomState, - breakoutChannel: breakoutRoomState, - }); - - return ( - - {isBreakoutRoomActive && breakoutRoomState?.channel ? ( - - ) : ( - - )} - - ); -}; - -export default VideoCallRoomOrchestrator; diff --git a/template/src/pages/video-call/BreakoutVideoCallContent.tsx b/template/src/pages/video-call/BreakoutVideoCallContent.tsx index 090e3a9c9..ae5e72577 100644 --- a/template/src/pages/video-call/BreakoutVideoCallContent.tsx +++ b/template/src/pages/video-call/BreakoutVideoCallContent.tsx @@ -65,10 +65,10 @@ interface BreakoutVideoCallContentProps extends VideoCallContentProps { const BreakoutVideoCallContent: React.FC = ({ rtcProps, breakoutChannelDetails, + onLeave, callActive, callbacks, styleProps, - onLeave, }) => { const [isRecordingActive, setRecordingActive] = useState(false); const [sttAutoStarted, setSttAutoStarted] = useState(false); @@ -82,7 +82,6 @@ const BreakoutVideoCallContent: React.FC = ({ screenShareUid: breakoutChannelDetails?.screenShareUid as number, screenShareToken: breakoutChannelDetails?.screenShareToken || '', }); - console.log('supriya breakoutRoomRTCProps', breakoutRoomRTCProps); const {client, isLoggedIn} = useRTMCore(); @@ -114,19 +113,6 @@ const BreakoutVideoCallContent: React.FC = ({ }; }, [client, isLoggedIn, rtcProps.channel]); - // Modified callbacks that use the onLeave prop - const endCallModifiedCallbacks = useMemo( - () => ({ - ...callbacks, - EndCall: () => { - console.log('Breakout room end call triggered'); - // Use the parent's onLeave callback - onLeave?.(); - }, - }), - [callbacks, onLeave], - ); - return ( = ({ ...breakoutRoomRTCProps, callActive, }, - callbacks: endCallModifiedCallbacks, + callbacks, styleProps, mode: $config.EVENT_MODE ? ChannelProfileType.ChannelProfileLiveBroadcasting @@ -197,6 +183,9 @@ const BreakoutVideoCallContent: React.FC = ({ >; - rtcProps: RtcPropsInterface; - setRtcProps: React.Dispatch>>; - callbacks: CallbacksInterface; - styleProps: any; -} - -const MainVideoCallContent: React.FC = ({ - callActive, - setCallActive, - rtcProps, - setRtcProps, - callbacks, - styleProps, -}) => { - const [isRecordingActive, setRecordingActive] = useState(false); - const [sttAutoStarted, setSttAutoStarted] = useState(false); - const [recordingAutoStarted, setRecordingAutoStarted] = useState(false); - const {PrefereceWrapper} = useCustomization(data => { - let components: { - PrefereceWrapper: React.ComponentType; - } = { - PrefereceWrapper: React.Fragment, - }; - if ( - data?.components?.preferenceWrapper && - typeof data?.components?.preferenceWrapper !== 'object' && - isValidReactComponent(data?.components?.preferenceWrapper) - ) { - components.PrefereceWrapper = data?.components?.preferenceWrapper; - } - - return components; - }); - - return ( - - - - - - - - - - - - - - - - - - - - - - - - {!isMobileUA() && ( - - )} - - - - {/* */} - - {callActive ? ( - - - - - - - - ) : $config.PRECALL ? ( - - - - ) : ( - <> - )} - - {/* */} - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default MainVideoCallContent; diff --git a/template/src/pages/video-call/VideoCallContent.tsx b/template/src/pages/video-call/VideoCallContent.tsx index 19c375641..c045c4edb 100644 --- a/template/src/pages/video-call/VideoCallContent.tsx +++ b/template/src/pages/video-call/VideoCallContent.tsx @@ -19,6 +19,7 @@ import VideoCall from '../VideoCall'; import BreakoutVideoCallContent from './BreakoutVideoCallContent'; import {BreakoutRoomEventNames} from '../../components/breakout-room/events/constants'; import BreakoutRoomTransition from '../../components/breakout-room/ui/BreakoutRoomTransition'; +import Toast from '../../../react-native-toast-message'; export interface BreakoutChannelDetails { channel: string; @@ -53,6 +54,11 @@ const VideoCallContent: React.FC = props => { const [breakoutChannelDetails, setBreakoutChannelDetails] = useState(null); + // Track transition direction for better UX + const [transitionDirection, setTransitionDirection] = useState< + 'enter' | 'exit' + >('enter'); + // Listen for breakout room join events useEffect(() => { const handleBreakoutJoin = (evtData: any) => { @@ -64,9 +70,10 @@ const VideoCallContent: React.FC = props => { // Process the event payload const {payload} = evtData; const data: BreakoutChannelJoinEventPayload = JSON.parse(payload); - console.log('supriya Breakout room join event received', data); + console.log('supriya-event Breakout room join event received', data); if (data?.data?.act === 'CHAN_JOIN') { - const {channel_name, mainUser, screenShare, chat} = data.data.data; + const {channel_name, mainUser, screenShare, chat, room_name} = + data.data.data; // Extract breakout channel details const breakoutDetails: BreakoutChannelDetails = { channel: channel_name, @@ -79,6 +86,7 @@ const VideoCallContent: React.FC = props => { // Set breakout state active history.push(`/${phrase}?breakout=true`); setBreakoutChannelDetails(null); + setTransitionDirection('enter'); // Set direction for entering // Add state after a delay to show transitioning screen breakoutTimeoutRef.current = setTimeout(() => { setBreakoutChannelDetails(prev => ({ @@ -87,9 +95,17 @@ const VideoCallContent: React.FC = props => { })); breakoutTimeoutRef.current = null; }, 800); + + setTimeout(() => { + Toast.show({ + type: 'success', + text1: `You’ve been added to ${room_name} by the host.`, + visibilityTime: 3000, + }); + }, 500); } } catch (error) { - console.error(' supriya Failed to process breakout join event'); + console.error('Failed to process breakout join event'); } }; @@ -120,12 +136,30 @@ const VideoCallContent: React.FC = props => { // Handle leaving breakout room const handleLeaveBreakout = useCallback(() => { console.log('Leaving breakout room, returning to main room'); - // Clear breakout channel details + // Set direction for exiting + setTransitionDirection('exit'); + // Clear breakout channel details to show transition setBreakoutChannelDetails(null); - // Navigate back to main room - history.push(`/${phrase}`); + // Navigate back to main room after a delay + setTimeout(() => { + history.push(`/${phrase}`); + }, 800); }, [history, phrase]); + // Route protection: Prevent direct navigation to breakout route + useEffect(() => { + if (isBreakoutMode && !breakoutChannelDetails) { + // If user navigated to breakout route without valid channel details, + // redirect to main room after a short delay to prevent infinite loops + const redirectTimer = setTimeout(() => { + console.log('Invalid breakout route access, redirecting to main room'); + history.replace(`/${phrase}`); // Use replace to prevent back navigation + }, 2000); // Give 2s for legitimate transitions + + return () => clearTimeout(redirectTimer); + } + }, [isBreakoutMode, breakoutChannelDetails, history, phrase]); + // Conditional rendering based on URL params return ( <> @@ -140,6 +174,7 @@ const VideoCallContent: React.FC = props => { /> ) : ( { setBreakoutChannelDetails(null); }} diff --git a/template/src/pages/video-call/VideoCallStateSetup.tsx b/template/src/pages/video-call/VideoCallStateSetup.tsx deleted file mode 100644 index 526ad48f0..000000000 --- a/template/src/pages/video-call/VideoCallStateSetup.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, {useEffect} from 'react'; -import {useRoomInfo, useRtc} from 'customization-api'; -import {VideoCallProps} from '../VideoCall'; - -const VideoCallStateSetup: React.FC = ({ - setMainRtcEngine, - setMainChannelDetails, -}) => { - const {RtcEngineUnsafe} = useRtc(); - const {data: roomInfo} = useRoomInfo(); - - // Listen for engine changes and notify orchestrator - useEffect(() => { - if ($config.ENABLE_BREAKOUT_ROOM && RtcEngineUnsafe && setMainRtcEngine) { - console.log( - 'supriya [VideoCallStateSetup] Engine ready, storing in state', - ); - setMainRtcEngine(RtcEngineUnsafe); - } - }, [RtcEngineUnsafe, setMainRtcEngine]); - - // Listen for channel details and notify orchestrator - useEffect(() => { - if ( - $config.ENABLE_BREAKOUT_ROOM && - roomInfo?.channel && - setMainChannelDetails - ) { - console.log( - 'supriya [VideoCallStateSetup] Channel details ready, storing in state', - ); - setMainChannelDetails({ - channel: roomInfo.channel || '', - token: roomInfo.token || '', - uid: roomInfo.uid || 0, - screenShareToken: roomInfo.screenShareToken, - screenShareUid: roomInfo.screenShareUid, - rtmToken: roomInfo.rtmToken, - }); - } - }, [roomInfo, setMainChannelDetails]); - - // This component only handles effects, renders nothing - return null; -}; - -export default VideoCallStateSetup; diff --git a/template/src/rtm/RTMCoreProvider.tsx b/template/src/rtm/RTMCoreProvider.tsx index 8155624ae..c4d7f849a 100644 --- a/template/src/rtm/RTMCoreProvider.tsx +++ b/template/src/rtm/RTMCoreProvider.tsx @@ -19,6 +19,8 @@ import type { import {UidType} from '../../agora-rn-uikit'; import RTMEngine from '../rtm/RTMEngine'; import {nativePresenceEventTypeMapping} from '../../bridge/rtm/web/Types'; +import {isWeb, isWebInternal} from '../utils/common'; +import isSDK from '../utils/isSDK'; // Event callback types type MessageCallback = (message: MessageEvent) => void; @@ -69,7 +71,7 @@ export const RTMCoreProvider: React.FC = ({ const [client, setClient] = useState(null); // Use state instead const [isLoggedIn, setIsLoggedIn] = useState(false); const [connectionState, setConnectionState] = useState(0); - console.log('supriya-rtm-restest connectionState: ', connectionState); + console.log('supriya-rtm connectionState: ', connectionState); const [error, setError] = useState(null); const [onlineUsers, setOnlineUsers] = useState>(new Set()); // Callback registration storage @@ -215,7 +217,6 @@ export const RTMCoreProvider: React.FC = ({ 'supriya-rtm-global ######################## ---MessageEvent event: ', message, ); - console.log('supriya callbackRegistry', callbackRegistry); // Distribute to all registered callbacks callbackRegistry.current.forEach((callbacks, channelName) => { if (callbacks.message) { @@ -233,7 +234,6 @@ export const RTMCoreProvider: React.FC = ({ client.addEventListener('message', handleGlobalMessageEvent); return () => { - console.log('supriya removing up global listeners'); // Remove global event listeners client.removeEventListener('storage', handleGlobalStorageEvent); client.removeEventListener('presence', handleGlobalPresenceEvent); @@ -293,17 +293,51 @@ export const RTMCoreProvider: React.FC = ({ return () => { // Cleanup - console.log('supriya-rtm-retest RTM cleanup is happening'); + console.log('supriya-rtm RTM cleanup is happening'); if (client) { console.log('supriya RTM cleanup is happening'); - client.removeAllListeners(); - client.logout().catch(() => {}); RTMEngine.getInstance().destroy(); setClient(null); } }; }, [client, stableUserInfo, setAttribute]); + // Handle browser close/reload events for RTM cleanup + useEffect(() => { + if (!$config.ENABLE_CONVERSATIONAL_AI) { + const handleBrowserClose = (ev: BeforeUnloadEvent) => { + ev.preventDefault(); + return (ev.returnValue = 'Are you sure you want to exit?'); + }; + + const handleRTMLogout = () => { + if (client && isLoggedIn) { + console.log('Browser closing, logging out from RTM'); + client.logout().catch(() => {}); + } + }; + + if (!isWebInternal()) { + return; + } + + window.addEventListener( + 'beforeunload', + isWeb() && !isSDK() ? handleBrowserClose : () => {}, + ); + + window.addEventListener('pagehide', handleRTMLogout); + + return () => { + window.removeEventListener( + 'beforeunload', + isWeb() && !isSDK() ? handleBrowserClose : () => {}, + ); + window.removeEventListener('pagehide', handleRTMLogout); + }; + } + }, [client, isLoggedIn]); + return ( { - if (!client || !channelId) { - return; - } - let cancelled = false; - (async () => { - try { - await client.subscribe(channelId, { - withMessage: true, - withPresence: true, - withMetadata: true, - withLock: false, - }); - RTMEngine.getInstance().addChannel(channelId); - } catch (err) { - console.warn(`Failed to subscribe to secondary ${channelId}`, err); - } - })(); - - return () => { - if (!cancelled) { - client.unsubscribe(channelId).catch(() => {}); - RTMEngine.getInstance().removeChannel(channelId); - } - cancelled = true; - }; - }, [client, channelId]); -} From cf648fc95fc8e0b783127a3ab43422488ca540e0 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Fri, 12 Sep 2025 11:20:57 +0530 Subject: [PATCH 04/56] SubFeature: Breakout Room adding edgecase and debug logging (#746) * testing edgecases and adding logging --- config.json | 82 +- .../context/BreakoutRoomContext.tsx | 1996 +++++++++++++---- .../events/BreakoutRoomEventsConfigure.tsx | 190 +- .../BreakoutRoomMainEventsConfigure.tsx | 25 +- .../breakout-room/events/constants.ts | 4 + .../components/breakout-room/state/reducer.ts | 84 +- .../components/breakout-room/state/types.ts | 8 +- .../ui/BreakoutRoomGroupSettings.tsx | 31 +- .../ui/BreakoutRoomMainRoomUsers.tsx | 35 +- .../breakout-room/ui/BreakoutRoomSettings.tsx | 47 +- .../breakout-room/ui/BreakoutRoomView.tsx | 29 +- .../ui/ParticipantManualAssignmentModal.tsx | 10 +- .../controls/useControlPermissionMatrix.tsx | 3 +- template/src/logger/AppBuilderLogger.tsx | 3 +- template/src/pages/VideoCall.tsx | 8 +- template/src/rtm-events-api/Events.ts | 14 +- template/src/rtm/RTMCoreProvider.tsx | 20 - template/src/utils/useDebouncedCallback.tsx | 20 + 18 files changed, 1961 insertions(+), 648 deletions(-) create mode 100644 template/src/utils/useDebouncedCallback.tsx diff --git a/config.json b/config.json index ceaa4b278..959b2b5cc 100644 --- a/config.json +++ b/config.json @@ -1,22 +1,5 @@ { - "PROJECT_ID": "49c705c1c9efb71000d7", - "APP_ID": "a569f8fb0309417780b793786b534a86", - "APP_CERTIFICATE": "6545ecd19d554737be863eb1eaaf9cee", - "CUSTOMER_ID": "40b25d211955491580720cb54099c3c4", - "CUSTOMER_CERTIFICATE": "555d0c42035c450a9b562ec20773d6b4", - "PRODUCT_ID": "helloworld", - "APP_NAME": "HelloWorld", - "LOGO": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKIAAAA5CAYAAAC1U/CbAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABFwSURBVHgB7Z0NnFTVdcDPue/NzJuFBSV+EBEFAvxsCjZVUxXUfGgNajWkiRoTg4GdXVBQSLStpmUFTWt+KthE4rK7s7uuJmqoaaOm1aZNFaI0mlhav1JTMVsMMcSA4sLMezPv3ZNzZ0H36933MW83+Nv5/37+lpl75r77cd6955577hUhBuNb6CgvC7PA9Y4hgJQAKEgydgsLXil8AV8Pm8+4LppMrndqodF8CKpk0jdogpMuT5cpMZMI6oAAoQos03j4rcX4FlTBuPZ9R0svMwNSOJXLlOEC2QS0B8rmdvtK7IFqaN0zMYP1p6Ew/9fO4f+H/t0assZPLs3wTOPEYs58AGJS10rvl+jORsTJXDdTGNBb4rq5wnwBIrSb1UrHUcr9g9CdZbWXPwooPsv/XMj/He0vib8ApM0A8h67IfX4cBLpDvdTQoqVLPcR/mjbOSMLMchspFloykWscxci0Inq4ZAQHooPlRvwfyAiVr58FkpYSEJ8hj9O1Yju4pd4iyB3U7Ex8yBEfQ53IBjygALi04TY7DTgD3zlO7mtPe8qErgACSao77jdw7fXespmJsJ8lN4V/LwF3NJH+MoS/JzTt3jotZYb0j8dtjx5N8f1X4aAJ7NsT2BBrHZ3MXf09YgwG6LTA5LW2k3m3ZVPm8jI7PU28luU6ycTWRHNtvI8fgX/lmusFDkx5XsXfM3OieOi/CKTLy8Awq9y3U6G6PSw4t7h7IANsIbVOAQDFbEP7tgNzgSxCi5B7+B36TzNEUDf5NSzBucRShEfJzPzilyJSDdwu7wPIsIzwLNCwLriEvN+9XnSt2jC/gLdh4IueEeIFdH0y2BCniaVyNsAiJdV0dPTQGCX1eGdbpbEavdtuRwGKmFkMp30JZByLVexHkYM+cOwkqqdHHS/giS+zA0at6mmoZRft6bQn0FLscm+MtsDMeCHr7B6YbfNE7D6nMkTzxReVxwFUtR10DHeduriQejcuO+7GvFIwn2ZvHeSUxB/s7/grkEhLhgqNwzZPM0nkN8F7RQcmV0++YUaEVWjSKD7gCqj4EjyWyiXPhxGGbijZyLIfwP1wiUFwmus1NcWc/gPOrHhRsR3kdxGJo+A8mZdHroRMdvhXiIJ21hgIiQEj9hvcX6HDUngEVEM/q4u714gyVMjQpJKCNXkpxpdEv1oFJSQeMq4JpQSttFsVsInIUklrJQApvIgsKky8sfOQjwUpIQ6ePS6iggfSFIJFcMq4QEGKKLVQtM8wk1s52TgkIEQDI9HB5oBIwiv/N5EFBcftGV0ZFtoCorKSJj0y/oOPFWvt/LFs2L8VNvhQaS7nBP49xtgRGxvf95VxFZKQYp+yPZAHRxCWHn1ZuOfwAiDAtNc918EChIJSknlDYi0mIlXqPS36lvfPgJGCTUQCc98FEZZCRXvKKJleq0hR53dRPI2cuV5WBbH1lmCh28xjb87l4u/kTsqvE8rAF6xX85//jpIjkezAo/iD/L0/TkwStPrCmJipiQmeeSdznbJ1SzxUwjOZJz05COHt5J2OrI6YTX/mQVB2fXZQ3fwqvEi1U7jUUww9hUn81B3JiffAsqjEJzJ1LIx7uuQHHuAJHsb5DkiI44ZkpqmjRDC1OC6qT6+CYQ8W6CYonSAbZoPsElwHrdjN6fthohUND/dRXOFJ58LFCa57rCjzebXL8KCr1DrTi7X5Os559UQDt/FCq+2t3OtZwQUaiuWjEuKV+JOnRi7MS5mm6490O4haLYbjeHtq06aannyZa5bVl8kuDNjiuYgh3g2763gTr0TAuAOvsjJ4SP9v9MvVoYpE8j14503b3xj+VH7hkvvW2HLhwMzknCT/StxM7uZXD8R5ch30WrmFfNVEK5wfYsVIeWXg2TZz7Wy2Ji6TquEiqVTCtyRzfyvr0AVpDvdzwQqIUC3LYyzg5RQUVKrUBQnsftI33kIq6CLrOGSLA8+H6SEnH5DsdG4JsyuTDFnbBCSLuR6FvRZUmD/+MGzRRklXVrMpa71U8LKM4iWafNRIzznYzcZN+qUULG/cfwuJ2eyq67PjRQGAbxdx0/5vE6IK/MlZwl+AyLAo9wtXLvFEBPeefmiVoCgxW4Qi3k7yYaQ8M7Dq+CiWgDotiEnpV3v/GFTUC7V/I5997LZbjC+BhEoNJnfZ0VboornL0UfTW90PgjxWFlsMjfpBKx2ms59db5GhKQU5wblMxhui7XcT2vDyIp0yjuH/6Z8JRC6nEbj7yEGdoN5NxFF3r6Cu+hw7lZdw/SkHLGaRzeCqGVaijt4FNK+/ULgpYO/y3bQ6aCznxCfLjWmYrlMio3md7irb9XJiLT5KYhON/ddS5AQGXChNp3k7eUm/AnEgGfHNdw4TwTJcZvDOToB8kSkN3zIA8BYBxHJmO7Jqmf90jmhpXcFRjaID8Kj0MPsJ9vuKyBhiNtESnceaCBP/h1UgS3NdfxW7ffNX6L2+cPBo9htYeTQ8z6hSSYBZqTZcJgnfDVIgrch4QT/ZHrGacKfQxUUG/HH7KANXAgNKJQQ2v1aiSL6KDsIFN69/okwWRnc/b/i1eGp4M8up8l4BKphKe4V5B+0wK/lSRABVuptpSZ8MZSswBP886F/4T78JVSB/UtQ7i7twMGLFZztX0DaAgkgAJ+MIs+rxOP90+DViq1XLQg/0iWXIH3swOdWpmafMtG2ylq5SiS4/nvcRJPhHhoHIeEVcLgXYw2ZXPLpfskCcTNUyxqUbAf/u05ErZr9vfAevQYJIDGa4vAI6uvE5Qol46d0TG2ZBIiBuyboHzggkKqaNQ7Cnolf6dKz+8PvmJA0Q9l09e/vVXn6mkGeFNshAaQkbd1ExQTwAw0HkoANqCji/Ib6jy4IHiQA1oMb6QdS004SItXPD/RA7wEwI+x4SDdU/aSoN2AUYNNCq0uCAHs1P54MSRSC8KhIPwDSGe3HQhLsc3VBq1x3Y6DPDcXbfrK8qxArzGrIMw393rUloRcSZn/Jf4GkQCwfCUmAOEWXzItmucsvkVeWyezxCjwlkjyQr3HML8eMoG24MEgD5+rSyYSBTnKSvoY/D9/zIQkknqFJLbzJCxpImuXILxy+4ZeMYPwxJAAPeCfq0tkWNfzD4RHmqbMgUA0tveww1zbwUAxTZ5ekC+idCVXCq+DzfBMJHKdnYAAE+0Of8RPn+XJGppviRLAPzujj/on0PIwY0r+9CRbygkZAFVhdNI3b6I90Muzbpaf9Ennb57BCVl4N1RTCrGtUf6L8Bh3QrrBQwF9CFWTuKs5gM3Sh/wNo65CQfYF6D4ILseMHFVaHu4j/+K5eed/2WRghSMV6+j/46OwU71KoAt6WuSFIRqRlWbvRzT6m6ypbQDFQYUU8bV0PEenbO9ZFzOCZmby3EuLA9oZIpf9JK4MwJCbRwTe3cF187UQkuUwdnIIYqHbiWUO7Fca7G9+FEQIN85916YS4Xp3agxjUddJJvPhsCpITvU0Wux7oP/0E1KjIQ/fj2c7CVIiA1U3HgUmb2aYbDzGQhNqG56H+tnS7++cQkUyH10YIWnsF3NK/Dvmu4chenp8e1f4OxP1Wno6HCBzWxe2bpqDjBj12Y+o/YISwl+BmtuF0TuvJZNAj9d0UaVFmtRene1KGKnff3C/hm1ophONJZrao8HgIQaqdToOyt4W1OHbwaKksWrlxdMZ5ig3cTaxYfwEhqL+Pjsjk3Yd5igs4vEX32kvrdgybQkLfTgDHsNTWrIocCoE6DmtL+RRnPFMnxy9d0HOrhv2zrbp0NmVOLpfl01ZLcRqEINNWPgcw/VTY4wZ9fik2RjNT5E9CbSMh3M3eyW47Z24eEHTQSilLuPP5O7YJ8TKA0D4v/3jEvKfCycJEb7zAfrOvpTLpx3qvGLgHPb6F5rhp+Wm2g1axEuodwsSWnluapTuzksnL73GnfRIC4OnsQbYr25xe2ALX4Ls+NG5r61j3DBZYxA7sy3jGqQso0yt2lrc8L8cBZkFQPKIKXHaWpR6DsHRQvUXeS9xtQe4x5cft9ki0l3n7dkCKOvtcD7ww9VYh4vkQlv7nmit2Skr+DEIuLNQGPXesWsnZ/OF9hJWVUZwjnv6n+LhiVj2xwtOHISyIL3F5dvNLlSVSB9wp/LkSFcYVEEFTOWIp5csRTI4yd8rzXBZuL5gggY6PcqbEtN05+1ZkhriOAgNjDfExe3Fw1Et/xrWVP+EJEVp5WQf2ct1U2cr835EkWQfiHDUZfMA+2+6tYvvpDhhdtMdJMx00A8l7Ju7Z3LCoEcxpEBeHkbXy7he4PCokfmTPdpC8mV+M5uGSgkdEMdtZhv8HEeFZSM1AzTCaDD5OWlRxh1WGMyWNCnBwhXER/7MAI8eLWaFMinCwWXIvv/nXwcjS7qeEIXg1jhIqeFC4kUfvNhhlhjgq7aUpdVjpJjiEcJfgVhelipv8NSSNpCdTKfGRqBcuFRuM9expvLYSv54w6jin3SCWQkxIQlXxg06jWEYgbodRZFiPuXorUNDnlA0ACaDOOwgQy7mJX4aYmAVXKWHoYwFhIQNf7l0EeyAGxSZjPYHxyaROLnI7OazW1xVzxtVxos8P0OOURQdUBZKTQ+WNuBEgmSATrtxvePNEbSIM65Hw3bpRB82xLD7EjRx44Fz/fHxIkJhbyOFdbLYvgDDHKAdh3VmcDtnME5D0rQpQifRpsDrl3crRDTFQp+t4P/YM/nV10xnREzwKneI0Ro9o78cOdtef3bd/XD08IN3Ei56Z/FL8AOLC7gpWgg6zKOaUGs2HyBZ/CsMoY6jGT+edOQLNVZyhCikPjn7BSjRuOyvgJlbAbf2Tsp00lXcJ1nIBv3jgOEDg3TcV53hJ+SUxkrM4Cjz83O7kjFA+ST8qO0kpdzlvRy3kms0M8cy3BMG3IS3uKV6Bz0AE+i9WqG+m2GA6Yt3+5Zi8+QKVfptHnsc7JLxHjxQmmup11oM8mqKV6zYggKTioUl7t/AK8bOVL8JcSzeYVHvpNBCpEwzyVMSJcnBnePPXYd/a81LScwbIZws7U9uCrldTOxDkwQIwvHOdnPnpoOdmumgWuPK/gtwm3Ckuy+zl9zAd3Z1Ei9RCBBJAvbzsQzmRXVynsIvjD9kYHc8bB8rN8RIPvs+ZKe/H++emXoRTsAwxOKCIt3I9N2dNcX+1l4qGhgtvtbnzKWXORSlP5c8zWJHUFTUFJPxvHv5ekBlja2kR/iwoK+XQBwMuQPQ+OLLuh4QxO8pnGKSmwiGOabbxZCsK+H5xn/nsQQeyutm2ZHhnCoHKNrk8KH91/006LWYNdorXGHneU4qosDrKH+OpTzld0+qzulXBkmJ1UKxeppU+gEbl4iRtAAePqLfyFP1XUGNUec8pooIdyldw0bukgNWlJerm2JDk906ycNxjbJvodmr2WYaYOmpTXY0KVQU8/r5gO66bR8WPR1JCRW7iHts0VECs7uDUeNv1Am3WGsnynlREhd0YbR/1HZT9R7JBJ8IukDi3KtSogvesIlYDb509ob8Gg+bF9SvWiMeYVEQF+1l9o7RZAw/P5mEK1Bg1xqwiCkNoL+90oZzMsdUaoRi7I2IZtPe5iITOKtcIx5hVRPZC6uvuUjKb/TVCMWYVET1XawOiYWpvQKiRLGNWEaUU+vM5aO+AGqPG2J2aBehO871h57LJ3DpWIxQmjEEOHIT3PdtMI3irQo3hGXMj4sRvq/u5RbdOBqX7HagxqowpRVRHQZ0iPQX6SO89dsmu7hriGpEZM1OzivKWZakuUgqK8v5HWDGhFo84yoyZEdHOwk5CDLjaDd+AcilaRE+NRBg7U/Ml6DmA6mTiNj8RBLwm7v+0u0Z1jK3FSgP2OoZQkTVbh6QRrC3m8AGo8Xth7PkR1f8yLW1c1v8sMrtrvmfvFIfUpQJjjTEbc1e5U0d6TwLio7YnlsHSeKfpatSomsolmTUOCX4Hn8i63ZAyQc4AAAAASUVORK5CYII=", - "ICON": "logoSquare.png", - "FRONTEND_ENDPOINT": "https://app-builder-core-git-preprod-agoraio.vercel.app", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "WEB", - "SCREEN_SHARING": true, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform", + "APP_ID": "2ab137bc27094a03b48afada20014df4", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, "ENABLE_APPLE_OAUTH": false, @@ -26,6 +9,11 @@ "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", + "PROJECT_ID": "3c13e13f4dfa39d6a3af", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", "BUCKET_NAME": "", "BUCKET_ACCESS_KEY": "", "BUCKET_ACCESS_SECRET": "", @@ -38,19 +26,40 @@ "PSTN_EMAIL": "", "PSTN_ACCOUNT": "", "PSTN_PASSWORD": "", - "RECORDING_REGION": 0, + "RECORDING_REGION": 3, "GEO_FENCING": true, - "GEO_FENCING_INCLUDE_AREA": "GLOBAL", - "GEO_FENCING_EXCLUDE_AREA": "CHINA", "LOG_ENABLED": true, "EVENT_MODE": false, "RAISE_HAND": false, "AUDIO_ROOM": false, + "PRODUCT_ID": "breakoutroom", + "APP_NAME": "BreakoutRoom", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "", + "BACKEND_ENDPOINT": "https://managedservices-staging.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1572645", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", "BG": "", "PRIMARY_FONT_COLOR": "#363636", "SECONDARY_FONT_COLOR": "#FFFFFF", - "PROFILE": "720p_3", + "SENTRY_DSN": "", + "PROFILE": "480p_8", "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", @@ -76,28 +85,21 @@ "ICON_BG_COLOR": "#242529", "TOOLBAR_COLOR": "#111111", "ACTIVE_SPEAKER": true, - "ENABLE_IDP_AUTH": false, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, + "WHITEBOARD_APPIDENTIFIER": "", + "WHITEBOARD_REGION": "us-sv", "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, - "ENABLE_WHITEBOARD": true, + "ENABLE_WHITEBOARD": false, "ENABLE_WHITEBOARD_FILE_UPLOAD": false, + "ENABLE_CHAT_NOTIFICATION": true, + "ENABLE_CHAT_OPTION": true, "ENABLE_WAITING_ROOM": false, - "WHITEBOARD_APPIDENTIFIER": "EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "41754367", - "CHAT_APP_NAME": "1042822", - "CHAT_URL": "https://a41.chat.agora.io", - "ENABLE_NOISE_CANCELLATION_BY_DEFAULT": true, - "DISABLE_LANDSCAPE_MODE": false, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false, - "ENABLE_CONVERSATIONAL_AI": false, - "CUSTOMIZE_AGENT": false, - "ENABLE_TEXT_TRACKS": false + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, + "SDK_CODEC": "vp8", + "ENABLE_BREAKOUT_ROOM": true } diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 0cc7297ad..add45850c 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -4,7 +4,6 @@ import React, { useEffect, useState, useCallback, - useMemo, useRef, } from 'react'; import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; @@ -12,9 +11,8 @@ import {createHook} from 'customization-implementation'; import {randomNameGenerator} from '../../../utils'; import StorageContext from '../../StorageContext'; import getUniqueID from '../../../utils/getUniqueID'; -import {logger} from '../../../logger/AppBuilderLogger'; +import {logger, LogSource} from '../../../logger/AppBuilderLogger'; import {useRoomInfo} from 'customization-api'; -import {useLocation} from '../../Router'; import { BreakoutGroupActionTypes, BreakoutGroup, @@ -33,6 +31,23 @@ import {BreakoutRoomSyncStateEventPayload} from '../state/types'; import {IconsInterface} from '../../../atoms/CustomIcon'; import Toast from '../../../../react-native-toast-message'; import useBreakoutRoomExit from '../hooks/useBreakoutRoomExit'; +import {useDebouncedCallback} from '../../../utils/useDebouncedCallback'; + +const BREAKOUT_LOCK_TIMEOUT_MS = 5000; +const HOST_OPERATION_LOCK_TIMEOUT_MS = 10000; // Emergency timeout for network failures only + +const HOST_BROADCASTED_OPERATIONS = [ + BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, + BreakoutGroupActionTypes.CREATE_GROUP, + BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, + BreakoutGroupActionTypes.CLOSE_GROUP, + BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, + BreakoutGroupActionTypes.RENAME_GROUP, +] as const; const getSanitizedPayload = (payload: BreakoutGroup[]) => { return payload.map(({id, ...rest}) => { @@ -43,6 +58,47 @@ const getSanitizedPayload = (payload: BreakoutGroup[]) => { }); }; +const validateRollbackState = (state: BreakoutRoomState): boolean => { + return ( + Array.isArray(state.breakoutGroups) && + typeof state.breakoutSessionId === 'string' && + typeof state.canUserSwitchRoom === 'boolean' && + state.breakoutGroups.every( + group => + typeof group.id === 'string' && + typeof group.name === 'string' && + Array.isArray(group.participants?.hosts) && + Array.isArray(group.participants?.attendees), + ) + ); +}; + +export const deepCloneBreakoutGroups = ( + groups: BreakoutGroup[] = [], +): BreakoutGroup[] => + groups.map(group => ({ + ...group, + participants: { + hosts: [...(group.participants?.hosts ?? [])], + attendees: [...(group.participants?.attendees ?? [])], + }, + })); + +const needsDeepCloning = (action: BreakoutRoomAction): boolean => { + const CLONING_REQUIRED_ACTIONS = [ + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, + BreakoutGroupActionTypes.EXIT_GROUP, + BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.CLOSE_GROUP, // Safe to include + BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, // Safe to include + BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, + BreakoutGroupActionTypes.SYNC_STATE, + ]; + + return CLONING_REQUIRED_ACTIONS.includes(action.type as any); +}; export interface MemberDropdownOption { type: 'move-to-main' | 'move-to-room' | 'make-presenter'; icon: keyof IconsInterface; @@ -62,28 +118,43 @@ interface BreakoutRoomPermissions { canRaiseHands: boolean; canSeeRaisedHands: boolean; // Room management (host only) + canHostManageMainRoom: boolean; canAssignParticipants: boolean; canCreateRooms: boolean; canMoveUsers: boolean; canCloseRooms: boolean; canMakePresenter: boolean; } +const defaulBreakoutRoomPermission: BreakoutRoomPermissions = { + canJoinRoom: false, + canExitRoom: false, + canSwitchBetweenRooms: false, // Media controls + canScreenshare: true, + canRaiseHands: false, + canSeeRaisedHands: false, + // Room management (host only) + canHostManageMainRoom: false, + canAssignParticipants: false, + canCreateRooms: false, + canMoveUsers: false, + canCloseRooms: false, + canMakePresenter: false, +}; interface BreakoutRoomContextValue { mainChannelId: string; breakoutSessionId: BreakoutRoomState['breakoutSessionId']; breakoutGroups: BreakoutRoomState['breakoutGroups']; assignmentStrategy: RoomAssignmentStrategy; - setStrategy: (strategy: RoomAssignmentStrategy) => void; canUserSwitchRoom: boolean; - toggleSwitchRooms: (value: boolean) => void; - unsassignedParticipants: {uid: UidType; user: ContentInterface}[]; + toggleRoomSwitchingAllowed: (value: boolean) => void; + unassignedParticipants: {uid: UidType; user: ContentInterface}[]; manualAssignments: ManualParticipantAssignment[]; setManualAssignments: (assignments: ManualParticipantAssignment[]) => void; clearManualAssignments: () => void; createBreakoutRoomGroup: (name?: string) => void; isUserInRoom: (room?: BreakoutGroup) => boolean; - joinRoom: (roomId: string) => void; - exitRoom: (roomId?: string) => Promise; + joinRoom: (roomId: string, permissionAtCallTime?: boolean) => void; + exitRoom: (roomId?: string, permissionAtCallTime?: boolean) => Promise; closeRoom: (roomId: string) => void; closeAllRooms: () => void; updateRoomName: (newRoomName: string, roomId: string) => void; @@ -106,21 +177,36 @@ interface BreakoutRoomContextValue { handleBreakoutRoomSyncState: ( data: BreakoutRoomSyncStateEventPayload['data']['data'], ) => void; + // Multi-host coordination handlers + handleHostOperationStart: ( + operationName: string, + hostUid: UidType, + hostName: string, + ) => void; + handleHostOperationEnd: ( + operationName: string, + hostUid: UidType, + hostName: string, + ) => void; permissions: BreakoutRoomPermissions; + // Loading states + isBreakoutUpdateInFlight: boolean; + // Multi-host coordination + isAnotherHostOperating: boolean; + currentOperatingHostName?: string; } const BreakoutRoomContext = React.createContext({ mainChannelId: '', breakoutSessionId: undefined, - unsassignedParticipants: [], + unassignedParticipants: [], breakoutGroups: [], assignmentStrategy: RoomAssignmentStrategy.NO_ASSIGN, - setStrategy: () => {}, manualAssignments: [], setManualAssignments: () => {}, clearManualAssignments: () => {}, canUserSwitchRoom: false, - toggleSwitchRooms: () => {}, + toggleRoomSwitchingAllowed: () => {}, handleAssignParticipants: () => {}, createBreakoutRoomGroup: () => {}, isUserInRoom: () => false, @@ -142,7 +228,16 @@ const BreakoutRoomContext = React.createContext({ onRaiseHand: () => {}, clearAllRaisedHands: () => {}, handleBreakoutRoomSyncState: () => {}, - permissions: null, + // Multi-host coordination handlers + handleHostOperationStart: () => {}, + handleHostOperationEnd: () => {}, + // Provide a safe non-null default object + permissions: {...defaulBreakoutRoomPermission}, + // Loading states + isBreakoutUpdateInFlight: false, + // Multi-host coordination + isAnotherHostOperating: false, + currentOperatingHostName: undefined, }); const BreakoutRoomProvider = ({ @@ -157,28 +252,29 @@ const BreakoutRoomProvider = ({ const {store} = useContext(StorageContext); const {defaultContent, activeUids} = useContent(); const localUid = useLocalUid(); + const { + data: {isHost, roomId}, + } = useRoomInfo(); + const breakoutRoomExit = useBreakoutRoomExit(handleLeaveBreakout); const [state, baseDispatch] = useReducer( breakoutRoomReducer, initialBreakoutRoomState, ); - const { - data: {isHost, roomId}, - } = useRoomInfo(); + const [isBreakoutUpdateInFlight, setBreakoutUpdateInFlight] = useState(false); - const location = useLocation(); - const isInBreakoutRoute = location.pathname.includes('breakout'); + // Permissions: + const [permissions, setPermissions] = useState({ + ...defaulBreakoutRoomPermission, + }); - const breakoutRoomExit = useBreakoutRoomExit(handleLeaveBreakout); - // Sync state - // Join Room - const [selfJoinRoomId, setSelfJoinRoomId] = useState(null); + // Multi-host coordination state + const [isAnotherHostOperating, setIsAnotherHostOperating] = useState(false); + const [currentOperatingHostName, setCurrentOperatingHostName] = useState< + string | undefined + >(undefined); - // Enhanced dispatch that tracks user actions - const [lastAction, setLastAction] = useState(null); - const dispatch = useCallback((action: BreakoutRoomAction) => { - baseDispatch(action); - setLastAction(action); - }, []); + // Join Room pending intent + const [selfJoinRoomId, setSelfJoinRoomId] = useState(null); // Presenter const [canIPresent, setICanPresent] = useState(false); @@ -194,9 +290,285 @@ const BreakoutRoomProvider = ({ // Polling control const [isPollingPaused, setIsPollingPaused] = useState(false); - // Note: No initial data loading needed here as host polls every 2s and sends RTM sync events - // All participants (including attendees) receive current data via RTM sync events - // BreakoutRoomView handles data loading only when panel is opened + // Refs to avoid stale closures in async callbacks + const stateRef = useRef(state); + const prevStateRef = useRef(state); + const isHostRef = useRef(isHost); + const defaultContentRef = useRef(defaultContent); + const isMountedRef = useRef(true); + // Concurrent action protection - track users being moved + const usersBeingMovedRef = useRef>(new Set()); + // Enhanced dispatch that tracks user actions + const [lastAction, setLastAction] = useState(null); + + const dispatch = useCallback((action: BreakoutRoomAction) => { + if (needsDeepCloning(action)) { + // Only deep clone when necessary + prevStateRef.current = { + ...stateRef.current, + breakoutGroups: deepCloneBreakoutGroups( + stateRef.current.breakoutGroups, + ), + }; + } else { + // Shallow copy for non-participant actions + prevStateRef.current = { + ...stateRef.current, + breakoutGroups: [...stateRef.current.breakoutGroups], // Shallow copy + }; + } + baseDispatch(action); + setLastAction(action); + }, []); + + useEffect(() => { + stateRef.current = state; + }, [state]); + useEffect(() => { + isHostRef.current = isHost; + }, [isHost]); + useEffect(() => { + defaultContentRef.current = defaultContent; + }, [defaultContent]); + useEffect(() => { + return () => { + isMountedRef.current = false; + }; + }, []); + + // Timeouts + const timeoutsRef = useRef>>(new Set()); + // Track host operation timeout for manual clearing + const hostOperationTimeoutRef = useRef | null>( + null, + ); + + const safeSetTimeout = useCallback((fn: () => void, delay: number) => { + const id = setTimeout(() => { + fn(); + timeoutsRef.current.delete(id); // cleanup after execution + }, delay); + + timeoutsRef.current.add(id); + return id; + }, []); + const safeClearTimeout = useCallback((id: ReturnType) => { + clearTimeout(id); + timeoutsRef.current.delete(id); + }, []); + // Clear all timeouts + useEffect(() => { + const snapshot = timeoutsRef.current; + return () => { + snapshot.forEach(timeoutId => clearTimeout(timeoutId)); + snapshot.clear(); + }; + }, []); + + // Toast duplication + const toastDedupeRef = useRef>(new Set()); + + const showDeduplicatedToast = useCallback((key: string, toastConfig: any) => { + if (toastDedupeRef.current.has(key)) { + return; + } + + toastDedupeRef.current.add(key); + Toast.show(toastConfig); + + safeSetTimeout(() => { + toastDedupeRef.current.delete(key); + }, toastConfig.visibilityTime || 3000); + }, []); + + // Multi-host coordination functions + const broadcastHostOperationStart = useCallback( + (operationName: string) => { + if (!isHostRef.current) { + return; + } + + const hostName = defaultContentRef.current[localUid]?.name || 'Host'; + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Broadcasting host operation start', + {operation: operationName, hostName, hostUid: localUid}, + ); + + events.send( + BreakoutRoomEventNames.BREAKOUT_ROOM_HOST_OPERATION_START, + JSON.stringify({ + operationName, + hostUid: localUid, + hostName, + timestamp: Date.now(), + }), + ); + }, + [localUid], + ); + + const broadcastHostOperationEnd = useCallback( + (operationName: string) => { + if (!isHostRef.current) { + return; + } + + const hostName = defaultContentRef.current[localUid]?.name || 'Host'; + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Broadcasting host operation end', + {operation: operationName, hostName, hostUid: localUid}, + ); + + events.send( + BreakoutRoomEventNames.BREAKOUT_ROOM_HOST_OPERATION_END, + JSON.stringify({ + operationName, + hostUid: localUid, + hostName, + timestamp: Date.now(), + }), + ); + }, + [localUid], + ); + + // Common operation lock for API-triggering actions with multi-host coordination + const acquireOperationLock = useCallback( + (operationName: string, showToast = true): boolean => { + // Check if another host is operating + console.log('supriya-state-sync acquiring lock step 1'); + if (isAnotherHostOperating) { + console.log('supriya-state-sync isAnotherHostOperating is true'); + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Operation blocked - another host is operating', + { + blockedOperation: operationName, + operatingHost: currentOperatingHostName, + }, + ); + + if (showToast) { + showDeduplicatedToast(`operation-blocked-host-${operationName}`, { + type: 'info', + text1: `${ + currentOperatingHostName || 'Another host' + } is currently managing breakout rooms`, + text2: 'Please wait for them to finish', + visibilityTime: 3000, + }); + } + return false; + } + + // Check if API call is in progress + if (isBreakoutUpdateInFlight) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Operation blocked - API call in progress', + { + blockedOperation: operationName, + currentlyInFlight: isBreakoutUpdateInFlight, + }, + ); + + if (showToast) { + showDeduplicatedToast(`operation-blocked-${operationName}`, { + type: 'info', + text1: 'Please wait for current operation to complete', + visibilityTime: 3000, + }); + } + return false; + } + + // Broadcast that this host is starting an operation + console.log( + 'supriya-state-sync broadcasting host operation start', + operationName, + ); + broadcastHostOperationStart(operationName); + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `Operation lock acquired for ${operationName}`, + {operation: operationName}, + ); + return true; + }, + [ + isBreakoutUpdateInFlight, + isAnotherHostOperating, + currentOperatingHostName, + showDeduplicatedToast, + broadcastHostOperationStart, + ], + ); + + // Individual user lock: so that same user is not moved from two different actions + const acquireUserLock = (uid: UidType, operation: string): boolean => { + if (usersBeingMovedRef.current.has(uid)) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Concurrent action blocked - user already being moved', + { + uid, + operation, + currentlyBeingMoved: Array.from(usersBeingMovedRef.current), + }, + ); + return false; + } + + usersBeingMovedRef.current.add(uid); + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `User lock acquired for ${operation}`, + {uid, operation}, + ); + + // 🛡️ Auto-release lock after timeout to prevent deadlocks + safeSetTimeout(() => { + if (usersBeingMovedRef.current.has(uid)) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Auto-releasing user lock after timeout', + {uid, operation, timeoutMs: BREAKOUT_LOCK_TIMEOUT_MS}, + ); + usersBeingMovedRef.current.delete(uid); + } + }, BREAKOUT_LOCK_TIMEOUT_MS); + + return true; + }; + + const releaseUserLock = (uid: UidType, operation: string): void => { + const wasLocked = usersBeingMovedRef.current.has(uid); + usersBeingMovedRef.current.delete(uid); + + if (wasLocked) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `User lock released for ${operation}`, + {uid, operation}, + ); + } + }; // Update unassigned participants whenever defaultContent or activeUids change useEffect(() => { @@ -205,9 +577,12 @@ const BreakoutRoomProvider = ({ // 1. Custom content (not type 'rtc') // 2. Screenshare UIDs // 3. Offline users + if (!stateRef?.current?.breakoutSessionId) { + return; + } const filteredParticipants = activeUids .filter(uid => { - const user = defaultContent[uid]; + const user = defaultContentRef.current[uid]; if (!user) { return false; } @@ -219,6 +594,10 @@ const BreakoutRoomProvider = ({ if (user.offline) { return false; } + // Exclude hosts + if (user?.isHost) { + return false; + } // Exclude screenshare UIDs (they typically have a parentUid) if (user.parentUid) { return false; @@ -231,49 +610,97 @@ const BreakoutRoomProvider = ({ }) .map(uid => ({ uid, - user: defaultContent[uid], + user: defaultContentRef.current[uid], })); - // Sort participants with local user first - const sortedParticipants = filteredParticipants.sort((a, b) => { - if (a.uid === localUid) { - return -1; - } - if (b.uid === localUid) { - return 1; - } - return 0; - }); + // // Sort participants with local user first + // const sortedParticipants = filteredParticipants.sort((a, b) => { + // if (a.uid === localUid) { + // return -1; + // } + // if (b.uid === localUid) { + // return 1; + // } + // return 0; + // }); dispatch({ type: BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS, payload: { - unassignedParticipants: sortedParticipants, + unassignedParticipants: filteredParticipants, }, }); - }, [defaultContent, activeUids, localUid, dispatch]); + }, [activeUids, localUid, dispatch, state.breakoutSessionId]); + // Check if there is already an active breakout session + // We can call this to trigger sync events const checkIfBreakoutRoomSessionExistsAPI = async (): Promise => { + console.log( + 'supriya-state-sync calling checkIfBreakoutRoomSessionExistsAPI', + ); + const startTime = Date.now(); + const requestId = getUniqueID(); + const url = `${ + $config.BACKEND_ENDPOINT + }/v1/channel/breakout-room?passphrase=${ + isHostRef.current ? roomId.host : roomId.attendee + }`; + + // Log internals for breakout room lifecycle + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Checking active session', + { + isHost: isHostRef.current, + sessionId: stateRef.current.breakoutSessionId, + }, + ); + try { - const requestId = getUniqueID(); - const response = await fetch( - `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room?passphrase=${ - isHost ? roomId.host : roomId.attendee - }`, + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + authorization: store.token ? `Bearer ${store.token}` : '', + 'X-Request-Id': requestId, + 'X-Session-Id': logger.getSessionId(), + }, + }); + + // 🛡️ Guard against component unmount after fetch + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Check session API cancelled - component unmounted', + {requestId}, + ); + return false; + } + + const latency = Date.now() - startTime; + + // Log network request + logger.log( + LogSource.NetworkRest, + 'breakout-room', + 'GET breakout-room session', { + url, method: 'GET', - headers: { - 'Content-Type': 'application/json', - authorization: store.token ? `Bearer ${store.token}` : '', - 'X-Request-Id': requestId, - 'X-Session-Id': logger.getSessionId(), - }, + status: response.status, + latency, + requestId, }, ); if (response.status === 204) { - // No active breakout session - console.log('No active breakout room session (204)'); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'No active session found', + ); return false; } @@ -283,13 +710,38 @@ const BreakoutRoomProvider = ({ const data = await response.json(); + // 🛡️ Guard against component unmount after JSON parsing + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Session sync cancelled - component unmounted after parsing', + {requestId}, + ); + return false; + } + if (data?.session_id) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Session synced successfully', + { + sessionId: data.session_id, + roomCount: data?.breakout_room?.length || 0, + assignmentType: data?.assignment_type, + switchRoom: data?.switch_room, + }, + ); + dispatch({ type: BreakoutGroupActionTypes.SYNC_STATE, payload: { sessionId: data.session_id, rooms: data?.breakout_room || [], - switchRoom: data.switch_room || false, + assignmentStrategy: + data?.assignment_type || RoomAssignmentStrategy.NO_ASSIGN, + switchRoom: data?.switch_room ?? true, }, }); return true; @@ -297,97 +749,170 @@ const BreakoutRoomProvider = ({ return false; } catch (error) { - console.error('Error checking active breakout room:', error); + const latency = Date.now() - startTime; + logger.log(LogSource.NetworkRest, 'breakout-room', 'API call failed', { + url, + method: 'GET', + error: error.message, + latency, + requestId, + }); return false; } }; // Polling for sync event const pollBreakoutGetAPI = useCallback(async () => { - if (isHost && state.breakoutSessionId) { - console.log( - 'supriya-polling-check Polling: Calling breakout session to sync events', - ); + if (isHostRef.current && stateRef.current.breakoutSessionId) { await checkIfBreakoutRoomSessionExistsAPI(); } - }, [isHost, state.breakoutSessionId]); + }, []); // Automatic interval management with cleanup only host will poll - useEffect(() => { - console.log( - 'supriya-polling-check for automatic interval', - isHost, - state.breakoutSessionId, - isPollingPaused, - ); - if ( - isHost && - !isPollingPaused && - (state.breakoutSessionId || isInBreakoutRoute) - ) { - console.log('supriya inside automatic interval'); - - // Check every 2 seconds - const interval = setInterval(pollBreakoutGetAPI, 2000); - // React will automatically call this cleanup function - return () => clearInterval(interval); - } - }, [ - isHost, - state.breakoutSessionId, - isPollingPaused, - isInBreakoutRoute, - pollBreakoutGetAPI, - ]); + // useEffect(() => { + // if ( + // isHostRef.current && + // !isPollingPaused && + // (stateRef.current.breakoutSessionId || isInBreakoutRoute) + // ) { + // const interval = setInterval(pollBreakoutGetAPI, 2000); + // return () => clearInterval(interval); + // } + // }, [isPollingPaused, isInBreakoutRoute, pollBreakoutGetAPI]); const upsertBreakoutRoomAPI = useCallback( - async (type: 'START' | 'UPDATE' = 'START') => { + async (type: 'START' | 'UPDATE' = 'START', retryCount = 0) => { + type UpsertPayload = { + passphrase: string; + switch_room: boolean; + session_id: string; + assignment_type: RoomAssignmentStrategy; + breakout_room: ReturnType; + join_room_id?: string; + }; + const startReqTs = Date.now(); const requestId = getUniqueID(); + const url = `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room`; - // Pause polling during API call to prevent conflicts - setIsPollingPaused(true); + // Log internals for lifecycle + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `Upsert API called - ${type}`, + { + type, + isHost: isHostRef.current, + sessionId: stateRef.current.breakoutSessionId, + roomCount: stateRef.current.breakoutGroups.length, + assignmentStrategy: stateRef.current.assignmentStrategy, + canSwitchRoom: stateRef.current.canUserSwitchRoom, + selfJoinRoomId, + }, + ); try { - const payload = { - passphrase: roomId.host, - switch_room: state.canUserSwitchRoom, - session_id: state.breakoutSessionId || randomNameGenerator(6), + const sessionId = + stateRef.current.breakoutSessionId || randomNameGenerator(6); + + const payload: UpsertPayload = { + passphrase: isHostRef.current ? roomId.host : roomId.attendee, + switch_room: stateRef.current.canUserSwitchRoom, + session_id: sessionId, + assignment_type: stateRef.current.assignmentStrategy, breakout_room: type === 'START' ? getSanitizedPayload(initialBreakoutGroups) - : getSanitizedPayload(state.breakoutGroups), + : getSanitizedPayload(stateRef.current.breakoutGroups), }; - // Only add join_room_id if there's a pending join + // Only add join_room_id if attendee has called this api(during join room) if (selfJoinRoomId) { payload.join_room_id = selfJoinRoomId; } - const response = await fetch( - `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room`, + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + authorization: store.token ? `Bearer ${store.token}` : '', + 'X-Request-Id': requestId, + 'X-Session-Id': logger.getSessionId(), + }, + body: JSON.stringify(payload), + }); + + // Guard against component unmount after fetch + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Upsert API cancelled - component unmounted after fetch', + {type, requestId}, + ); + return; + } + + const endRequestTs = Date.now(); + const latency = endRequestTs - startReqTs; + + // Log network request + logger.log( + LogSource.NetworkRest, + 'breakout-room', + 'POST breakout-room upsert', { + url, method: 'POST', - headers: { - 'Content-Type': 'application/json', - authorization: store.token ? `Bearer ${store.token}` : '', - 'X-Request-Id': requestId, - 'X-Session-Id': logger.getSessionId(), - }, - body: JSON.stringify(payload), + status: response.status, + latency, + requestId, + type, + payloadSize: JSON.stringify(payload).length, }, ); - const endRequestTs = Date.now(); - const latency = endRequestTs - startReqTs; + if (!response.ok) { const msg = await response.text(); + + // 🛡️ Guard against component unmount after error text parsing + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Error text parsing cancelled - component unmounted', + {type, status: response.status, requestId}, + ); + return; + } + throw new Error(`Breakout room creation failed: ${msg}`); } else { const data = await response.json(); - if (selfJoinRoomId) { - setSelfJoinRoomId(null); + // 🛡️ Guard against component unmount after JSON parsing + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Upsert API success cancelled - component unmounted after parsing', + {type, requestId}, + ); + return; } + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `Upsert API success - ${type}`, + { + type, + newSessionId: data?.session_id, + roomsUpdated: !!data?.breakout_room, + latency, + }, + ); + if (type === 'START' && data?.session_id) { dispatch({ type: BreakoutGroupActionTypes.SET_SESSION_ID, @@ -400,33 +925,74 @@ const BreakoutRoomProvider = ({ payload: data.breakout_room, }); } - // API update completed successfully } } catch (err) { - console.log('debugging err', err); + const latency = Date.now() - startReqTs; + const maxRetries = 3; + const isRetriableError = + err.name === 'TypeError' || // Network errors + err.message.includes('fetch') || + err.message.includes('timeout') || + err.response?.status >= 500; // Server errors + + logger.log( + LogSource.NetworkRest, + 'breakout-room', + 'Upsert API failed', + { + url, + method: 'POST', + error: err.message, + latency, + requestId, + type, + retryCount, + isRetriableError, + willRetry: retryCount < maxRetries && isRetriableError, + }, + ); + + // 🛡️ Retry logic for network/server errors + if (retryCount < maxRetries && isRetriableError) { + const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 5000); // Exponential backoff, max 5s + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `Retrying upsert API in ${retryDelay}ms`, + {retryCount: retryCount + 1, maxRetries, type}, + ); + + // Don't clear polling/selfJoinRoomId on retry + safeSetTimeout(() => { + // 🛡️ Guard against component unmount during retry delay + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'API retry cancelled - component unmounted', + {type, retryCount: retryCount + 1}, + ); + return; + } + console.log('supriya-state-sync calling upsertBreakoutRoomAPI 941'); + upsertBreakoutRoomAPI(type, retryCount + 1); + }, retryDelay); + return; // Don't execute finally block on retry + } + + // 🛡️ Only clear state if we're not retrying + setSelfJoinRoomId(null); } finally { - // Resume polling after API call completes (success or error) - setIsPollingPaused(false); + // 🛡️ Only clear state on successful completion (not on retry) + if (retryCount === 0) { + setSelfJoinRoomId(null); + } } }, - [ - roomId.host, - state.breakoutSessionId, - state.breakoutGroups, - state.canUserSwitchRoom, - store.token, - dispatch, - selfJoinRoomId, - ], + [roomId.host, store.token, dispatch, selfJoinRoomId, roomId.attendee], ); - const setStrategy = (strategy: RoomAssignmentStrategy) => { - dispatch({ - type: BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY, - payload: {strategy}, - }); - }; - const setManualAssignments = useCallback( (assignments: ManualParticipantAssignment[]) => { dispatch({ @@ -443,7 +1009,31 @@ const BreakoutRoomProvider = ({ }); }, [dispatch]); - const toggleSwitchRooms = (value: boolean) => { + const toggleRoomSwitchingAllowed = (value: boolean) => { + console.log( + 'supriya-state-sync toggleRoomSwitchingAllowed value is', + value, + ); + if (!acquireOperationLock('SET_ALLOW_PEOPLE_TO_SWITCH_ROOM')) { + console.log('supriya-state-sync lock acquired'); + return; + } + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Switch rooms permission changed', + { + previousValue: stateRef.current.canUserSwitchRoom, + newValue: value, + isHost: isHostRef.current, + roomCount: stateRef.current.breakoutGroups.length, + }, + ); + console.log( + 'supriya-state-sync dispatching SET_ALLOW_PEOPLE_TO_SWITCH_ROOM', + ); + dispatch({ type: BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, payload: { @@ -453,14 +1043,37 @@ const BreakoutRoomProvider = ({ }; const createBreakoutRoomGroup = () => { + if (!acquireOperationLock('CREATE_GROUP')) return; + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Creating new breakout room', + { + currentRoomCount: stateRef.current.breakoutGroups.length, + isHost: isHostRef.current, + sessionId: stateRef.current.breakoutSessionId, + }, + ); + dispatch({ type: BreakoutGroupActionTypes.CREATE_GROUP, }); }; const handleAssignParticipants = (strategy: RoomAssignmentStrategy) => { - console.log('supriya participant assign strategy strategy: ', strategy); - if (strategy === RoomAssignmentStrategy.AUTO_ASSIGN) { + if (!acquireOperationLock(`ASSIGN_${strategy}`)) { + return; + } + + logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Assigning participants', { + strategy, + unassignedCount: stateRef.current.unassignedParticipants.length, + roomCount: stateRef.current.breakoutGroups.length, + isHost: isHostRef.current, + }); + + if (strategy === RoomAssignmentStrategy.AUTO_ASSIGN) { dispatch({ type: BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, }); @@ -472,70 +1085,172 @@ const BreakoutRoomProvider = ({ } if (strategy === RoomAssignmentStrategy.NO_ASSIGN) { dispatch({ - type: BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, - payload: { - canUserSwitchRoom: true, - }, + type: BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, }); } }; const moveUserToMainRoom = (user: ContentInterface) => { try { - // Find user's current breakout group - const currentGroup = state.breakoutGroups.find( + if (!user) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Move to main room failed - no user provided', + ); + return; + } + + // 🛡️ Check for API operation conflicts first + if (!acquireOperationLock('MOVE_PARTICIPANT_TO_MAIN', false)) { + return; + } + + const operation = 'moveToMain'; + + // 🛡️ Check if user is already being moved by another action + if (!acquireUserLock(user.uid, operation)) { + return; // Action blocked due to concurrent operation + } + + // 🛡️ Use fresh state to avoid race conditions + const currentState = stateRef.current; + const currentGroup = currentState.breakoutGroups.find( group => group.participants.hosts.includes(user.uid) || group.participants.attendees.includes(user.uid), ); - // Dispatch action to remove user from breakout group + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Moving user to main room', + { + userId: user.uid, + userName: user.name, + fromGroupId: currentGroup?.id, + fromGroupName: currentGroup?.name, + }, + ); + if (currentGroup) { dispatch({ type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, payload: { - user: user, + user, fromGroupId: currentGroup.id, }, }); } - console.log(`User ${user.name} (${user.uid}) moved to main room`); + + // 🛡️ Release lock after successful dispatch + releaseUserLock(user.uid, operation); } catch (error) { - console.error('Error moving user to main room:', error); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Error moving user to main room', + { + userId: user.uid, + userName: user.name, + error: error.message, + }, + ); + // 🛡️ Always release lock on error + releaseUserLock(user.uid, 'moveToMain'); } }; const moveUserIntoGroup = (user: ContentInterface, toGroupId: string) => { - console.log('move user to another room', user, toGroupId); try { - // Find user's current breakout group - const currentGroup = state.breakoutGroups.find( + if (!user) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Move to group failed - no user provided', + {toGroupId}, + ); + return; + } + + // 🛡️ Check for API operation conflicts first + if (!acquireOperationLock('MOVE_PARTICIPANT_TO_GROUP', false)) { + return; + } + + const operation = `moveToGroup-${toGroupId}`; + + // 🛡️ Check if user is already being moved by another action + if (!acquireUserLock(user.uid, operation)) { + return; // Action blocked due to concurrent operation + } + + // 🛡️ Use fresh state to avoid race conditions + const currentState = stateRef.current; + const currentGroup = currentState.breakoutGroups.find( group => group.participants.hosts.includes(user.uid) || group.participants.attendees.includes(user.uid), ); - // Find target group - const targetGroup = state.breakoutGroups.find( + const targetGroup = currentState.breakoutGroups.find( group => group.id === toGroupId, ); + if (!targetGroup) { - console.error('Target group not found:', toGroupId); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Target group not found', + { + userId: user.uid, + userName: user.name, + toGroupId, + }, + ); + // 🛡️ Release lock if target group not found + releaseUserLock(user.uid, operation); return; } - // Dispatch action to move user between groups + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Moving user between groups', + { + userId: user.uid, + userName: user.name, + fromGroupId: currentGroup?.id, + fromGroupName: currentGroup?.name, + toGroupId, + toGroupName: targetGroup.name, + }, + ); + dispatch({ type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, payload: { - user: user, + user, fromGroupId: currentGroup?.id, toGroupId, }, }); - console.log( - `User ${user.name} (${user.uid}) moved to ${targetGroup.name}`, - ); + // 🛡️ Release lock after successful dispatch + releaseUserLock(user.uid, operation); } catch (error) { - console.error('Error moving user to breakout room:', error); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Error moving user to breakout room', + { + userId: user.uid, + userName: user.name, + toGroupId, + error: error.message, + }, + ); + // 🛡️ Always release lock on error + releaseUserLock(user.uid, `moveToGroup-${toGroupId}`); } }; @@ -550,79 +1265,262 @@ const BreakoutRoomProvider = ({ ); } else { // Check ALL rooms - is user in any room? - return state.breakoutGroups.some( + return stateRef.current.breakoutGroups.some( group => group.participants.hosts.includes(localUid) || group.participants.attendees.includes(localUid), ); } }, - [localUid, state.breakoutGroups], + [localUid], ); - // Function to get current room - const getCurrentRoom = useCallback((): BreakoutGroup => { - const userRoom = state.breakoutGroups.find( + const getCurrentRoom = useCallback((): BreakoutGroup | null => { + const userRoom = stateRef.current.breakoutGroups.find( group => group.participants.hosts.includes(localUid) || group.participants.attendees.includes(localUid), ); - return userRoom ? userRoom : null; - }, [localUid, state.breakoutGroups]); + return userRoom ?? null; + }, [localUid]); + + // Permissions + useEffect(() => { + const currentlyInRoom = isUserInRoom(); + const hasAvailableRooms = stateRef.current.breakoutGroups.length > 0; + const allowAttendeeSwitch = stateRef.current.canUserSwitchRoom; + + const nextPermissions: BreakoutRoomPermissions = { + canJoinRoom: + !currentlyInRoom && + hasAvailableRooms && + (isHostRef.current || allowAttendeeSwitch), + canExitRoom: currentlyInRoom, + canSwitchBetweenRooms: + currentlyInRoom && + hasAvailableRooms && + (isHostRef.current || allowAttendeeSwitch), + canScreenshare: currentlyInRoom ? canIPresent : true, + canRaiseHands: !isHostRef.current && !!stateRef.current.breakoutSessionId, + canSeeRaisedHands: isHostRef.current, + canAssignParticipants: isHostRef.current, + canHostManageMainRoom: isHostRef.current && !currentlyInRoom, + canCreateRooms: isHostRef.current, + canMoveUsers: isHostRef.current, + canCloseRooms: + isHostRef.current && + hasAvailableRooms && + !!stateRef.current.breakoutSessionId, + canMakePresenter: isHostRef.current, + }; + + setPermissions(nextPermissions); + }, [ + state.breakoutGroups, + state.canUserSwitchRoom, + state.breakoutSessionId, + isUserInRoom, + canIPresent, + ]); + + const joinRoom = ( + toRoomId: string, + permissionAtCallTime = permissions.canJoinRoom, + ) => { + // 🛡️ Use permission passed at call time to avoid race conditions + if (!permissionAtCallTime) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Join room blocked - no permission at call time', + { + toRoomId, + permissionAtCallTime, + currentPermission: permissions.canJoinRoom, + }, + ); + return; + } + const user = defaultContentRef.current[localUid]; + if (!user) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Join room failed - user not found', + {localUid, toRoomId}, + ); + return; + } + + logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'User joining room', { + userId: localUid, + userName: user.name, + toRoomId, + toRoomName: stateRef.current.breakoutGroups.find(r => r.id === toRoomId) + ?.name, + }); - const joinRoom = (toRoomId: string) => { - const user = defaultContent[localUid]; moveUserIntoGroup(user, toRoomId); setSelfJoinRoomId(toRoomId); }; - const exitRoom = async (fromRoomId?: string) => { - try { - const localUser = defaultContent[localUid]; - const currentRoomId = fromRoomId ? fromRoomId : getCurrentRoom()?.id; - if (currentRoomId) { - // Use breakout-specific exit (doesn't destroy main RTM) - await breakoutRoomExit(); - - dispatch({ - type: BreakoutGroupActionTypes.EXIT_GROUP, - payload: { - user: localUser, - fromGroupId: currentRoomId, + const exitRoom = useCallback( + async ( + fromRoomId?: string, + permissionAtCallTime = permissions.canExitRoom, + ) => { + // 🛡️ Use permission passed at call time to avoid race conditions + if (!permissionAtCallTime) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Exit room blocked - no permission at call time', + { + fromRoomId, + permissionAtCallTime, + currentPermission: permissions.canExitRoom, }, - }); + ); + return; } - } catch (error) { - const localUser = defaultContent[localUid]; + + const localUser = defaultContentRef.current[localUid]; const currentRoom = getCurrentRoom(); - if (currentRoom) { - dispatch({ - type: BreakoutGroupActionTypes.EXIT_GROUP, - payload: { - user: localUser, - fromGroupId: currentRoom.id, + const currentRoomId = fromRoomId ? fromRoomId : currentRoom?.id; + + logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'User exiting room', { + userId: localUid, + userName: localUser?.name, + fromRoomId: currentRoomId, + fromRoomName: currentRoom?.name, + hasLocalUser: !!localUser, + }); + + try { + if (currentRoomId && localUser) { + // Use breakout-specific exit (doesn't destroy main RTM) + await breakoutRoomExit(); + + // 🛡️ Guard against component unmount + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Exit room cancelled - component unmounted', + {userId: localUid, fromRoomId: currentRoomId}, + ); + return; + } + + dispatch({ + type: BreakoutGroupActionTypes.EXIT_GROUP, + payload: { + user: localUser, + fromGroupId: currentRoomId, + }, + }); + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'User exit room success', + {userId: localUid, fromRoomId: currentRoomId}, + ); + } + } catch (error) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Exit room error - fallback dispatch', + { + userId: localUid, + fromRoomId: currentRoomId, + error: error.message, }, - }); + ); + + if (currentRoom && localUser) { + dispatch({ + type: BreakoutGroupActionTypes.EXIT_GROUP, + payload: { + user: localUser, + fromGroupId: currentRoom.id, + }, + }); + } } - } - }; + }, + [ + dispatch, + getCurrentRoom, + localUid, + permissions.canExitRoom, // TODO:SUP move to the method call + breakoutRoomExit, + ], + ); const closeRoom = (roomIdToClose: string) => { + if (!acquireOperationLock('CLOSE_GROUP')) { + return; + } + + const roomToClose = stateRef.current.breakoutGroups.find( + r => r.id === roomIdToClose, + ); + + logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Closing breakout room', { + roomId: roomIdToClose, + roomName: roomToClose?.name, + participantCount: + (roomToClose?.participants.hosts.length || 0) + + (roomToClose?.participants.attendees.length || 0), + isHost: isHostRef.current, + }); + dispatch({ type: BreakoutGroupActionTypes.CLOSE_GROUP, - payload: { - groupId: roomIdToClose, - }, + payload: {groupId: roomIdToClose}, }); }; const closeAllRooms = () => { - dispatch({ - type: BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, - }); + if (!acquireOperationLock('CLOSE_ALL_GROUPS')) return; + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Closing all breakout rooms', + { + roomCount: stateRef.current.breakoutGroups.length, + totalParticipants: stateRef.current.breakoutGroups.reduce( + (sum, room) => + sum + + room.participants.hosts.length + + room.participants.attendees.length, + 0, + ), + isHost: isHostRef.current, + sessionId: stateRef.current.breakoutSessionId, + }, + ); + + dispatch({type: BreakoutGroupActionTypes.CLOSE_ALL_GROUPS}); }; const sendAnnouncement = (announcement: string) => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Sending announcement to all rooms', + { + announcementLength: announcement.length, + roomCount: stateRef.current.breakoutGroups.length, + senderUserId: localUid, + senderUserName: defaultContentRef.current[localUid]?.name, + isHost: isHostRef.current, + }, + ); + events.send( BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT, JSON.stringify({ @@ -634,31 +1532,48 @@ const BreakoutRoomProvider = ({ }; const updateRoomName = (newRoomName: string, roomIdToEdit: string) => { + if (!acquireOperationLock('RENAME_GROUP')) return; + + const roomToRename = stateRef.current.breakoutGroups.find( + r => r.id === roomIdToEdit, + ); + + logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Renaming breakout room', { + roomId: roomIdToEdit, + oldName: roomToRename?.name, + newName: newRoomName, + isHost: isHostRef.current, + }); + dispatch({ type: BreakoutGroupActionTypes.RENAME_GROUP, - payload: { - newName: newRoomName, - groupId: roomIdToEdit, - }, + payload: {newName: newRoomName, groupId: roomIdToEdit}, }); }; const getAllRooms = () => { - return state.breakoutGroups.length > 0 ? state.breakoutGroups : []; + return stateRef.current.breakoutGroups.length > 0 + ? stateRef.current.breakoutGroups + : []; }; const getRoomMemberDropdownOptions = (memberUid: UidType) => { const options: MemberDropdownOption[] = []; // Find which room the user is currently in + + const memberUser = defaultContentRef.current[memberUid]; + if (!memberUser) { + return options; + } + const getCurrentUserRoom = (uid: UidType) => { - return state.breakoutGroups.find( + return stateRef.current.breakoutGroups.find( group => group.participants.hosts.includes(uid) || group.participants.attendees.includes(uid), ); }; const currentRoom = getCurrentUserRoom(memberUid); - const memberUser = defaultContent[memberUid]; // Move to Main Room option options.push({ icon: 'double-up-arrow', @@ -668,7 +1583,7 @@ const BreakoutRoomProvider = ({ }); // Move to other breakout rooms (exclude current room) - state.breakoutGroups + stateRef.current.breakoutGroups .filter(group => group.id !== currentRoom?.id) .forEach(group => { options.push({ @@ -682,14 +1597,14 @@ const BreakoutRoomProvider = ({ }); // Make presenter option (only for hosts) - if (isHost) { + if (isHostRef.current) { const userIsPresenting = isUserPresenting(memberUid); const title = userIsPresenting ? 'Stop presenter' : 'Make a Presenter'; const action = userIsPresenting ? 'stop' : 'start'; options.push({ type: 'make-presenter', icon: 'promote-filled', - title: title, + title, onOptionPress: () => makePresenter(memberUser, action), }); } @@ -698,19 +1613,29 @@ const BreakoutRoomProvider = ({ const isUserPresenting = useCallback( (uid?: UidType) => { - if (uid) { - // Check specific user + if (uid !== undefined) { return presenters.some(presenter => presenter.uid === uid); - } else { - // Check current user (same as canIPresent) - return false; } + // fall back to current user + return canIPresent; }, - [presenters], + [presenters, canIPresent], ); // User wants to start presenting const makePresenter = (user: ContentInterface, action: 'start' | 'stop') => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `Make presenter - ${action}`, + { + targetUserId: user.uid, + targetUserName: user.name, + action, + isHost: isHostRef.current, + }, + ); + try { // Host can make someone a presenter events.send( @@ -718,7 +1643,7 @@ const BreakoutRoomProvider = ({ JSON.stringify({ uid: user.uid, timestamp: Date.now(), - action: action, + action, }), PersistanceLevel.None, user.uid, @@ -729,7 +1654,17 @@ const BreakoutRoomProvider = ({ removePresenter(user.uid); } } catch (error) { - console.log('Error making user presenter:', error); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Error making user presenter', + { + targetUserId: user.uid, + targetUserName: user.name, + action, + error: error.message, + }, + ); } }; @@ -751,7 +1686,13 @@ const BreakoutRoomProvider = ({ } }, []); - const onMakeMePresenter = (action: 'start' | 'stop') => { + const onMakeMePresenter = useCallback((action: 'start' | 'stop') => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `User became presenter - ${action}`, + ); + if (action === 'start') { setICanPresent(true); // Show toast notification when presenter permission is granted @@ -769,73 +1710,24 @@ const BreakoutRoomProvider = ({ visibilityTime: 3000, }); } - }; + }, []); const clearAllPresenters = useCallback(() => { setPresenters([]); }, []); - // Raised hand management functions - const addRaisedHand = useCallback( - (uid: UidType) => { - setRaisedHands(prev => { - // Check if already raised to avoid duplicates - const exists = prev.find(hand => hand.uid === uid); - if (exists) { - return prev; - } - return [...prev, {uid, timestamp: Date.now()}]; - }); - if (isHost) { - const userName = defaultContent[uid]?.name || `User ${uid}`; - Toast.show({ - leadingIconName: 'raise-hand', - type: 'info', - text1: `${userName} raised their hand`, - visibilityTime: 3000, - primaryBtn: null, - secondaryBtn: null, - leadingIcon: null, - }); - } - }, - [defaultContent, isHost], - ); - - const removeRaisedHand = useCallback( - (uid: UidType) => { - if (uid) { - setRaisedHands(prev => prev.filter(hand => hand.uid !== uid)); - } - if (isHost) { - const userName = defaultContent[uid]?.name || `User ${uid}`; - Toast.show({ - leadingIconName: 'raise-hand', - type: 'info', - text1: `${userName} lowered their hand`, - visibilityTime: 3000, - primaryBtn: null, - secondaryBtn: null, - leadingIcon: null, - }); - } - }, - [defaultContent, isHost], - ); - - const clearAllRaisedHands = useCallback(() => { - setRaisedHands([]); - }, []); - + // Raise Hand // Send raise hand event via RTM const sendRaiseHandEvent = useCallback( (action: 'raise' | 'lower') => { - const payload = { - action, - uid: localUid, - timestamp: Date.now(), - }; + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `Send raise hand event - ${action}`, + {action, userId: localUid}, + ); + const payload = {action, uid: localUid, timestamp: Date.now()}; events.send( BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, JSON.stringify(payload), @@ -844,227 +1736,404 @@ const BreakoutRoomProvider = ({ [localUid], ); - // Handle incoming raise hand events (only host sees notifications) - const onRaiseHand = (action: 'raise' | 'lower', uid: UidType) => { - try { - if (action === 'raise') { - addRaisedHand(uid); - // Show toast notification only to host - } else if (action === 'lower') { - removeRaisedHand(uid); - // Show toast notification only to host + // Raised hand management functions + const addRaisedHand = useCallback((uid: UidType) => { + setRaisedHands(prev => { + // Check if already raised to avoid duplicates + const exists = prev.find(hand => hand.uid === uid); + if (exists) { + return prev; } - } catch (error) { - console.error('Error handling raise hand event:', error); + return [...prev, {uid, timestamp: Date.now()}]; + }); + if (isHostRef.current) { + const userName = defaultContentRef.current[uid]?.name || `User ${uid}`; + Toast.show({ + leadingIconName: 'raise-hand', + type: 'info', + text1: `${userName} raised their hand`, + visibilityTime: 3000, + primaryBtn: null, + secondaryBtn: null, + leadingIcon: null, + }); } - }; - - // Calculate permissions dynamically - const permissions = useMemo((): BreakoutRoomPermissions => { - const currentlyInRoom = isUserInRoom(); - console.log('supriya-let currentlyInRoom: ', currentlyInRoom); - const hasAvailableRooms = state.breakoutGroups.length > 0; - console.log('supriya-let hasAvailableRooms: ', hasAvailableRooms); - const canUserSwitchRoom = state.canUserSwitchRoom; - console.log('supriya-let canUserSwitchRoom: ', canUserSwitchRoom); + }, []); - console.log( - 'supriya-let canJoinRoom', - !currentlyInRoom && hasAvailableRooms && (isHost || canUserSwitchRoom), - ); - if (true) { - return { - // Room navigation - canJoinRoom: - !currentlyInRoom && - hasAvailableRooms && - (isHost || canUserSwitchRoom), - canExitRoom: currentlyInRoom, - canSwitchBetweenRooms: - currentlyInRoom && hasAvailableRooms && (isHost || canUserSwitchRoom), - // Media controls - canScreenshare: currentlyInRoom ? canIPresent : isHost, - canRaiseHands: !isHost && !!state.breakoutSessionId, - canSeeRaisedHands: isHost, - // Room management (host only) - canAssignParticipants: isHost, - canCreateRooms: isHost, - canMoveUsers: isHost, - canCloseRooms: isHost && hasAvailableRooms && !!state.breakoutSessionId, - canMakePresenter: isHost, - }; + const removeRaisedHand = useCallback((uid: UidType) => { + if (uid) { + setRaisedHands(prev => prev.filter(hand => hand.uid !== uid)); } - return { - canJoinRoom: false, - canExitRoom: false, - canSwitchBetweenRooms: false, // Media controls - canScreenshare: true, - canRaiseHands: false, - // Room management (host only) - canAssignParticipants: false, - canCreateRooms: false, - canMoveUsers: false, - canCloseRooms: false, - canMakePresenter: false, - }; - }, [ - isUserInRoom, - isHost, - state.canUserSwitchRoom, - state.breakoutGroups, - state.breakoutSessionId, - state.assignmentStrategy, - canIPresent, - ]); + if (isHostRef.current) { + const userName = defaultContentRef.current[uid]?.name || `User ${uid}`; + Toast.show({ + leadingIconName: 'raise-hand', + type: 'info', + text1: `${userName} lowered their hand`, + visibilityTime: 3000, + primaryBtn: null, + secondaryBtn: null, + leadingIcon: null, + }); + } + }, []); + + const clearAllRaisedHands = useCallback(() => { + setRaisedHands([]); + }, []); + + // Handle incoming raise hand events (only host sees notifications) + const onRaiseHand = useCallback( + (action: 'raise' | 'lower', uid: UidType) => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `Received raise hand event - ${action}`, + ); + + try { + if (action === 'raise') { + addRaisedHand(uid); + } else if (action === 'lower') { + removeRaisedHand(uid); + } + } catch (error) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Error handling raise hand event', + {action, fromUserId: uid, error: error.message}, + ); + } + }, + [addRaisedHand, removeRaisedHand], + ); const handleBreakoutRoomSyncState = useCallback( (data: BreakoutRoomSyncStateEventPayload['data']['data']) => { - const {switch_room, breakout_room} = data; - // Store previous state to compare changes - const prevGroups = state.breakoutGroups; - const prevSwitchRoom = state.canUserSwitchRoom; - const userCurrentRoom = getCurrentRoom(); - const userCurrentRoomId = userCurrentRoom?.id || null; - - dispatch({ - type: BreakoutGroupActionTypes.SYNC_STATE, - payload: { - sessionId: data.session_id, - switchRoom: data.switch_room, - rooms: data.breakout_room, + const {session_id, switch_room, breakout_room, assignment_type} = data; + console.log('supriya-state-sync new data: ', data); + console.log('supriya-state-sync old data: ', stateRef.current); + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Sync state event received', + { + sessionId: session_id, + incomingRoomCount: breakout_room?.length || 0, + currentRoomCount: stateRef.current.breakoutGroups.length, + switchRoom: switch_room, + assignmentType: assignment_type, }, - }); + ); + + if (isAnotherHostOperating) { + setIsAnotherHostOperating(false); + setCurrentOperatingHostName(undefined); + } + // 🛡️ BEFORE snapshot - using stateRef to avoid stale closure + const prevGroups = stateRef.current.breakoutGroups; + const prevSwitchRoom = stateRef.current.canUserSwitchRoom; + + // Helpers to find membership + const findUserRoomId = (uid: UidType, groups: BreakoutGroup[] = []) => + groups.find(g => { + const hosts = Array.isArray(g?.participants?.hosts) + ? g.participants.hosts + : []; + const attendees = Array.isArray(g?.participants?.attendees) + ? g.participants.attendees + : []; + return hosts.includes(uid) || attendees.includes(uid); + })?.id ?? null; + + const prevRoomId = findUserRoomId(localUid, prevGroups); // before + const nextRoomId = findUserRoomId(localUid, breakout_room); // Show notifications based on changes // 1. Switch room enabled notification if (switch_room && !prevSwitchRoom) { - Toast.show({ + console.log('supriya-toast 1'); + showDeduplicatedToast('switch-room-toggle', { leadingIconName: 'info', type: 'info', text1: 'Breakout rooms are now open. Please choose a room to join.', - visibilityTime: 4000, + visibilityTime: 3000, }); - return; // Don't show other notifications when rooms first open } // 2. User joined a room (compare previous and current state) - if (userCurrentRoomId) { - const wasInRoom = prevGroups.some( - group => - group.participants.hosts.includes(localUid) || - group.participants.attendees.includes(localUid), - ); + if (!prevRoomId && nextRoomId) { + console.log('supriya-toast 2', prevRoomId, nextRoomId); + const currentRoom = breakout_room.find(r => r.id === nextRoomId); - if (!wasInRoom) { - const currentRoom = breakout_room.find( - room => room.id === userCurrentRoomId, - ); - Toast.show({ - type: 'success', - text1: `You've joined ${currentRoom?.name || 'a breakout room'}.`, - visibilityTime: 3000, - }); - return; - } + showDeduplicatedToast(`joined-room-${nextRoomId}`, { + type: 'success', + text1: `You've joined ${currentRoom?.name || 'a breakout room'}.`, + visibilityTime: 3000, + }); } // 3. User was moved to a different room by host - if (userCurrentRoom) { - const prevUserRoom = prevGroups.find( - group => - group.participants.hosts.includes(localUid) || - group.participants.attendees.includes(localUid), - ); - - if (prevUserRoom && prevUserRoom.id !== userCurrentRoomId) { - Toast.show({ - type: 'info', - text1: `You've been moved to ${userCurrentRoom.name} by the host.`, - visibilityTime: 4000, - }); - return; - } + // Moved to a different room (before: A, after: B, A≠B) + if (prevRoomId && nextRoomId && prevRoomId !== nextRoomId) { + console.log('supriya-toast 3', prevRoomId, nextRoomId); + const afterRoom = breakout_room.find(r => r.id === nextRoomId); + showDeduplicatedToast(`moved-to-room-${nextRoomId}`, { + type: 'info', + text1: `You've been moved to ${afterRoom.name} by the host.`, + visibilityTime: 4000, + }); } // 4. User was moved to main room - if (!userCurrentRoom) { - const wasInRoom = prevGroups.some( - group => - group.participants.hosts.includes(localUid) || - group.participants.attendees.includes(localUid), - ); + if (prevRoomId && !nextRoomId) { + console.log('supriya-toast 4', prevRoomId, nextRoomId); + + const prevRoom = prevGroups.find(r => r.id === prevRoomId); + // Distinguish "room closed" vs "moved to main" + const roomStillExists = breakout_room.some(r => r.id === prevRoomId); - if (wasInRoom) { - Toast.show({ + if (!roomStillExists) { + showDeduplicatedToast(`current-room-closed-${prevRoomId}`, { + leadingIconName: 'alert', + type: 'error', + text1: `${ + prevRoom?.name || 'Your room' + } is currently closed. Returning to main room.`, + visibilityTime: 5000, + }); + } else { + showDeduplicatedToast(`moved-to-main-${prevRoomId}`, { leadingIconName: 'arrow-up', type: 'info', text1: "You've returned to the main room.", visibilityTime: 3000, }); - // Exit breakout room and return to main room - exitRoom(); - return; } + // Exit breakout room and return to main room + exitRoom(prevRoomId, true); + return; } // 5. All breakout rooms closed if (breakout_room.length === 0 && prevGroups.length > 0) { - Toast.show({ - leadingIconName: 'close', - type: 'warning', - text1: 'Breakout rooms are now closed. Returning to the main room...', - visibilityTime: 4000, - }); - // Exit breakout room and return to main room - exitRoom(); - return; - } + console.log('supriya-toast 5', prevRoomId, nextRoomId); - // 6. Specific room was closed (user was in it) - if (userCurrentRoomId) { - const roomStillExists = breakout_room.some( - room => room.id === userCurrentRoomId, - ); - if (!roomStillExists) { - const closedRoom = prevGroups.find( - room => room.id === userCurrentRoomId, - ); - Toast.show({ - leadingIconName: 'alert', - type: 'error', - text1: `${ - closedRoom?.name || 'Your room' - } is currently closed. Returning to main room. Please - contact the host.`, - visibilityTime: 5000, + // Show different messages based on user's current location + if (prevRoomId) { + // User was in a breakout room - returning to main + showDeduplicatedToast('all-rooms-closed', { + leadingIconName: 'close', + type: 'info', + text1: + 'Breakout rooms are now closed. Returning to the main room...', + visibilityTime: 3000, + }); + exitRoom(prevRoomId, true); + } else { + // User was already in main room - just notify about closure + showDeduplicatedToast('all-rooms-closed', { + leadingIconName: 'close', + type: 'info', + text1: 'All breakout rooms have been closed.', + visibilityTime: 4000, }); - // Exit breakout room and return to main room - exitRoom(); - return; } + return; } - // 7. Room name changed + // 6) Room renamed (compare per-room names) prevGroups.forEach(prevRoom => { - const currentRoom = breakout_room.find(room => room.id === prevRoom.id); - if (currentRoom && currentRoom.name !== prevRoom.name) { - Toast.show({ + const after = breakout_room.find(r => r.id === prevRoom.id); + if (after && after.name !== prevRoom.name) { + showDeduplicatedToast(`room-renamed-${after.id}`, { type: 'info', - text1: `${prevRoom.name} has been renamed to '${currentRoom.name}'.`, + text1: `${prevRoom.name} has been renamed to '${after.name}'.`, visibilityTime: 3000, }); } }); + + // Finally, apply the authoritative state + dispatch({ + type: BreakoutGroupActionTypes.SYNC_STATE, + payload: { + sessionId: session_id, + assignmentStrategy: assignment_type, + switchRoom: switch_room, + rooms: breakout_room, + }, + }); }, [ dispatch, - getCurrentRoom, + exitRoom, localUid, - state.breakoutGroups, - state.canUserSwitchRoom, + showDeduplicatedToast, + isAnotherHostOperating, ], ); + // Multi-host coordination handlers + const handleHostOperationStart = useCallback( + (operationName: string, hostUid: UidType, hostName: string) => { + // Only process if current user is also a host and it's not their own event + console.log('supriya-state-sync host operation started', operationName); + if (!isHostRef.current || hostUid === localUid) { + return; + } + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Another host started operation - locking UI', + {operationName, hostUid, hostName}, + ); + + setIsAnotherHostOperating(true); + setCurrentOperatingHostName(hostName); + + // Show toast notification + showDeduplicatedToast(`host-operation-start-${hostUid}`, { + type: 'info', + text1: `${hostName} is managing breakout rooms`, + text2: 'Please wait for them to finish', + visibilityTime: 5000, + }); + + // Emergency timeout ONLY as last resort (30 seconds for network failures) + const timeoutId = safeSetTimeout(() => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'EMERGENCY: Auto-clearing host operation lock after extended timeout', + { + operationName, + hostUid, + hostName, + timeoutMs: HOST_OPERATION_LOCK_TIMEOUT_MS, + reason: 'Possible network failure or host disconnection', + }, + ); + setIsAnotherHostOperating(false); + setCurrentOperatingHostName(undefined); + hostOperationTimeoutRef.current = null; // Clear the ref since timeout fired + + showDeduplicatedToast(`host-operation-emergency-unlock-${hostUid}`, { + type: 'info', + text1: 'Breakout room controls unlocked', + text2: 'The other host may have disconnected', + visibilityTime: 4000, + }); + }, HOST_OPERATION_LOCK_TIMEOUT_MS); + + // Store the timeout ID so we can clear it if operation ends normally + hostOperationTimeoutRef.current = timeoutId; + }, + [localUid, showDeduplicatedToast, safeSetTimeout], + ); + + const handleHostOperationEnd = useCallback( + (operationName: string, hostUid: UidType, hostName: string) => { + // Only process if current user is also a host and it's not their own event + console.log('supriya-state-sync host operation ended', operationName); + + if (!isHostRef.current || hostUid === localUid) { + return; + } + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Another host ended operation - unlocking UI', + {operationName, hostUid, hostName}, + ); + + setIsAnotherHostOperating(false); + setCurrentOperatingHostName(undefined); + + // Clear the emergency timeout since operation ended properly + if (hostOperationTimeoutRef.current) { + safeClearTimeout(hostOperationTimeoutRef.current); + hostOperationTimeoutRef.current = null; + } + }, + [localUid], + ); + + // Debounced API for performance with multi-host coordination + const debouncedUpsertAPI = useDebouncedCallback( + async (type: 'START' | 'UPDATE', operationName?: string) => { + setBreakoutUpdateInFlight(true); + setIsPollingPaused(true); + + try { + console.log( + 'supriya-state-sync before calling upsertBreakoutRoomAPI 2007', + ); + + await upsertBreakoutRoomAPI(type); + console.log( + 'supriya-state-sync after calling upsertBreakoutRoomAPI 2007', + ); + console.log('supriya-state-sync operationName', operationName); + + // Broadcast operation end after successful API call + if (operationName) { + console.log( + 'supriya-state-sync broadcasting host operation end', + operationName, + ); + + broadcastHostOperationEnd(operationName); + } + } catch (error) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'API call failed. Reverting to previous state.', + error, + ); + + // Broadcast operation end even on failure + if (operationName) { + broadcastHostOperationEnd(operationName); + } + + // 🔁 Rollback to last valid state + if ( + prevStateRef.current && + validateRollbackState(prevStateRef.current) + ) { + baseDispatch({ + type: BreakoutGroupActionTypes.SYNC_STATE, + payload: { + sessionId: prevStateRef.current.breakoutSessionId, + assignmentStrategy: prevStateRef.current.assignmentStrategy, + switchRoom: prevStateRef.current.canUserSwitchRoom, + rooms: prevStateRef.current.breakoutGroups, + }, + }); + showDeduplicatedToast('breakout-api-failure', { + type: 'error', + text1: 'Sync failed. Reverted to previous state.', + }); + } else { + showDeduplicatedToast('breakout-api-failure-no-rollback', { + type: 'error', + text1: 'Sync failed. Could not rollback safely.', + }); + } + } finally { + setBreakoutUpdateInFlight(false); + setIsPollingPaused(false); + } + }, + 500, + ); + // Action-based API triggering useEffect(() => { if (!lastAction || !lastAction.type) { @@ -1081,30 +2150,38 @@ const BreakoutRoomProvider = ({ BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, BreakoutGroupActionTypes.EXIT_GROUP, ]; // Host can always trigger API calls for any action // Attendees can only trigger API when they self-join a room and switch_room is enabled - console.log('supriya-allow-people lastAction', lastAction); - console.log('supriya-allow-people isHost', isHost); + const attendeeSelfJoinAllowed = + stateRef.current.canUserSwitchRoom && + lastAction.type === BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP; const shouldCallAPI = API_TRIGGERING_ACTIONS.includes(lastAction.type as any) && - (isHost || - (!isHost && - state.canUserSwitchRoom && - lastAction.type === - BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP)); - console.log('supriya-allow-people shouldCallAPI', shouldCallAPI); + (isHostRef.current || (!isHostRef.current && attendeeSelfJoinAllowed)); + + // Compute lastOperationName based on lastAction + const lastOperationName = HOST_BROADCASTED_OPERATIONS.includes( + lastAction?.type as any, + ) + ? lastAction?.type + : undefined; + + console.log( + 'supriya-state-sync shouldCallAPI', + shouldCallAPI, + lastAction.type, + lastOperationName, + ); if (shouldCallAPI) { - console.log('supriya-allow-people calling update api'); - upsertBreakoutRoomAPI('UPDATE').finally(() => {}); - } else { - console.log(`Action ${lastAction.type} - skipping API call`); + debouncedUpsertAPI('UPDATE', lastOperationName); } - }, [lastAction, upsertBreakoutRoomAPI, isHost, state.canUserSwitchRoom]); + }, [dispatch, lastAction]); return ( {children} diff --git a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx index a6b7c3745..d4483600f 100644 --- a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx +++ b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx @@ -1,9 +1,15 @@ -import React, {useEffect} from 'react'; +import React, {useEffect, useRef} from 'react'; import events from '../../../rtm-events-api'; import {BreakoutRoomEventNames} from './constants'; import Toast from '../../../../react-native-toast-message'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; -import {BreakoutRoomSyncStateEventPayload} from '../state/types'; +import { + BreakoutRoomAnnouncementEventPayload, + BreakoutRoomSyncStateEventPayload, +} from '../state/types'; +import {useLocalUid} from '../../../../agora-rn-uikit'; +import {useRoomInfo} from '../../../components/room-info/useRoomInfo'; +import {logger, LogSource} from '../../../logger/AppBuilderLogger'; interface Props { children: React.ReactNode; @@ -14,40 +20,102 @@ const BreakoutRoomEventsConfigure: React.FC = ({ children, mainChannelName, }) => { - const {onMakeMePresenter, handleBreakoutRoomSyncState, onRaiseHand} = - useBreakoutRoom(); + const { + onMakeMePresenter, + handleBreakoutRoomSyncState, + onRaiseHand, + handleHostOperationStart, + handleHostOperationEnd, + } = useBreakoutRoom(); + const localUid = useLocalUid(); + const { + data: {isHost}, + } = useRoomInfo(); + const isHostRef = React.useRef(isHost); + const localUidRef = React.useRef(localUid); + const onRaiseHandRef = useRef(onRaiseHand); + const onMakeMePresenterRef = useRef(onMakeMePresenter); + const handleBreakoutRoomSyncStateRef = useRef(handleBreakoutRoomSyncState); + const handleHostOperationStartRef = useRef(handleHostOperationStart); + const handleHostOperationEndRef = useRef(handleHostOperationEnd); + + useEffect(() => { + isHostRef.current = isHost; + }, [isHost]); + useEffect(() => { + localUidRef.current = localUid; + }, [localUid]); + useEffect(() => { + onRaiseHandRef.current = onRaiseHand; + }, [onRaiseHand]); + useEffect(() => { + onMakeMePresenterRef.current = onMakeMePresenter; + }, [onMakeMePresenter]); + useEffect(() => { + handleBreakoutRoomSyncStateRef.current = handleBreakoutRoomSyncState; + }, [handleBreakoutRoomSyncState]); + useEffect(() => { + handleHostOperationStartRef.current = handleHostOperationStart; + }, [handleHostOperationStart]); + useEffect(() => { + handleHostOperationEndRef.current = handleHostOperationEnd; + }, [handleHostOperationEnd]); useEffect(() => { const handlePresenterStatusEvent = (evtData: any) => { - console.log('supriya-event BREAKOUT_ROOM_MAKE_PRESENTER data: ', evtData); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'BREAKOUT_ROOM_MAKE_PRESENTER event recevied', + evtData, + ); try { - const {payload} = evtData; + const {sender, payload} = evtData; + if (sender === `${localUidRef.current}`) { + return; + } const data = JSON.parse(payload); if (data.action === 'start' || data.action === 'stop') { - onMakeMePresenter(data.action); + onMakeMePresenterRef.current(data.action); } } catch (error) {} }; const handleRaiseHandEvent = (evtData: any) => { - console.log( - 'supriya-event BREAKOUT_ROOM_ATTENDEE_RAISE_HAND data: ', + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'BREAKOUT_ROOM_ATTENDEE_RAISE_HAND event recevied', evtData, ); try { - const {payload} = evtData; + const {sender, payload} = evtData; + if (!isHostRef.current) { + return; + } + if (sender === `${localUidRef.current}`) { + return; + } const data = JSON.parse(payload); if (data.action === 'raise' || data.action === 'lower') { - onRaiseHand(data.action, data.uid); + onRaiseHandRef.current?.(data.action, data.uid); } } catch (error) {} }; const handleAnnouncementEvent = (evtData: any) => { - console.log('supriya-event BREAKOUT_ROOM_ANNOUNCEMENT data: ', evtData); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'BREAKOUT_ROOM_ANNOUNCEMENT event recevied', + evtData, + ); try { - const {_, payload} = evtData; - const data = JSON.parse(payload); + const {_, payload, sender} = evtData; + const data: BreakoutRoomAnnouncementEventPayload = JSON.parse(payload); + if (sender === `${localUidRef.current}`) { + return; + } if (data.announcement) { Toast.show({ leadingIconName: 'speaker', @@ -63,11 +131,83 @@ const BreakoutRoomEventsConfigure: React.FC = ({ }; const handleBreakoutRoomSyncStateEvent = (evtData: any) => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'BREAKOUT_ROOM_SYNC_STATE event recevied', + evtData, + ); const {payload} = evtData; - console.log('supriya-event BREAKOUT_ROOM_SYNC_STATE data: ', evtData); const data: BreakoutRoomSyncStateEventPayload = JSON.parse(payload); if (data.data.act === 'SYNC_STATE') { - handleBreakoutRoomSyncState(data.data.data); + console.log( + 'supriya-state-sync ********* BREAKOUT_ROOM_SYNC_STATE event triggered ***************', + ); + handleBreakoutRoomSyncStateRef.current(data.data.data); + } + }; + + const handleHostOperationStartEvent = (evtData: any) => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'BREAKOUT_ROOM_HOST_OPERATION_START event received', + evtData, + ); + try { + const {sender, payload} = evtData; + // Ignore events from self + if (sender === `${localUidRef.current}`) { + return; + } + // Only process if current user is also a host + if (!isHostRef.current) { + return; + } + + const data = JSON.parse(payload); + const {operationName, hostUid, hostName} = data; + + handleHostOperationStartRef.current(operationName, hostUid, hostName); + } catch (error) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Error handling host operation start event', + {error: error.message}, + ); + } + }; + + const handleHostOperationEndEvent = (evtData: any) => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'BREAKOUT_ROOM_HOST_OPERATION_END event received', + evtData, + ); + try { + const {sender, payload} = evtData; + // Ignore events from self + if (sender === `${localUidRef.current}`) { + return; + } + // Only process if current user is also a host + if (!isHostRef.current) { + return; + } + + const data = JSON.parse(payload); + const {operationName, hostUid, hostName} = data; + + handleHostOperationEndRef.current(operationName, hostUid, hostName); + } catch (error) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Error handling host operation end event', + {error: error.message}, + ); } }; @@ -87,6 +227,14 @@ const BreakoutRoomEventsConfigure: React.FC = ({ BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, handleBreakoutRoomSyncStateEvent, ); + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_HOST_OPERATION_START, + handleHostOperationStartEvent, + ); + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_HOST_OPERATION_END, + handleHostOperationEndEvent, + ); return () => { events.off(BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT); @@ -102,8 +250,16 @@ const BreakoutRoomEventsConfigure: React.FC = ({ BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, handleBreakoutRoomSyncStateEvent, ); + events.off( + BreakoutRoomEventNames.BREAKOUT_ROOM_HOST_OPERATION_START, + handleHostOperationStartEvent, + ); + events.off( + BreakoutRoomEventNames.BREAKOUT_ROOM_HOST_OPERATION_END, + handleHostOperationEndEvent, + ); }; - }, [onMakeMePresenter, handleBreakoutRoomSyncState, onRaiseHand]); + }, []); return <>{children}; }; diff --git a/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx b/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx index 81e105495..524465a7c 100644 --- a/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx +++ b/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx @@ -2,13 +2,14 @@ import React, {useEffect} from 'react'; import events from '../../../rtm-events-api'; import {BreakoutRoomEventNames} from './constants'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import {BreakoutRoomSyncStateEventPayload} from '../state/types'; interface Props { children: React.ReactNode; } const BreakoutRoomMainEventsConfigure: React.FC = ({children}) => { - const {onRaiseHand} = useBreakoutRoom(); + const {onRaiseHand, handleBreakoutRoomSyncState} = useBreakoutRoom(); useEffect(() => { const handleRaiseHandEvent = (evtData: any) => { @@ -25,18 +26,38 @@ const BreakoutRoomMainEventsConfigure: React.FC = ({children}) => { } catch (error) {} }; + const handleBreakoutRoomSyncStateEvent = (evtData: any) => { + const {payload} = evtData; + console.log( + 'supriya-event BREAKOUT_ROOM_SYNC_STATE data (main): ', + evtData, + ); + const data: BreakoutRoomSyncStateEventPayload = JSON.parse(payload); + if (data.data.act === 'SYNC_STATE') { + handleBreakoutRoomSyncState(data.data.data); + } + }; + events.on( BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, handleRaiseHandEvent, ); + events.on( + BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, + handleBreakoutRoomSyncStateEvent, + ); return () => { events.off( BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, handleRaiseHandEvent, ); + events.off( + BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, + handleBreakoutRoomSyncStateEvent, + ); }; - }, [onRaiseHand]); + }, [onRaiseHand, handleBreakoutRoomSyncState]); return <>{children}; }; diff --git a/template/src/components/breakout-room/events/constants.ts b/template/src/components/breakout-room/events/constants.ts index 4005e370d..3606d5d89 100644 --- a/template/src/components/breakout-room/events/constants.ts +++ b/template/src/components/breakout-room/events/constants.ts @@ -4,6 +4,8 @@ const BREAKOUT_ROOM_SYNC_STATE = 'BREAKOUT_ROOM_BREAKOUT_ROOM_STATE'; const BREAKOUT_ROOM_ANNOUNCEMENT = 'BREAKOUT_ROOM_ANNOUNCEMENT'; const BREAKOUT_ROOM_MAKE_PRESENTER = 'BREAKOUT_ROOM_MAKE_PRESENTER'; const BREAKOUT_ROOM_ATTENDEE_RAISE_HAND = 'BREAKOUT_ROOM_ATTENDEE_RAISE_HAND'; +const BREAKOUT_ROOM_HOST_OPERATION_START = 'BREAKOUT_ROOM_HOST_OPERATION_START'; +const BREAKOUT_ROOM_HOST_OPERATION_END = 'BREAKOUT_ROOM_HOST_OPERATION_END'; const BreakoutRoomEventNames = { BREAKOUT_ROOM_JOIN_DETAILS, @@ -11,5 +13,7 @@ const BreakoutRoomEventNames = { BREAKOUT_ROOM_ANNOUNCEMENT, BREAKOUT_ROOM_MAKE_PRESENTER, BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, + BREAKOUT_ROOM_HOST_OPERATION_START, + BREAKOUT_ROOM_HOST_OPERATION_END, }; export {BreakoutRoomEventNames}; diff --git a/template/src/components/breakout-room/state/reducer.ts b/template/src/components/breakout-room/state/reducer.ts index 6d2e1d230..7a5946804 100644 --- a/template/src/components/breakout-room/state/reducer.ts +++ b/template/src/components/breakout-room/state/reducer.ts @@ -2,9 +2,9 @@ import {ContentInterface, UidType} from '../../../../agora-rn-uikit/src'; import {randomNameGenerator} from '../../../utils'; export enum RoomAssignmentStrategy { - AUTO_ASSIGN = 'auto-assign', - MANUAL_ASSIGN = 'manual-assign', - NO_ASSIGN = 'no-assign', + AUTO_ASSIGN = 'AUTO_ASSIGN', + MANUAL_ASSIGN = 'MANUAL_ASSIGN', + NO_ASSIGN = 'NO_ASSIGN', } export interface ManualParticipantAssignment { uid: UidType; @@ -45,8 +45,8 @@ export const initialBreakoutGroups = [ export const initialBreakoutRoomState: BreakoutRoomState = { breakoutSessionId: '', - assignmentStrategy: RoomAssignmentStrategy.AUTO_ASSIGN, - canUserSwitchRoom: false, + assignmentStrategy: RoomAssignmentStrategy.NO_ASSIGN, + canUserSwitchRoom: true, unassignedParticipants: [], manualAssignments: [], breakoutGroups: [], @@ -57,8 +57,6 @@ export const BreakoutGroupActionTypes = { SYNC_STATE: 'BREAKOUT_ROOM/SYNC_STATE', // session SET_SESSION_ID: 'BREAKOUT_ROOM/SET_SESSION_ID', - // strategy - SET_ASSIGNMENT_STRATEGY: 'BREAKOUT_ROOM/SET_ASSIGNMENT_STRATEGY', // Manual assignment strategy SET_MANUAL_ASSIGNMENTS: 'BREAKOUT_ROOM/SET_MANUAL_ASSIGNMENTS', CLEAR_MANUAL_ASSIGNMENTS: 'BREAKOUT_ROOM/CLEAR_MANUAL_ASSIGNMENTS', @@ -78,6 +76,7 @@ export const BreakoutGroupActionTypes = { 'BREAKOUT_ROOM/UPDATE_UNASSIGNED_PARTICIPANTS', AUTO_ASSIGN_PARTICPANTS: 'BREAKOUT_ROOM/AUTO_ASSIGN_PARTICPANTS', MANUAL_ASSIGN_PARTICPANTS: 'BREAKOUT_ROOM/MANUAL_ASSIGN_PARTICPANTS', + NO_ASSIGN_PARTICIPANTS: 'BREAKOUT_ROOM/NO_ASSIGN_PARTICIPANTS', MOVE_PARTICIPANT_TO_MAIN: 'BREAKOUT_ROOM/MOVE_PARTICIPANT_TO_MAIN', MOVE_PARTICIPANT_TO_GROUP: 'BREAKOUT_ROOM/MOVE_PARTICIPANT_TO_GROUP', } as const; @@ -89,18 +88,13 @@ export type BreakoutRoomAction = sessionId: BreakoutRoomState['breakoutSessionId']; switchRoom: BreakoutRoomState['canUserSwitchRoom']; rooms: BreakoutRoomState['breakoutGroups']; + assignmentStrategy: BreakoutRoomState['assignmentStrategy']; }; } | { type: typeof BreakoutGroupActionTypes.SET_SESSION_ID; payload: {sessionId: string}; } - | { - type: typeof BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY; - payload: { - strategy: RoomAssignmentStrategy; - }; - } | { type: typeof BreakoutGroupActionTypes.SET_MANUAL_ASSIGNMENTS; payload: { @@ -155,6 +149,9 @@ export type BreakoutRoomAction = | { type: typeof BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS; } + | { + type: typeof BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS; + } | { type: typeof BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS; } @@ -184,11 +181,13 @@ export const breakoutRoomReducer = ( ...state, breakoutSessionId: action.payload.sessionId, canUserSwitchRoom: action.payload.switchRoom, + assignmentStrategy: action.payload.assignmentStrategy, breakoutGroups: action.payload.rooms.map(group => ({ - ...group, + id: group.id, + name: group.name, participants: { - hosts: group.participants?.hosts ?? [], - attendees: group.participants?.attendees ?? [], + hosts: group?.participants?.hosts ?? [], + attendees: group?.participants?.attendees ?? [], }, })), }; @@ -231,13 +230,6 @@ export const breakoutRoomReducer = ( }; } - case BreakoutGroupActionTypes.SET_ASSIGNMENT_STRATEGY: { - return { - ...state, - assignmentStrategy: action.payload.strategy, - }; - } - case BreakoutGroupActionTypes.SET_MANUAL_ASSIGNMENTS: return { ...state, @@ -276,6 +268,7 @@ export const breakoutRoomReducer = ( return { ...state, + assignmentStrategy: RoomAssignmentStrategy.MANUAL_ASSIGN, // Update strategy breakoutGroups: updatedGroups, manualAssignments: [], // Clear after applying }; @@ -287,8 +280,15 @@ export const breakoutRoomReducer = ( }; } + case BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS: { + return { + ...state, + assignmentStrategy: RoomAssignmentStrategy.NO_ASSIGN, // Update strategy + canUserSwitchRoom: true, + }; + } + case BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS: { - const selectedStrategy = state.assignmentStrategy; const roomAssignments = new Map< string, {hosts: UidType[]; attendees: UidType[]} @@ -301,24 +301,23 @@ export const breakoutRoomReducer = ( let assignedParticipantUids: UidType[] = []; // AUTO ASSIGN Simple round-robin assignment (no capacity limits) - if (selectedStrategy === RoomAssignmentStrategy.AUTO_ASSIGN) { - let roomIndex = 0; - const roomIds = state.breakoutGroups.map(room => room.id); - state.unassignedParticipants.forEach(participant => { - const currentRoomId = roomIds[roomIndex]; - const roomAssignment = roomAssignments.get(currentRoomId)!; - // Assign participant based on their isHost status (string "true"/"false") - if (participant.user?.isHost === 'true') { - roomAssignment.hosts.push(participant.uid); - } else { - roomAssignment.attendees.push(participant.uid); - } - // Move it to assigned list - assignedParticipantUids.push(participant.uid); - // Move to next room for round-robin - roomIndex = (roomIndex + 1) % roomIds.length; - }); - } + let roomIndex = 0; + const roomIds = state.breakoutGroups.map(room => room.id); + state.unassignedParticipants.forEach(participant => { + const currentRoomId = roomIds[roomIndex]; + const roomAssignment = roomAssignments.get(currentRoomId)!; + // Assign participant based on their isHost status (string "true"/"false") + if (participant.user?.isHost === 'true') { + roomAssignment.hosts.push(participant.uid); + } else { + roomAssignment.attendees.push(participant.uid); + } + // Move it to assigned list + assignedParticipantUids.push(participant.uid); + // Move to next room for round-robin + roomIndex = (roomIndex + 1) % roomIds.length; + }); + // Update breakoutGroups with new assignments const updatedBreakoutGroups = state.breakoutGroups.map(group => { const roomParticipants = roomAssignments.get(group.id) || { @@ -342,6 +341,7 @@ export const breakoutRoomReducer = ( return { ...state, unassignedParticipants: updatedUnassignedParticipants, + assignmentStrategy: RoomAssignmentStrategy.AUTO_ASSIGN, breakoutGroups: updatedBreakoutGroups, }; } diff --git a/template/src/components/breakout-room/state/types.ts b/template/src/components/breakout-room/state/types.ts index 4c1aa7bb0..0a6bd0914 100644 --- a/template/src/components/breakout-room/state/types.ts +++ b/template/src/components/breakout-room/state/types.ts @@ -1,4 +1,4 @@ -import {BreakoutGroup} from './reducer'; +import {BreakoutGroup, RoomAssignmentStrategy} from './reducer'; export type BreakoutGroupAssignStrategy = 'auto' | 'manual' | 'self-select'; @@ -39,10 +39,16 @@ export interface BreakoutRoomSyncStateEventPayload { switch_room: boolean; session_id: string; breakout_room: BreakoutGroup[]; + assignment_type: RoomAssignmentStrategy; }; act: 'SYNC_STATE'; // e.g., "CHAN_JOIN" }; } +export interface BreakoutRoomAnnouncementEventPayload { + uid: string; + timestamp: string; + announcement: string; +} // | {type: 'DELETE_GROUP'; payload: {groupId: string}} // | { // type: 'ADD_PARTICIPANT'; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index cbe4e6413..baae4ef54 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -14,7 +14,7 @@ import React, {useState, useRef} from 'react'; import {View, Text, StyleSheet} from 'react-native'; import IconButton from '../../../atoms/IconButton'; import ThemeConfig from '../../../theme'; -import {UidType} from 'agora-rn-uikit'; +import {UidType, useLocalUid} from '../../../../agora-rn-uikit'; import UserAvatar from '../../../atoms/UserAvatar'; import ImageIcon from '../../../atoms/ImageIcon'; import {BreakoutGroup} from '../state/reducer'; @@ -34,6 +34,7 @@ const BreakoutRoomGroupSettings: React.FC = () => { const { data: {isHost}, } = useRoomInfo(); + const localUid = useLocalUid(); const { breakoutGroups, @@ -107,7 +108,7 @@ const BreakoutRoomGroupSettings: React.FC = () => { raisedHands.some(hand => hand.uid === memberUId); return ( - + { ) : ( <> )} - {isHost || canUserSwitchRoom ? ( + {permissions.canHostManageMainRoom && memberUId !== localUid ? ( { const renderRoom = (room: BreakoutGroup) => { const isExpanded = expandedRooms.has(room.id); const memberCount = - room.participants.hosts.length || - 0 + room.participants.attendees.length || - 0; + room.participants.hosts.length + room.participants.attendees.length; return ( @@ -196,15 +195,13 @@ const BreakoutRoomGroupSettings: React.FC = () => { - + {isUserInRoom(room) ? ( { - exitRoom(room.id); - }} + onPress={() => exitRoom(room.id)} /> ) : ( { } textStyle={styles.roomActionBtnText} text={'Join'} - onPress={() => { - joinRoom(room.id); - }} + onPress={() => joinRoom(room.id)} /> )} {/* Only host can perform these actions */} - {isHost ? ( + {permissions.canHostManageMainRoom ? ( { closeRoom(room.id); @@ -281,8 +276,8 @@ const BreakoutRoomGroupSettings: React.FC = () => { All Rooms - {isHost ? ( - + {permissions.canHostManageMainRoom ? ( + { iconSize: 20, tintColor: $config.SECONDARY_ACTION_COLOR, }} - onPress={() => { - setAnnouncementModal(true); - }} + onPress={() => setAnnouncementModal(true)} /> ) : ( diff --git a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx index 1b766f309..c4d1588f9 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx @@ -1,10 +1,10 @@ import React, {useEffect, useState, useCallback} from 'react'; import {View, Text, StyleSheet} from 'react-native'; import {useRTMCore} from '../../../rtm/RTMCoreProvider'; -import {nativeChannelTypeMapping} from '../../../../bridge/rtm/web/Types'; import ThemeConfig from '../../../theme'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; import UserAvatar from '../../../atoms/UserAvatar'; +import {type GetOnlineUsersResponse} from 'agora-react-native-rtm'; interface OnlineUser { userId: string; @@ -35,7 +35,7 @@ const getUserNameFromAttributes = ( }; const BreakoutRoomMainRoomUsers: React.FC = () => { - const {client, onlineUsers} = useRTMCore(); + const {client} = useRTMCore(); const {mainChannelId, breakoutGroups} = useBreakoutRoom(); const [usersWithNames, setUsersWithNames] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -54,7 +54,7 @@ const BreakoutRoomMainRoomUsers: React.FC = () => { // Fetch attributes only when online users change useEffect(() => { const fetchUserAttributes = async () => { - if (!client || !onlineUsers || onlineUsers.size === 0) { + if (!client) { setUsersWithNames([]); return; } @@ -63,26 +63,31 @@ const BreakoutRoomMainRoomUsers: React.FC = () => { setError(null); try { - console.log( - `Fetching attributes for online users: of channel ${mainChannelId}`, - Array.from(onlineUsers), - ); + const onlineUsers: GetOnlineUsersResponse = + await client.presence.getOnlineUsers(mainChannelId, 1); + const users = await Promise.all( - Array.from(onlineUsers).map(async userId => { + Array.from(onlineUsers.occupants).map(async member => { try { const attributes = await client.storage.getUserMetadata({ - userId: userId, + userId: member.userId, }); - const username = getUserNameFromAttributes(attributes, userId); + const username = getUserNameFromAttributes( + attributes, + member.userId, + ); return { - userId: userId, + userId: member.userId, name: username, }; } catch (e) { - console.warn(`Failed to get attributes for user ${userId}:`, e); + console.warn( + `Failed to get attributes for user ${member.userId}:`, + e, + ); return { - userId: userId, - name: userId, + userId: member.userId, + name: 'User', }; } }), @@ -98,7 +103,7 @@ const BreakoutRoomMainRoomUsers: React.FC = () => { }; fetchUserAttributes(); - }, [client, onlineUsers, mainChannelId]); + }, [client, mainChannelId]); // Filter out users who are assigned to breakout rooms const mainRoomOnlyUsers = usersWithNames.filter(user => { diff --git a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx index c9403a996..dfa7e7981 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, {useState} from 'react'; import {View, StyleSheet, Text} from 'react-native'; import BreakoutRoomParticipants from './BreakoutRoomParticipants'; import SelectParticipantAssignmentStrategy from './SelectParticipantAssignmentStrategy'; @@ -13,41 +13,48 @@ import TertiaryButton from '../../../atoms/TertiaryButton'; export default function BreakoutRoomSettings() { const { - unsassignedParticipants, + unassignedParticipants, assignmentStrategy, - setStrategy, handleAssignParticipants, canUserSwitchRoom, - toggleSwitchRooms, + toggleRoomSwitchingAllowed, } = useBreakoutRoom(); - const disableAssignmentSelect = unsassignedParticipants.length === 0; + // Local dropdown state to prevent sync conflicts + const [localAssignmentStrategy, setLocalAssignmentStrategy] = + useState(assignmentStrategy); + + const disableAssignmentSelect = unassignedParticipants.length === 0; const disableHandleAssignment = disableAssignmentSelect || - assignmentStrategy === RoomAssignmentStrategy.NO_ASSIGN; + localAssignmentStrategy === RoomAssignmentStrategy.NO_ASSIGN; const { modalOpen: isManualAssignmentModalOpen, setModalOpen: setManualAssignmentModalOpen, } = useModal(); + // Handle strategy change from dropdown + const handleStrategyChange = (newStrategy: RoomAssignmentStrategy) => { + setLocalAssignmentStrategy(newStrategy); + + // Immediately call API for NO_ASSIGN strategy + if (newStrategy === RoomAssignmentStrategy.NO_ASSIGN) { + console.log( + 'supriya-state-sync calling handleAssignParticipants on strategy change', + ); + handleAssignParticipants(newStrategy); + } + }; + // Handle assign participants button click const handleAssignClick = () => { - if (assignmentStrategy === RoomAssignmentStrategy.MANUAL_ASSIGN) { + if (localAssignmentStrategy === RoomAssignmentStrategy.MANUAL_ASSIGN) { // Open manual assignment modal setManualAssignmentModalOpen(true); } else { // Handle other assignment strategies - handleAssignParticipants(assignmentStrategy); - } - }; - - // Handle strategy change - automatically trigger assignment for NO_ASSIGN - const handleStrategyChange = (strategy: RoomAssignmentStrategy) => { - setStrategy(strategy); - // NO_ASSIGN needs to be applied immediately to enable switch rooms - if (strategy === RoomAssignmentStrategy.NO_ASSIGN) { - handleAssignParticipants(strategy); + handleAssignParticipants(localAssignmentStrategy); } }; @@ -55,12 +62,12 @@ export default function BreakoutRoomSettings() { {/* Avatar list */} - + @@ -85,7 +92,7 @@ export default function BreakoutRoomSettings() { diff --git a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx index fdca43eb5..0178006f8 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx @@ -27,7 +27,8 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { upsertBreakoutRoomAPI, closeAllRooms, permissions, - isUserInRoom, + isBreakoutUpdateInFlight, + isAnotherHostOperating, } = useBreakoutRoom(); useEffect(() => { @@ -47,6 +48,9 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { init(); }, []); + // Disable all actions when API is in flight or another host is operating + const disableAllActions = isBreakoutUpdateInFlight || isAnotherHostOperating; + return ( <> @@ -64,16 +68,23 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { /> ) : ( - + {permissions?.canRaiseHands ? : <>} - {permissions?.canAssignParticipants ? ( + {permissions?.canHostManageMainRoom && + permissions.canAssignParticipants ? ( ) : ( )} - {permissions?.canCreateRooms ? ( + {permissions?.canHostManageMainRoom && + permissions?.canCreateRooms ? ( )} - {!isInitializing && permissions?.canCloseRooms ? ( + {!isInitializing && + permissions.canHostManageMainRoom && + permissions?.canCloseRooms ? ( ({ + return unassignedParticipants.map(participant => ({ uid: participant.uid, roomId: null, // Start unassigned - isHost: participant.user.isHost || false, + isHost: participant.user?.isHost || false, isSelected: false, })); }); @@ -235,7 +235,7 @@ export default function ParticipantManualAssignmentModal( 0 ? {} : style.titleLowOpacity, + unassignedParticipants?.length > 0 ? {} : style.titleLowOpacity, ]}> $config.ENABLE_BREAKOUT_ROOM, + breakoutRoom: () => $config.ENABLE_BREAKOUT_ROOM && ENABLE_AUTH, }; export const useControlPermissionMatrix = ( diff --git a/template/src/logger/AppBuilderLogger.tsx b/template/src/logger/AppBuilderLogger.tsx index 5293adb87..6c53f5a50 100644 --- a/template/src/logger/AppBuilderLogger.tsx +++ b/template/src/logger/AppBuilderLogger.tsx @@ -87,7 +87,8 @@ type LogType = { | 'STORE' | 'GET_MEETING_PHRASE' | 'MUTE_PSTN' - | 'FULL_SCREEN'; + | 'FULL_SCREEN' + | 'BREAKOUT_ROOM'; [LogSource.NetworkRest]: | 'idp_login' | 'token_login' diff --git a/template/src/pages/VideoCall.tsx b/template/src/pages/VideoCall.tsx index e0c509579..4254c5adf 100644 --- a/template/src/pages/VideoCall.tsx +++ b/template/src/pages/VideoCall.tsx @@ -83,6 +83,7 @@ import Toast from '../../react-native-toast-message'; import {AuthErrorCodes} from '../utils/common'; import {BreakoutRoomProvider} from '../components/breakout-room/context/BreakoutRoomContext'; import BreakoutRoomMainEventsConfigure from '../components/breakout-room/events/BreakoutRoomMainEventsConfigure'; +import BreakoutRoomEventsConfigure from '../components/breakout-room/events/BreakoutRoomEventsConfigure'; interface VideoCallProps { callActive: boolean; @@ -213,9 +214,12 @@ const VideoCall = (videoCallProps: VideoCallProps) => { handleLeaveBreakout={ null }> - + - + diff --git a/template/src/rtm-events-api/Events.ts b/template/src/rtm-events-api/Events.ts index db8cd7e41..2a47ff3e1 100644 --- a/template/src/rtm-events-api/Events.ts +++ b/template/src/rtm-events-api/Events.ts @@ -134,7 +134,18 @@ class Events { ); try { const targetChannelId = channelId || RTMEngine.getInstance().channelUid; - console.log('supriya targetChannelId: ', targetChannelId); + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'event is sent to targetChannelId ->', + targetChannelId, + ); + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'event is sent to targetChannelId ->', + targetChannelId, + ); if (!targetChannelId || targetChannelId.trim() === '') { throw new Error( 'Channel ID is not set. Cannot send channel messages.', @@ -162,7 +173,6 @@ class Events { ); const adjustedUID = adjustUID(to); try { - console.log('supriya 2 '); await rtmEngine.publish(`${adjustedUID}`, text, { channelType: nativeChannelTypeMapping.USER, // user }); diff --git a/template/src/rtm/RTMCoreProvider.tsx b/template/src/rtm/RTMCoreProvider.tsx index c4d7f849a..1f3d7385b 100644 --- a/template/src/rtm/RTMCoreProvider.tsx +++ b/template/src/rtm/RTMCoreProvider.tsx @@ -38,7 +38,6 @@ interface RTMContextType { connectionState: number; error: Error | null; isLoggedIn: boolean; - onlineUsers: Set; // Callback registration methods registerCallbacks: (channelName: string, callbacks: EventCallbacks) => void; unregisterCallbacks: (channelName: string) => void; @@ -49,7 +48,6 @@ const RTMContext = createContext({ connectionState: 0, error: null, isLoggedIn: false, - onlineUsers: new Set(), registerCallbacks: () => {}, unregisterCallbacks: () => {}, }); @@ -73,7 +71,6 @@ export const RTMCoreProvider: React.FC = ({ const [connectionState, setConnectionState] = useState(0); console.log('supriya-rtm connectionState: ', connectionState); const [error, setError] = useState(null); - const [onlineUsers, setOnlineUsers] = useState>(new Set()); // Callback registration storage const callbackRegistry = useRef>(new Map()); @@ -184,22 +181,6 @@ export const RTMCoreProvider: React.FC = ({ 'supriya-rtm-global @@@@@@@@@@@@@@@@@@@@@@@ ---PresenceEvent: ', presence, ); - if (presence.type === nativePresenceEventTypeMapping.SNAPSHOT) { - // Initial snapshot - set all online users - setOnlineUsers( - new Set(presence.snapshot?.userStateList.map(u => u.userId) || []), - ); - } else if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { - setOnlineUsers(prev => new Set([...prev, presence.publisher])); - } else if ( - presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE - ) { - setOnlineUsers(prev => { - const newSet = new Set(prev); - newSet.delete(presence.publisher); - return newSet; - }); - } // Distribute to all registered callbacks callbackRegistry.current.forEach((callbacks, channelName) => { if (callbacks.presence) { @@ -345,7 +326,6 @@ export const RTMCoreProvider: React.FC = ({ isLoggedIn, connectionState, error, - onlineUsers, registerCallbacks, unregisterCallbacks, }}> diff --git a/template/src/utils/useDebouncedCallback.tsx b/template/src/utils/useDebouncedCallback.tsx new file mode 100644 index 000000000..bcafd938f --- /dev/null +++ b/template/src/utils/useDebouncedCallback.tsx @@ -0,0 +1,20 @@ +import {useRef, useCallback} from 'react'; + +export function useDebouncedCallback void>( + fn: T, + delay: number, +): (...args: Parameters) => void { + const timeoutRef = useRef | null>(null); + + const debounced = useCallback( + (...args: Parameters) => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => fn(...args), delay); + }, + [fn, delay], + ); + + return debounced; +} From e4bf036c2f2ba383529cb1d3c75c8a1c8d18e408 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Fri, 12 Sep 2025 11:35:57 +0530 Subject: [PATCH 05/56] add polling --- .../context/BreakoutRoomContext.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index add45850c..5d3d543b9 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -24,6 +24,7 @@ import { } from '../state/reducer'; import {useLocalUid} from '../../../../agora-rn-uikit'; import {useContent} from '../../../../customization-api'; +import {useLocation} from '../../Router'; import events, {PersistanceLevel} from '../../../rtm-events-api'; import {BreakoutRoomAction, initialBreakoutGroups} from '../state/reducer'; import {BreakoutRoomEventNames} from '../events/constants'; @@ -255,6 +256,8 @@ const BreakoutRoomProvider = ({ const { data: {isHost, roomId}, } = useRoomInfo(); + const location = useLocation(); + const isInBreakoutRoute = location.pathname.includes('breakout'); const breakoutRoomExit = useBreakoutRoomExit(handleLeaveBreakout); const [state, baseDispatch] = useReducer( breakoutRoomReducer, @@ -769,16 +772,16 @@ const BreakoutRoomProvider = ({ }, []); // Automatic interval management with cleanup only host will poll - // useEffect(() => { - // if ( - // isHostRef.current && - // !isPollingPaused && - // (stateRef.current.breakoutSessionId || isInBreakoutRoute) - // ) { - // const interval = setInterval(pollBreakoutGetAPI, 2000); - // return () => clearInterval(interval); - // } - // }, [isPollingPaused, isInBreakoutRoute, pollBreakoutGetAPI]); + useEffect(() => { + if ( + isHostRef.current && + !isPollingPaused && + (stateRef.current.breakoutSessionId || isInBreakoutRoute) + ) { + const interval = setInterval(pollBreakoutGetAPI, 2000); + return () => clearInterval(interval); + } + }, [isPollingPaused, isInBreakoutRoute, pollBreakoutGetAPI]); const upsertBreakoutRoomAPI = useCallback( async (type: 'START' | 'UPDATE' = 'START', retryCount = 0) => { From 16c23242fd19a3517b0d4c7b6802148edde7e8cd Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Fri, 12 Sep 2025 13:55:16 +0530 Subject: [PATCH 06/56] fix polling --- .../context/BreakoutRoomContext.tsx | 246 ++++++++++-------- 1 file changed, 133 insertions(+), 113 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 5d3d543b9..0ed271909 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -254,8 +254,9 @@ const BreakoutRoomProvider = ({ const {defaultContent, activeUids} = useContent(); const localUid = useLocalUid(); const { - data: {isHost, roomId}, + data: {isHost, roomId: joinRoomId}, } = useRoomInfo(); + console.log('supriya-room-data', joinRoomId); const location = useLocation(); const isInBreakoutRoute = location.pathname.includes('breakout'); const breakoutRoomExit = useBreakoutRoomExit(handleLeaveBreakout); @@ -637,151 +638,164 @@ const BreakoutRoomProvider = ({ // Check if there is already an active breakout session // We can call this to trigger sync events - const checkIfBreakoutRoomSessionExistsAPI = async (): Promise => { - console.log( - 'supriya-state-sync calling checkIfBreakoutRoomSessionExistsAPI', - ); - const startTime = Date.now(); - const requestId = getUniqueID(); - const url = `${ - $config.BACKEND_ENDPOINT - }/v1/channel/breakout-room?passphrase=${ - isHostRef.current ? roomId.host : roomId.attendee - }`; - - // Log internals for breakout room lifecycle - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Checking active session', - { - isHost: isHostRef.current, - sessionId: stateRef.current.breakoutSessionId, - }, - ); - - try { - const response = await fetch(url, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - authorization: store.token ? `Bearer ${store.token}` : '', - 'X-Request-Id': requestId, - 'X-Session-Id': logger.getSessionId(), - }, - }); - - // 🛡️ Guard against component unmount after fetch - if (!isMountedRef.current) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Check session API cancelled - component unmounted', - {requestId}, - ); + const checkIfBreakoutRoomSessionExistsAPI = + useCallback(async (): Promise => { + // Skip API call if roomId is not available or if API update is in progress + if (!joinRoomId?.host && !joinRoomId?.attendee) { + console.log('supriya-polling: Skipping - no roomId available'); return false; } - const latency = Date.now() - startTime; + if (isBreakoutUpdateInFlight) { + console.log('supriya-polling: Skipping - API update in progress'); + return false; + } + console.log( + 'supriya-state-sync calling checkIfBreakoutRoomSessionExistsAPI', + joinRoomId, + isHostRef.current, + ); + const startTime = Date.now(); + const requestId = getUniqueID(); + const url = `${ + $config.BACKEND_ENDPOINT + }/v1/channel/breakout-room?passphrase=${ + isHostRef.current ? joinRoomId.host : joinRoomId.attendee + }`; - // Log network request + // Log internals for breakout room lifecycle logger.log( - LogSource.NetworkRest, - 'breakout-room', - 'GET breakout-room session', + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Checking active session', { - url, - method: 'GET', - status: response.status, - latency, - requestId, + isHost: isHostRef.current, + sessionId: stateRef.current.breakoutSessionId, }, ); - if (response.status === 204) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'No active session found', - ); - return false; - } - - if (!response.ok) { - throw new Error(`Failed with status ${response.status}`); - } + try { + const response = await fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + authorization: store.token ? `Bearer ${store.token}` : '', + 'X-Request-Id': requestId, + 'X-Session-Id': logger.getSessionId(), + }, + }); - const data = await response.json(); + // 🛡️ Guard against component unmount after fetch + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Check session API cancelled - component unmounted', + {requestId}, + ); + return false; + } - // 🛡️ Guard against component unmount after JSON parsing - if (!isMountedRef.current) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Session sync cancelled - component unmounted after parsing', - {requestId}, - ); - return false; - } + const latency = Date.now() - startTime; - if (data?.session_id) { + // Log network request logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Session synced successfully', + LogSource.NetworkRest, + 'breakout-room', + 'GET breakout-room session', { - sessionId: data.session_id, - roomCount: data?.breakout_room?.length || 0, - assignmentType: data?.assignment_type, - switchRoom: data?.switch_room, + url, + method: 'GET', + status: response.status, + latency, + requestId, }, ); - dispatch({ - type: BreakoutGroupActionTypes.SYNC_STATE, - payload: { - sessionId: data.session_id, - rooms: data?.breakout_room || [], - assignmentStrategy: - data?.assignment_type || RoomAssignmentStrategy.NO_ASSIGN, - switchRoom: data?.switch_room ?? true, - }, + if (response.status === 204) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'No active session found', + ); + return false; + } + + if (!response.ok) { + throw new Error(`Failed with status ${response.status}`); + } + + const data = await response.json(); + + // 🛡️ Guard against component unmount after JSON parsing + if (!isMountedRef.current) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Session sync cancelled - component unmounted after parsing', + {requestId}, + ); + return false; + } + + if (data?.session_id) { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + 'Session synced successfully', + { + sessionId: data.session_id, + roomCount: data?.breakout_room?.length || 0, + assignmentType: data?.assignment_type, + switchRoom: data?.switch_room, + }, + ); + + dispatch({ + type: BreakoutGroupActionTypes.SYNC_STATE, + payload: { + sessionId: data.session_id, + rooms: data?.breakout_room || [], + assignmentStrategy: + data?.assignment_type || RoomAssignmentStrategy.NO_ASSIGN, + switchRoom: data?.switch_room ?? true, + }, + }); + return true; + } + + return false; + } catch (error) { + const latency = Date.now() - startTime; + logger.log(LogSource.NetworkRest, 'breakout-room', 'API call failed', { + url, + method: 'GET', + error: error.message, + latency, + requestId, }); - return true; + return false; } - - return false; - } catch (error) { - const latency = Date.now() - startTime; - logger.log(LogSource.NetworkRest, 'breakout-room', 'API call failed', { - url, - method: 'GET', - error: error.message, - latency, - requestId, - }); - return false; - } - }; + }, [isBreakoutUpdateInFlight, dispatch, joinRoomId, store.token]); // Polling for sync event const pollBreakoutGetAPI = useCallback(async () => { if (isHostRef.current && stateRef.current.breakoutSessionId) { await checkIfBreakoutRoomSessionExistsAPI(); } - }, []); + }, [checkIfBreakoutRoomSessionExistsAPI]); // Automatic interval management with cleanup only host will poll useEffect(() => { if ( isHostRef.current && !isPollingPaused && - (stateRef.current.breakoutSessionId || isInBreakoutRoute) + stateRef.current.breakoutSessionId ) { const interval = setInterval(pollBreakoutGetAPI, 2000); return () => clearInterval(interval); } - }, [isPollingPaused, isInBreakoutRoute, pollBreakoutGetAPI]); + }, [isPollingPaused, pollBreakoutGetAPI]); const upsertBreakoutRoomAPI = useCallback( async (type: 'START' | 'UPDATE' = 'START', retryCount = 0) => { @@ -819,7 +833,7 @@ const BreakoutRoomProvider = ({ stateRef.current.breakoutSessionId || randomNameGenerator(6); const payload: UpsertPayload = { - passphrase: isHostRef.current ? roomId.host : roomId.attendee, + passphrase: isHostRef.current ? joinRoomId.host : joinRoomId.attendee, switch_room: stateRef.current.canUserSwitchRoom, session_id: sessionId, assignment_type: stateRef.current.assignmentStrategy, @@ -993,7 +1007,13 @@ const BreakoutRoomProvider = ({ } } }, - [roomId.host, store.token, dispatch, selfJoinRoomId, roomId.attendee], + [ + joinRoomId.host, + store.token, + dispatch, + selfJoinRoomId, + joinRoomId.attendee, + ], ); const setManualAssignments = useCallback( From ebf26bb8e829b0cc33b322c9edf57cfde04892e4 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Fri, 12 Sep 2025 14:24:24 +0530 Subject: [PATCH 07/56] fix user check consition --- .../breakout-room/context/BreakoutRoomContext.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 0ed271909..b79b60229 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -581,12 +581,14 @@ const BreakoutRoomProvider = ({ // 1. Custom content (not type 'rtc') // 2. Screenshare UIDs // 3. Offline users + console.log('supriya-breakoutSessionId', stateRef); if (!stateRef?.current?.breakoutSessionId) { return; } const filteredParticipants = activeUids .filter(uid => { const user = defaultContentRef.current[uid]; + console.log('supriya-breakoutSessionId user: ', user); if (!user) { return false; } @@ -598,8 +600,8 @@ const BreakoutRoomProvider = ({ if (user.offline) { return false; } - // Exclude hosts - if (user?.isHost) { + // // Exclude hosts + if (user?.isHost === 'true') { return false; } // Exclude screenshare UIDs (they typically have a parentUid) From b754744cc8da719745ae7527aba2dacdc8f3b009 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Thu, 18 Sep 2025 12:23:05 +0530 Subject: [PATCH 08/56] QA breakout/Add testing feedback Part 1 (#750) add qa feedbacks --- config.json | 14 +- package.json | 2 +- .../ai-agent/components/ControlButtons.tsx | 2 +- template/src/components/ChatContext.ts | 4 +- template/src/components/Controls.tsx | 2 + template/src/components/EventsConfigure.tsx | 39 +- template/src/components/Navbar.tsx | 25 +- template/src/components/RTMConfigure.tsx | 925 +---------------- .../UserGlobalPreferenceProvider.tsx | 226 ++++ .../context/BreakoutRoomContext.tsx | 478 +++++---- .../events/BreakoutRoomEventsConfigure.tsx | 4 +- .../hoc/BreakoutRoomNameRenderer.tsx | 68 ++ .../components/breakout-room/state/reducer.ts | 21 +- .../components/breakout-room/state/types.ts | 21 +- .../breakout-room/ui/BreakoutMeetingTitle.tsx | 60 ++ .../ui/BreakoutRoomGroupSettings.tsx | 24 +- .../ui/BreakoutRoomMainRoomUsers.tsx | 175 +--- .../ui/BreakoutRoomParticipants.tsx | 1 + .../ui/ExitBreakoutRoomIconButton.tsx | 67 +- .../ui/ParticipantManualAssignmentModal.tsx | 16 +- .../chat-messages/useChatMessages.tsx | 15 +- .../participants/UserActionMenuOptions.tsx | 2 +- .../precall/joinWaitingRoomBtn.native.tsx | 20 +- .../components/precall/joinWaitingRoomBtn.tsx | 24 +- template/src/components/useUserPreference.tsx | 45 +- .../components/virtual-background/useVB.tsx | 18 + template/src/pages/VideoCall.tsx | 183 ++-- .../video-call/BreakoutVideoCallContent.tsx | 182 ++-- .../src/pages/video-call/VideoCallContent.tsx | 21 +- .../video-call/VideoCallStateWrapper.tsx | 22 +- template/src/rtm-events-api/Events.ts | 6 +- template/src/rtm/RTMConfigure-v2.tsx | 774 -------------- .../rtm/RTMConfigureBreakoutRoomProvider.tsx | 973 ++++++++++++++++++ .../src/rtm/RTMConfigureMainRoomProvider.tsx | 674 ++++++++++++ template/src/rtm/RTMCoreProvider.tsx | 18 +- template/src/rtm/RTMEngine.ts | 90 +- template/src/rtm/RTMGlobalStateProvider.tsx | 759 ++++++++++++++ template/src/rtm/constants.ts | 4 + .../rtm/hooks/useMainRoomUserDisplayName.ts | 35 + template/src/subComponents/ChatBubble.tsx | 6 +- template/src/subComponents/ChatContainer.tsx | 19 +- template/src/subComponents/LocalAudioMute.tsx | 4 +- template/src/subComponents/LocalVideoMute.tsx | 4 +- .../ScreenshareConfigure.native.tsx | 2 +- .../waiting-rooms/WaitingRoomControls.tsx | 11 +- template/src/utils/useMuteToggleLocal.ts | 42 +- 46 files changed, 3716 insertions(+), 2411 deletions(-) create mode 100644 template/src/components/UserGlobalPreferenceProvider.tsx create mode 100644 template/src/components/breakout-room/hoc/BreakoutRoomNameRenderer.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutMeetingTitle.tsx delete mode 100644 template/src/rtm/RTMConfigure-v2.tsx create mode 100644 template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx create mode 100644 template/src/rtm/RTMConfigureMainRoomProvider.tsx create mode 100644 template/src/rtm/RTMGlobalStateProvider.tsx create mode 100644 template/src/rtm/constants.ts create mode 100644 template/src/rtm/hooks/useMainRoomUserDisplayName.ts diff --git a/config.json b/config.json index 959b2b5cc..8f295ea69 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "APP_ID": "2ab137bc27094a03b48afada20014df4", + "APP_ID": "aae40f7b5ab348f2a27e992c9f3e13a7", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, "ENABLE_APPLE_OAUTH": false, @@ -9,7 +9,7 @@ "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROJECT_ID": "3c13e13f4dfa39d6a3af", + "PROJECT_ID": "8e5a647465fb0a5e9006", "RECORDING_MODE": "MIX", "APP_CERTIFICATE": "", "CUSTOMER_ID": "", @@ -32,18 +32,18 @@ "EVENT_MODE": false, "RAISE_HAND": false, "AUDIO_ROOM": false, - "PRODUCT_ID": "breakoutroom", - "APP_NAME": "BreakoutRoom", + "PRODUCT_ID": "breakoutroomfeature", + "APP_NAME": "BreakoutRoomFeature", "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", "ICON": "", "PRIMARY_COLOR": "#00AEFC", "FRONTEND_ENDPOINT": "", - "BACKEND_ENDPOINT": "https://managedservices-staging.rteappbuilder.com", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", "PSTN": false, "PRECALL": true, "CHAT": true, "CHAT_ORG_NAME": "61394961", - "CHAT_APP_NAME": "1572645", + "CHAT_APP_NAME": "1573238", "CHAT_URL": "https://a61.chat.agora.io", "CLOUD_RECORDING": true, "SCREEN_SHARING": true, @@ -87,7 +87,7 @@ "ACTIVE_SPEAKER": true, "WHITEBOARD_APPIDENTIFIER": "", "WHITEBOARD_REGION": "us-sv", - "ENABLE_NOISE_CANCELLATION": true, + "ENABLE_NOISE_CANCELLATION": false, "ENABLE_VIRTUAL_BACKGROUND": true, "ENABLE_WHITEBOARD": false, "ENABLE_WHITEBOARD_FILE_UPLOAD": false, diff --git a/package.json b/package.json index 8a5223bd2..bf897576a 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ ], "scripts": { "vercel-build": "npm run dev-setup && cd template && npm run web:build && cd .. && npm run copy-vercel", - "uikit": "rm -rf template/agora-rn-uikit && git clone https://github.com/AgoraIO-Community/appbuilder-ui-kit.git template/agora-rn-uikit && cd template/agora-rn-uikit && git checkout appbuilder-uikit-3.1.8", + "uikit": "rm -rf template/agora-rn-uikit && git clone https://github.com/AgoraIO-Community/appbuilder-ui-kit.git template/agora-rn-uikit && cd template/agora-rn-uikit && git checkout appbuilder-uikit-4.0.0-beta", "deps": "cd template && npm i --force", "dev-setup": "npm run uikit && npm run deps && node devSetup.js", "web-build": "cd template && npm run web:build && cd .. && npm run copy-vercel", diff --git a/template/src/ai-agent/components/ControlButtons.tsx b/template/src/ai-agent/components/ControlButtons.tsx index 5f19fe2c8..b3c6a320c 100644 --- a/template/src/ai-agent/components/ControlButtons.tsx +++ b/template/src/ai-agent/components/ControlButtons.tsx @@ -31,7 +31,7 @@ export const MicButton = () => { borderRadius: 50, marginHorizontal: 8, }} - onPress={() => muteToggle(MUTE_LOCAL_TYPE.audio)}> + onPress={async () => await muteToggle(MUTE_LOCAL_TYPE.audio)}> ) => void; } export enum controlMessageEnum { diff --git a/template/src/components/Controls.tsx b/template/src/components/Controls.tsx index 556dbcc86..aa49a6893 100644 --- a/template/src/components/Controls.tsx +++ b/template/src/components/Controls.tsx @@ -709,6 +709,8 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { icon: isScreenshareActive ? 'stop-screen-share' : 'screen-share', iconColor: isScreenshareActive ? $config.SEMANTIC_ERROR + : !canScreenshareInBreakoutRoom + ? $config.SEMANTIC_NEUTRAL : $config.SECONDARY_ACTION_COLOR, textColor: isScreenshareActive ? $config.SEMANTIC_ERROR diff --git a/template/src/components/EventsConfigure.tsx b/template/src/components/EventsConfigure.tsx index 61d10b8e0..3167997a9 100644 --- a/template/src/components/EventsConfigure.tsx +++ b/template/src/components/EventsConfigure.tsx @@ -268,7 +268,8 @@ const EventsConfigure: React.FC = ({ permissionStatusRef.current = permissionStatus; }, [permissionStatus]); - const {hasUserJoinedRTM, isInitialQueueCompleted} = useContext(ChatContext); + const {hasUserJoinedRTM, isInitialQueueCompleted, syncUserState} = + useContext(ChatContext); const {startSpeechToText, addStreamMessageListener} = useSpeechToText(); //auto start stt @@ -612,10 +613,11 @@ const EventsConfigure: React.FC = ({ if (!isHostRef.current) return; const {attendee_uid, approved} = JSON.parse(data?.payload); // update waiting room status in other host's panel - dispatch({ - type: 'UpdateRenderList', - value: [attendee_uid, {isInWaitingRoom: false}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [attendee_uid, {isInWaitingRoom: false}], + // }); + syncUserState(attendee_uid, {isInWaitingRoom: false}); waitingRoomRef.current[attendee_uid] = approved ? 'APPROVED' : 'REJECTED'; // hide toast in other host's screen @@ -660,10 +662,11 @@ const EventsConfigure: React.FC = ({ defaultContentRef.current.defaultContent[attendee_uid]?.name || 'Attendee'; // put the attendee in waitingroom in renderlist - dispatch({ - type: 'UpdateRenderList', - value: [attendee_uid, {isInWaitingRoom: true}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [attendee_uid, {isInWaitingRoom: true}], + // }); + syncUserState(attendee_uid, {isInWaitingRoom: true}); waitingRoomRef.current[attendee_uid] = 'PENDING'; // check if any other host has approved then dont show permission to join the room @@ -676,10 +679,11 @@ const EventsConfigure: React.FC = ({ attendee_screenshare_uid: attendee_screenshare_uid, approved: true, }); - dispatch({ - type: 'UpdateRenderList', - value: [attendee_uid, {isInWaitingRoom: false}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [attendee_uid, {isInWaitingRoom: false}], + // }); + syncUserState(attendee_uid, {isInWaitingRoom: false}); waitingRoomRef.current[attendee_uid] = 'APPROVED'; @@ -724,10 +728,11 @@ const EventsConfigure: React.FC = ({ attendee_screenshare_uid: attendee_screenshare_uid, approved: false, }); - dispatch({ - type: 'UpdateRenderList', - value: [attendee_uid, {isInWaitingRoom: false}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [attendee_uid, {isInWaitingRoom: false}], + // }); + syncUserState(attendee_uid, {isInWaitingRoom: false}); waitingRoomRef.current[attendee_uid] = 'REJECTED'; diff --git a/template/src/components/Navbar.tsx b/template/src/components/Navbar.tsx index abb66fd91..7ff2a9613 100644 --- a/template/src/components/Navbar.tsx +++ b/template/src/components/Navbar.tsx @@ -70,6 +70,7 @@ import { SettingsToolbarItem, } from './controls/toolbar-items'; import {useControlPermissionMatrix} from './controls/useControlPermissionMatrix'; +import BreakoutMeetingTitle from './breakout-room/ui/BreakoutMeetingTitle'; export const ParticipantsCountView = ({ isMobileView = false, @@ -376,13 +377,16 @@ export const MeetingTitleToolbarItem = () => { } = useRoomInfo(); return ( - - {trimText(meetingTitle)} - + + + {trimText(meetingTitle)} + + + ); }; @@ -632,12 +636,11 @@ const style = StyleSheet.create({ roomNameContainer: { zIndex: 10, flex: 1, - flexDirection: 'row', - alignItems: 'center', + flexDirection: 'column', + alignItems: 'flex-start', + paddingLeft: 13, }, - roomNameText: { - alignSelf: 'center', fontSize: ThemeConfig.FontSize.normal, color: $config.FONT_COLOR, fontWeight: '600', diff --git a/template/src/components/RTMConfigure.tsx b/template/src/components/RTMConfigure.tsx index 858c7f4d8..38d0a0346 100644 --- a/template/src/components/RTMConfigure.tsx +++ b/template/src/components/RTMConfigure.tsx @@ -2,931 +2,56 @@ ******************************************** Copyright © 2021 Agora Lab, Inc., all rights reserved. AppBuilder and all associated components, source code, APIs, services, and documentation - (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. Use without a license or in violation of any license terms and conditions (including use for - any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more information visit https://appbuilder.agora.io. ********************************************* */ -import React, {useState, useContext, useEffect, useRef} from 'react'; -import { - type GetChannelMetadataResponse, - type GetOnlineUsersResponse, - type LinkStateEvent, - type MessageEvent, - type Metadata, - type PresenceEvent, - type SetOrUpdateUserMetadataOptions, - type StorageEvent, - type RTMClient, - type GetUserMetadataResponse, -} from 'agora-react-native-rtm'; -import { - ContentInterface, - DispatchContext, - PropsContext, - UidType, - useLocalUid, -} from '../../agora-rn-uikit'; +import React from 'react'; +import {useLocalUid} from '../../agora-rn-uikit'; import ChatContext from './ChatContext'; -import {Platform} from 'react-native'; -import {backOff} from 'exponential-backoff'; -import {isAndroid, isIOS, isWeb, isWebInternal} from '../utils/common'; -import {useContent} from 'customization-api'; -import { - safeJsonParse, - timeNow, - hasJsonStructure, - getMessageTime, - get32BitUid, -} from '../rtm/utils'; -import {EventUtils, EventsQueue} from '../rtm-events'; -import {PersistanceLevel} from '../rtm-events-api'; -import RTMEngine from '../rtm/RTMEngine'; -import {filterObject} from '../utils'; -import SDKEvents from '../utils/SdkEvents'; -import isSDK from '../utils/isSDK'; -import {useAsyncEffect} from '../utils/useAsyncEffect'; -import { - WaitingRoomStatus, - useRoomInfo, -} from '../components/room-info/useRoomInfo'; -import LocalEventEmitter, { - LocalEventsEnum, -} from '../rtm-events-api/LocalEvents'; -import {controlMessageEnum} from '../components/ChatContext'; -import {LogSource, logger} from '../logger/AppBuilderLogger'; -import {RECORDING_BOT_UID} from '../utils/constants'; -import { - nativeChannelTypeMapping, - nativeLinkStateMapping, - nativePresenceEventTypeMapping, - nativeStorageEventTypeMapping, -} from '../../bridge/rtm/web/Types'; import {useRTMCore} from '../rtm/RTMCoreProvider'; - -export enum UserType { - ScreenShare = 'screenshare', -} - -const eventTimeouts = new Map>(); +import {useRTMConfigureMain} from '../rtm/RTMConfigureMainRoomProvider'; +import {useRTMConfigureBreakout} from '../rtm/RTMConfigureBreakoutRoomProvider'; +import {RTM_ROOMS} from '../rtm/constants'; interface Props { - callActive: boolean; + room: RTM_ROOMS; children: React.ReactNode; - channelName: string; } const RtmConfigure = (props: Props) => { - const rtmInitTimstamp = new Date().getTime(); const localUid = useLocalUid(); - const {callActive, channelName} = props; - const {dispatch} = useContext(DispatchContext); - const {defaultContent, activeUids} = useContent(); - const { - waitingRoomStatus, - data: {isHost}, - } = useRoomInfo(); - const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); - const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); - const [onlineUsersCount, setTotalOnlineUsers] = useState(0); - const timerValueRef: any = useRef(5); - // Track RTM connection state (equivalent to v1.5x connectionState check) - const {client, isLoggedIn, registerCallbacks, unregisterCallbacks} = - useRTMCore(); - - /** - * inside event callback state won't have latest value. - * so creating ref to access the state - */ - const isHostRef = useRef({isHost: isHost}); - useEffect(() => { - isHostRef.current.isHost = isHost; - }, [isHost]); - - const waitingRoomStatusRef = useRef({waitingRoomStatus: waitingRoomStatus}); - useEffect(() => { - waitingRoomStatusRef.current.waitingRoomStatus = waitingRoomStatus; - }, [waitingRoomStatus]); - - const activeUidsRef = useRef({activeUids: activeUids}); - useEffect(() => { - activeUidsRef.current.activeUids = activeUids; - }, [activeUids]); - - const defaultContentRef = useRef({defaultContent: defaultContent}); - useEffect(() => { - defaultContentRef.current.defaultContent = defaultContent; - }, [defaultContent]); - - // Eventdispatcher timeout refs clean - const isRTMMounted = useRef(true); - useEffect(() => { - return () => { - isRTMMounted.current = false; - // Clear all pending timeouts on unmount - for (const timeout of eventTimeouts.values()) { - clearTimeout(timeout); - } - eventTimeouts.clear(); - }; - }, []); - - // Set online users - React.useEffect(() => { - setTotalOnlineUsers( - Object.keys( - filterObject( - defaultContent, - ([k, v]) => - v?.type === 'rtc' && - !v.offline && - activeUidsRef.current.activeUids.indexOf(v?.uid) !== -1, - ), - ).length, - ); - }, [defaultContent]); - - // React.useEffect(() => { - // // If its not a convo ai project and - // // the platform is web execute the window listeners - // if (!$config.ENABLE_CONVERSATIONAL_AI && isWebInternal()) { - // const handBrowserClose = ev => { - // ev.preventDefault(); - // return (ev.returnValue = 'Are you sure you want to exit?'); - // }; - - // const logoutRtm = () => { - // try { - // if (client && RTMEngine.getInstance().channelUid) { - // // First unsubscribe from channel (like v1.5x leaveChannel) - // client.unsubscribe(RTMEngine.getInstance().channelUid); - // // Then logout - // client.logout(); - // } - // } catch (error) { - // console.error('Error during browser close RTM cleanup:', error); - // } - // }; + const {room} = props; + const {client} = useRTMCore(); - // // Set up window listeners - // window.addEventListener( - // 'beforeunload', - // isWeb() && !isSDK() ? handBrowserClose : () => {}, - // ); + // Call hooks unconditionally, but only use data based on room type + let rtmMainData = null; + let rtmBreakoutData = null; + rtmMainData = useRTMConfigureMain(); + rtmBreakoutData = useRTMConfigureBreakout(); - // window.addEventListener('pagehide', logoutRtm); - // return () => { - // // Remove listeners on unmount - // window.removeEventListener( - // 'beforeunload', - // isWeb() && !isSDK() ? handBrowserClose : () => {}, - // ); - // window.removeEventListener('pagehide', logoutRtm); - // }; - // } - // }, []); + const rtmData = room === RTM_ROOMS.MAIN ? rtmMainData : rtmBreakoutData; - const init = async () => { - await subscribeChannel(); - setHasUserJoinedRTM(true); - await runQueuedEvents(); - setIsInitialQueueCompleted(true); - logger.log(LogSource.AgoraSDK, 'Log', 'RTM queued events finished running'); - }; - - const subscribeChannel = async () => { - try { - if (RTMEngine.getInstance().allChannels.includes(channelName)) { - logger.debug( - LogSource.AgoraSDK, - 'Log', - '🚫 RTM already subscribed channel skipping', - channelName, - ); - } else { - await client.subscribe(channelName, { - withMessage: true, - withPresence: true, - withMetadata: true, - withLock: false, - }); - logger.log(LogSource.AgoraSDK, 'API', 'RTM subscribeChannel', { - data: channelName, - }); - - // Set channel ID AFTER successful subscribe (like v1.5x) - console.log('setting primary channel', channelName); - RTMEngine.getInstance().addChannel(channelName, true); - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM setChannelId as subscribe is successful', - channelName, - ); - logger.debug( - LogSource.SDK, - 'Event', - 'Emitting rtm joined', - channelName, - ); - // @ts-ignore - SDKEvents.emit('_rtm-joined', channelName); - timerValueRef.current = 5; - await getMembers(); - await readAllChannelAttributes(); - logger.log( - LogSource.AgoraSDK, - 'Log', - 'RTM readAllChannelAttributes and getMembers done', - ); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'Log', - 'RTM subscribeChannel failed..Trying again', - {error}, - ); - setTimeout(async () => { - // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); - subscribeChannel(); - }, timerValueRef.current * 1000); - } - }; - - const getMembers = async () => { - try { - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM presence.getOnlineUsers(getMembers) start', - ); - await client.presence - .getOnlineUsers(channelName, 1) - .then(async (data: GetOnlineUsersResponse) => { - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM presence.getOnlineUsers data received', - data, - ); - await Promise.all( - data.occupants?.map(async member => { - try { - const backoffAttributes = - await fetchUserAttributesWithBackoffRetry(member.userId); - - await processUserUidAttributes( - backoffAttributes, - member.userId, - ); - // setting screenshare data - // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) - // isActive to identify all active screenshare users in the call - backoffAttributes?.items?.forEach(item => { - try { - if (hasJsonStructure(item.value as string)) { - const data = { - evt: item.key, // Use item.key instead of key - value: item.value, // Use item.value instead of value - }; - // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events - EventsQueue.enqueue({ - data: data, - uid: member.userId, - ts: timeNow(), - }); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'Log', - `RTM Failed to process user attribute item for ${ - member.userId - }: ${JSON.stringify(item)}`, - {error}, - ); - // Continue processing other items - } - }); - } catch (e) { - logger.error( - LogSource.AgoraSDK, - 'Log', - `RTM Could not retrieve name of ${member.userId}`, - {error: e}, - ); - } - }), - ); - logger.debug( - LogSource.AgoraSDK, - 'Log', - 'RTM fetched all data and user attr...RTM init done', - ); - }); - timerValueRef.current = 5; - } catch (error) { - setTimeout(async () => { - // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); - await getMembers(); - }, timerValueRef.current * 1000); - } - }; - - const readAllChannelAttributes = async () => { - try { - await client.storage - .getChannelMetadata(channelName, 1) - .then(async (data: GetChannelMetadataResponse) => { - for (const item of data.items) { - try { - const {key, value, authorUserId, updateTs} = item; - if (hasJsonStructure(value as string)) { - const evtData = { - evt: key, - value, - }; - // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events - EventsQueue.enqueue({ - data: evtData, - uid: authorUserId, - ts: updateTs, - }); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'Log', - `RTM Failed to process channel attribute item: ${JSON.stringify( - item, - )}`, - {error}, - ); - // Continue processing other items - } - } - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM storage.getChannelMetadata data received', - data, - ); - }); - timerValueRef.current = 5; - } catch (error) { - setTimeout(async () => { - // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); - await readAllChannelAttributes(); - }, timerValueRef.current * 1000); - } - }; - - const fetchUserAttributesWithBackoffRetry = async ( - userId: string, - ): Promise => { - return backOff( - async () => { - logger.log( - LogSource.AgoraSDK, - 'API', - `RTM fetching getUserMetadata for member ${userId}`, - ); - - const attr: GetUserMetadataResponse = - await client.storage.getUserMetadata({ - userId: userId, - }); - - if (!attr || !attr.items) { - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM attributes for member not found', - ); - throw attr; - } - - logger.log( - LogSource.AgoraSDK, - 'API', - `RTM getUserMetadata for member ${userId} received`, - {attr}, - ); - - if (attr.items && attr.items.length > 0) { - return attr; - } else { - throw attr; - } - }, - { - retry: (e, idx) => { - logger.debug( - LogSource.AgoraSDK, - 'Log', - `RTM [retrying] Attempt ${idx}. Fetching ${userId}'s attributes`, - e, - ); - return true; - }, - }, + if (!rtmData) { + throw new Error( + `RTMConfigure: Invalid room prop '${room}' or missing context provider`, ); - }; - - const processUserUidAttributes = async ( - attr: GetUserMetadataResponse, - userId: string, - ) => { - try { - console.log('[user attributes]:', {attr}); - const uid = parseInt(userId, 10); - const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); - const isHostItem = attr?.items?.find(item => item.key === 'isHost'); - const screenUid = screenUidItem?.value - ? parseInt(screenUidItem.value, 10) - : undefined; - - //start - updating user data in rtc - const userData = { - screenUid: screenUid, - //below thing for livestreaming - type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', - uid, - offline: false, - isHost: isHostItem?.value || false, - lastMessageTimeStamp: 0, - }; - console.log('new user joined', uid, userData); - updateRenderListState(uid, userData); - //end- updating user data in rtc - - //start - updating screenshare data in rtc - if (screenUid) { - const screenShareUser = { - type: UserType.ScreenShare, - parentUid: uid, - }; - updateRenderListState(screenUid, screenShareUser); - } - //end - updating screenshare data in rtc - } catch (e) { - logger.error( - LogSource.AgoraSDK, - 'Event', - `RTM Failed to process user data for ${userId}`, - {error: e}, - ); - } - }; - - const updateRenderListState = ( - uid: number, - data: Partial, - ) => { - dispatch({type: 'UpdateRenderList', value: [uid, data]}); - }; - - const runQueuedEvents = async () => { - try { - while (!EventsQueue.isEmpty()) { - const currEvt = EventsQueue.dequeue(); - await eventDispatcher(currEvt.data, `${currEvt.uid}`, currEvt.ts); - } - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while running queue events', - {error}, - ); - } - }; - - const eventDispatcher = async ( - data: { - evt: string; - value: string; - feat?: string; - etyp?: string; - }, - sender: string, - ts: number, - ) => { - console.log( - LogSource.Events, - 'CUSTOM_EVENTS', - 'inside eventDispatcher ', - data, - ); - - let evt = '', - value = ''; - - if (data?.feat === 'BREAKOUT_ROOM') { - const outputData = { - evt: `${data.feat}_${data.etyp}`, - payload: JSON.stringify({ - data: data.data, - action: data.act, - }), - persistLevel: 1, - source: 'core', - }; - const formattedData = JSON.stringify(outputData); - evt = data.feat + '_' + data.etyp; - value = formattedData; - } else if (data?.feat === 'WAITING_ROOM') { - if (data?.etyp === 'REQUEST') { - const outputData = { - evt: `${data.feat}_${data.etyp}`, - payload: JSON.stringify({ - attendee_uid: data.data.data.attendee_uid, - attendee_screenshare_uid: data.data.data.attendee_screenshare_uid, - }), - persistLevel: 1, - source: 'core', - }; - const formattedData = JSON.stringify(outputData); - evt = data.feat + '_' + data.etyp; - value = formattedData; - } - if (data?.etyp === 'RESPONSE') { - const outputData = { - evt: `${data.feat}_${data.etyp}`, - payload: JSON.stringify({ - approved: data.data.data.approved, - channelName: data.data.data.channel_name, - mainUser: data.data.data.mainUser, - screenShare: data.data.data.screenShare, - whiteboard: data.data.data.whiteboard, - chat: data.data.data?.chat, - }), - persistLevel: 1, - source: 'core', - }; - const formattedData = JSON.stringify(outputData); - evt = data.feat + '_' + data.etyp; - value = formattedData; - } - } else { - if ( - $config.ENABLE_WAITING_ROOM && - !isHostRef.current?.isHost && - waitingRoomStatusRef.current?.waitingRoomStatus !== - WaitingRoomStatus.APPROVED - ) { - if ( - data.evt === controlMessageEnum.muteAudio || - data.evt === controlMessageEnum.muteVideo - ) { - return; - } else { - evt = data.evt; - value = data.value; - } - } else { - evt = data.evt; - value = data.value; - } - } - - try { - let parsedValue; - try { - parsedValue = typeof value === 'string' ? JSON.parse(value) : value; - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'RTM Failed to parse event value in event dispatcher:', - {error}, - ); - return; - } - const {payload, persistLevel, source} = parsedValue; - // Step 1: Set local attributes - if (persistLevel === PersistanceLevel.Session) { - const rtmAttribute = {key: evt, value: value}; - const options: SetOrUpdateUserMetadataOptions = { - userId: `${localUid}`, - }; - await client.storage.setUserMetadata( - { - items: [rtmAttribute], - }, - options, - ); - } - // Step 2: Emit the event - console.log(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: ', evt); - EventUtils.emitEvent(evt, source, {payload, persistLevel, sender, ts}); - // Because async gets evaluated in a different order when in an sdk - if (evt === 'name') { - // 1. Cancel existing timeout for this sender - if (eventTimeouts.has(sender)) { - clearTimeout(eventTimeouts.get(sender)!); - } - // 2. Create new timeout with tracking - const timeout = setTimeout(() => { - // 3. Guard against unmounted component - if (!isRTMMounted.current) { - return; - } - EventUtils.emitEvent(evt, source, { - payload, - persistLevel, - sender, - ts, - }); - // 4. Clean up after execution - eventTimeouts.delete(sender); - }, 200); - // 5. Track the timeout for cleanup - eventTimeouts.set(sender, timeout); - } - } catch (error) { - console.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while emiting event:', - {error}, - ); - } - }; - - const unsubscribeAndCleanup = async (channelName: string) => { - if (!callActive || !isLoggedIn) { - return; - } - try { - client.unsubscribe(channelName); - RTMEngine.getInstance().removeChannel(channelName); - logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); - if (isIOS() || isAndroid()) { - EventUtils.clear(); - } - setHasUserJoinedRTM(false); - setIsInitialQueueCompleted(false); - logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); - } catch (unsubscribeError) { - console.log('supriya error while unsubscribing: ', unsubscribeError); - } - }; - - // Register listeners when client is created - useEffect(() => { - if (!client) { - return; - } - - const handleStorageEvent = (storage: StorageEvent) => { - // when remote user sets/updates metadata - 3 - if ( - storage.eventType === nativeStorageEventTypeMapping.SET || - storage.eventType === nativeStorageEventTypeMapping.UPDATE - ) { - const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; - const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; - logger.log( - LogSource.AgoraSDK, - 'Event', - `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, - storage, - ); - try { - if (storage.data?.items && Array.isArray(storage.data.items)) { - storage.data.items.forEach(item => { - try { - if (!item || !item.key) { - logger.warn( - LogSource.Events, - 'CUSTOM_EVENTS', - 'Invalid storage item:', - item, - ); - return; - } - - const {key, value, authorUserId, updateTs} = item; - const timestamp = getMessageTime(updateTs); - const sender = Platform.OS - ? get32BitUid(authorUserId) - : parseInt(authorUserId, 10); - eventDispatcher( - { - evt: key, - value, - }, - `${sender}`, - timestamp, - ); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - `Failed to process storage item: ${JSON.stringify(item)}`, - {error}, - ); - } - }); - } - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } - }; - - const handlePresenceEvent = async (presence: PresenceEvent) => { - if (`${localUid}` === presence.publisher) { - return; - } - if (presence.channelName !== channelName) { - console.log( - 'supriya event recevied in channel', - presence.channelName, - channelName, - ); - return; - } - // remoteJoinChannel - if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { - logger.log( - LogSource.AgoraSDK, - 'Event', - 'RTM presenceEvent of type [3 - remoteJoin] (channelMemberJoined)', - ); - const backoffAttributes = await fetchUserAttributesWithBackoffRetry( - presence.publisher, - ); - await processUserUidAttributes(backoffAttributes, presence.publisher); - } - // remoteLeaveChannel - if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { - logger.log( - LogSource.AgoraSDK, - 'Event', - 'RTM presenceEvent of type [4 - remoteLeave] (channelMemberLeft)', - presence, - ); - // Chat of left user becomes undefined. So don't cleanup - const uid = presence?.publisher - ? parseInt(presence.publisher, 10) - : undefined; - - if (!uid) { - return; - } - SDKEvents.emit('_rtm-left', uid); - // updating the rtc data - updateRenderListState(uid, { - offline: true, - }); - } - }; - - const handleMessageEvent = (message: MessageEvent) => { - console.log('supriya current message channel: ', channelName); - console.log('supriya message event is', message); - // message - 1 (channel) - if (message.channelType === nativeChannelTypeMapping.MESSAGE) { - // here the channel name will be the channel name - logger.debug( - LogSource.Events, - 'CUSTOM_EVENTS', - 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', - message, - ); - const { - publisher: uid, - channelName, - message: text, - timestamp: ts, - } = message; - //whiteboard upload - if (parseInt(uid, 10) === 1010101) { - const [err, res] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - if (res?.data?.data?.images) { - LocalEventEmitter.emit( - LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, - res?.data?.data?.images, - ); - } - } else { - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - - const timestamp = getMessageTime(ts); - const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); - try { - eventDispatcher(msg, `${sender}`, timestamp); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } - } - - // message - 3 (user) - if (message.channelType === nativeChannelTypeMapping.USER) { - logger.debug( - LogSource.Events, - 'CUSTOM_EVENTS', - 'messageEvent of type [3- USER] (messageReceived)', - message, - ); - // here the (message.channelname) channel name will be the to UID - const {publisher: peerId, timestamp: ts, message: text} = message; - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - - const timestamp = getMessageTime(ts); - - const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); - - try { - eventDispatcher(msg, `${sender}`, timestamp); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } - }; - - registerCallbacks(channelName, { - storage: handleStorageEvent, - presence: handlePresenceEvent, - message: handleMessageEvent, - }); - - return () => { - unregisterCallbacks(channelName); - }; - }, [client, channelName]); - - useAsyncEffect(async () => { - try { - if (isLoggedIn && callActive) { - await init(); - } - } catch (error) { - logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); - } - return async () => { - await unsubscribeAndCleanup(channelName); - }; - }, [isLoggedIn, callActive, channelName]); + } return ( {props.children} diff --git a/template/src/components/UserGlobalPreferenceProvider.tsx b/template/src/components/UserGlobalPreferenceProvider.tsx new file mode 100644 index 000000000..3944c794c --- /dev/null +++ b/template/src/components/UserGlobalPreferenceProvider.tsx @@ -0,0 +1,226 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, { + createContext, + useContext, + useCallback, + useRef, + useState, +} from 'react'; +import { + ToggleState, + PermissionState, + DefaultContentInterface, +} from '../../agora-rn-uikit'; +import {MUTE_LOCAL_TYPE} from '../utils/useMuteToggleLocal'; + +// RTM User Preferences interface - session-scoped preferences that survive room transitions +export interface UserGlobalPreferences { + audioMuted: boolean; // false = unmuted (0), true = muted (1) + videoMuted: boolean; // false = unmuted (0), true = muted (1) + virtualBackground?: { + type: 'blur' | 'image' | 'none'; + imageUrl?: string; + blurIntensity?: number; + }; +} + +// Default user preferences +export const DEFAULT_USER_PREFERENCES: UserGlobalPreferences = { + audioMuted: false, // Default unmuted (0 = unmuted) + videoMuted: false, // Default unmuted (0 = unmuted) + virtualBackground: { + type: 'none', + }, +}; + +interface UserGlobalPreferenceInterface { + userGlobalPreferences: UserGlobalPreferences; + syncUserPreferences: (prefs: Partial) => void; + applyUserPreferences: ( + currentUserData: DefaultContentInterface, + toggleMuteFn: (type: number, action?: number) => Promise, + ) => Promise; +} + +const UserGlobalPreferenceContext = + createContext(null); + +interface UserGlobalPreferenceProviderProps { + children: React.ReactNode; +} + +export const UserGlobalPreferenceProvider: React.FC< + UserGlobalPreferenceProviderProps +> = ({children}) => { + // User preferences (survives room transitions) + const [userGlobalPreferences, setUserGlobalPreferences] = + useState(DEFAULT_USER_PREFERENCES); + console.log('UP: userGlobalPreferences changed: ', userGlobalPreferences); + + const hasAppliedPreferences = useRef(false); + + const syncUserPreferences = useCallback( + (prefs: Partial) => { + console.log('UserGlobalPreference: Syncing preferences', prefs); + setUserGlobalPreferences(prev => ({ + ...prev, + ...prefs, + })); + }, + [setUserGlobalPreferences], + ); + + const applyUserPreferences = useCallback( + async ( + currentUserData: DefaultContentInterface, + toggleMuteFn: ( + type: MUTE_LOCAL_TYPE, + action?: ToggleState, + ) => Promise, + ) => { + console.log('UP: 1', userGlobalPreferences); + // Only apply preferences once per component lifecycle + if (hasAppliedPreferences.current) { + console.log('UP: 2'); + console.log( + 'UserGlobalPreference: Preferences already applied, skipping', + ); + return; + } + console.log('UP: 3'); + try { + console.log( + 'UserGlobalPreference: Applying preferences', + userGlobalPreferences, + ); + console.log('UP: 4'); + + const currentAudioState = currentUserData.audio; + const currentVideoState = currentUserData.video; + const permissionStatus = currentUserData.permissionStatus; + const audioForceDisabled = currentUserData.audioForceDisabled; + const videoForceDisabled = currentUserData.videoForceDisabled; + + console.log('UP: 5', { + currentAudioState, + currentVideoState, + permissionStatus, + audioForceDisabled, + videoForceDisabled, + }); + + // Check if audio permissions are available and not force disabled + const hasAudioPermission = + (permissionStatus === PermissionState.GRANTED_FOR_CAM_AND_MIC || + permissionStatus === PermissionState.GRANTED_FOR_MIC_ONLY) && + !audioForceDisabled; + + // Check if video permissions are available and not force disabled + const hasVideoPermission = + (permissionStatus === PermissionState.GRANTED_FOR_CAM_AND_MIC || + permissionStatus === PermissionState.GRANTED_FOR_CAM_ONLY) && + !videoForceDisabled; + + // Apply audio mute preference only if user has audio permission and not force disabled + if (hasAudioPermission) { + const desiredAudioState = userGlobalPreferences.audioMuted + ? ToggleState.disabled + : ToggleState.enabled; + console.log('UP: 6', desiredAudioState); + + if (currentAudioState !== desiredAudioState) { + console.log('UP: 7 changed', currentAudioState, desiredAudioState); + + console.log( + `UP: UserGlobalPreference: Applying audio state: ${ + desiredAudioState === ToggleState.disabled ? 'muted' : 'unmuted' + }`, + desiredAudioState, + ); + await toggleMuteFn(MUTE_LOCAL_TYPE.audio, desiredAudioState); + } + } else { + console.log( + 'UP: Skipping audio preference - no audio permission or force disabled', + ); + } + + // Apply video mute preference only if user has video permission and not force disabled + if (hasVideoPermission) { + const desiredVideoState = userGlobalPreferences.videoMuted + ? ToggleState.disabled + : ToggleState.enabled; + console.log('UP: 8', currentVideoState, desiredVideoState); + + if (currentVideoState !== desiredVideoState) { + console.log('UP: 9 changed'); + + console.log( + `UserGlobalPreference: Applying video state: ${ + desiredVideoState === ToggleState.disabled ? 'muted' : 'unmuted' + }`, + ); + await toggleMuteFn(MUTE_LOCAL_TYPE.video, desiredVideoState); + } + } else { + console.log( + 'UP: Skipping video preference - no video permission or force disabled', + ); + } + + // Virtual background preferences will be handled by useVB hook + // since it reads from userGlobalPreferences state on component mount + + hasAppliedPreferences.current = true; + console.log('UserGlobalPreference: Preferences applied successfully'); + } catch (error) { + console.warn( + 'UserGlobalPreference: Failed to apply preferences:', + error, + ); + } + }, + [userGlobalPreferences], + ); + + // Reset the application flag when preferences change + // This allows re-application if preferences are updated + React.useEffect(() => { + hasAppliedPreferences.current = false; + }, [userGlobalPreferences]); + + const contextValue: UserGlobalPreferenceInterface = { + userGlobalPreferences, + syncUserPreferences, + applyUserPreferences, + }; + + return ( + + {children} + + ); +}; + +export const useUserGlobalPreferences = (): UserGlobalPreferenceInterface => { + const context = useContext(UserGlobalPreferenceContext); + if (!context) { + throw new Error( + 'useUserGlobalPreferences must be used within UserGlobalPreferenceProvider', + ); + } + return context; +}; + +export default UserGlobalPreferenceProvider; diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index b79b60229..5a8d144f7 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -24,7 +24,6 @@ import { } from '../state/reducer'; import {useLocalUid} from '../../../../agora-rn-uikit'; import {useContent} from '../../../../customization-api'; -import {useLocation} from '../../Router'; import events, {PersistanceLevel} from '../../../rtm-events-api'; import {BreakoutRoomAction, initialBreakoutGroups} from '../state/reducer'; import {BreakoutRoomEventNames} from '../events/constants'; @@ -33,6 +32,12 @@ import {IconsInterface} from '../../../atoms/CustomIcon'; import Toast from '../../../../react-native-toast-message'; import useBreakoutRoomExit from '../hooks/useBreakoutRoomExit'; import {useDebouncedCallback} from '../../../utils/useDebouncedCallback'; +import {useLocation} from '../../../components/Router'; +import {useMainRoomUserDisplayName} from '../../../rtm/hooks/useMainRoomUserDisplayName'; +import { + RTMUserData, + useRTMGlobalState, +} from '../../../rtm/RTMGlobalStateProvider'; const BREAKOUT_LOCK_TIMEOUT_MS = 5000; const HOST_OPERATION_LOCK_TIMEOUT_MS = 10000; // Emergency timeout for network failures only @@ -50,12 +55,40 @@ const HOST_BROADCASTED_OPERATIONS = [ BreakoutGroupActionTypes.RENAME_GROUP, ] as const; -const getSanitizedPayload = (payload: BreakoutGroup[]) => { +const getSanitizedPayload = ( + payload: BreakoutGroup[], + mainRoomRTMUsers: {[uid: number]: RTMUserData}, +) => { return payload.map(({id, ...rest}) => { + const group = id !== undefined ? {...rest, id} : rest; + + // Filter out offline users from participants + const filteredGroup = { + ...group, + participants: { + hosts: group.participants.hosts.filter(uid => { + // Check defaultContent first + let user = mainRoomRTMUsers[uid]; + if (user) { + return !user.offline && user.type === 'rtc'; + } + }), + attendees: group.participants.attendees.filter(uid => { + // Check defaultContent first + let user = mainRoomRTMUsers[uid]; + if (user) { + return !user.offline && user.type === 'rtc'; + } + }), + }, + }; + + // Remove temp IDs for API payload if (typeof id === 'string' && id.startsWith('temp')) { - return rest; + const {id: _, ...withoutId} = filteredGroup; + return withoutId; } - return id !== undefined ? {...rest, id} : rest; + return filteredGroup; }); }; @@ -176,7 +209,8 @@ interface BreakoutRoomContextValue { clearAllRaisedHands: () => void; // State sync handleBreakoutRoomSyncState: ( - data: BreakoutRoomSyncStateEventPayload['data']['data'], + data: BreakoutRoomSyncStateEventPayload['data'], + timestamp: number, ) => void; // Multi-host coordination handlers handleHostOperationStart: ( @@ -195,6 +229,8 @@ interface BreakoutRoomContextValue { // Multi-host coordination isAnotherHostOperating: boolean; currentOperatingHostName?: string; + // State version for forcing re-computation in dependent hooks + breakoutRoomVersion: number; } const BreakoutRoomContext = React.createContext({ @@ -239,6 +275,8 @@ const BreakoutRoomContext = React.createContext({ // Multi-host coordination isAnotherHostOperating: false, currentOperatingHostName: undefined, + // State version for forcing re-computation in dependent hooks + breakoutRoomVersion: 0, }); const BreakoutRoomProvider = ({ @@ -252,19 +290,24 @@ const BreakoutRoomProvider = ({ }) => { const {store} = useContext(StorageContext); const {defaultContent, activeUids} = useContent(); + const {mainRoomRTMUsers} = useRTMGlobalState(); const localUid = useLocalUid(); const { data: {isHost, roomId: joinRoomId}, } = useRoomInfo(); - console.log('supriya-room-data', joinRoomId); - const location = useLocation(); - const isInBreakoutRoute = location.pathname.includes('breakout'); const breakoutRoomExit = useBreakoutRoomExit(handleLeaveBreakout); const [state, baseDispatch] = useReducer( breakoutRoomReducer, initialBreakoutRoomState, ); + console.log('supriya-event state', state); const [isBreakoutUpdateInFlight, setBreakoutUpdateInFlight] = useState(false); + // Parse URL to determine current mode + const location = useLocation(); + const searchParams = new URLSearchParams(location.search); + const isBreakoutMode = searchParams.get('breakout') === 'true'; + // Main Room RTM data + const getDisplayName = useMainRoomUserDisplayName(); // Permissions: const [permissions, setPermissions] = useState({ @@ -277,6 +320,9 @@ const BreakoutRoomProvider = ({ string | undefined >(undefined); + // Timestamp tracking for event ordering + const lastProcessedTimestampRef = useRef(0); + // Join Room pending intent const [selfJoinRoomId, setSelfJoinRoomId] = useState(null); @@ -291,8 +337,8 @@ const BreakoutRoomProvider = ({ {uid: UidType; timestamp: number}[] >([]); - // Polling control - const [isPollingPaused, setIsPollingPaused] = useState(false); + // State version tracker to force dependent hooks to re-compute + const [breakoutRoomVersion, setBreakoutRoomVersion] = useState(0); // Refs to avoid stale closures in async callbacks const stateRef = useRef(state); @@ -574,20 +620,23 @@ const BreakoutRoomProvider = ({ } }; - // Update unassigned participants whenever defaultContent or activeUids change + // Update unassigned participants and remove offline users from breakout rooms useEffect(() => { + if (!stateRef.current?.breakoutSessionId) { + return; + } + // Get currently assigned participants from all rooms // Filter active UIDs to exclude: // 1. Custom content (not type 'rtc') // 2. Screenshare UIDs // 3. Offline users - console.log('supriya-breakoutSessionId', stateRef); - if (!stateRef?.current?.breakoutSessionId) { - return; - } const filteredParticipants = activeUids - .filter(uid => { - const user = defaultContentRef.current[uid]; + .map(uid => ({ + uid, + user: defaultContent[uid], + })) + .filter(({uid, user}) => { console.log('supriya-breakoutSessionId user: ', user); if (!user) { return false; @@ -600,43 +649,60 @@ const BreakoutRoomProvider = ({ if (user.offline) { return false; } - // // Exclude hosts - if (user?.isHost === 'true') { - return false; - } // Exclude screenshare UIDs (they typically have a parentUid) if (user.parentUid) { return false; } - // Exclude yourself from assigning - if (uid === localUid) { - return false; - } return true; - }) - .map(uid => ({ - uid, - user: defaultContentRef.current[uid], - })); - - // // Sort participants with local user first - // const sortedParticipants = filteredParticipants.sort((a, b) => { - // if (a.uid === localUid) { - // return -1; - // } - // if (b.uid === localUid) { - // return 1; - // } - // return 0; + }); + + // Sort participants to show local user first + filteredParticipants.sort((a, b) => { + if (a.uid === localUid) { + return -1; + } + if (b.uid === localUid) { + return 1; + } + return 0; + }); + + // // Find offline users who are currently assigned to breakout rooms + // const currentlyAssignedUids = new Set(); + // stateRef.current.breakoutGroups.forEach(group => { + // group.participants.hosts.forEach(uid => currentlyAssignedUids.add(uid)); + // group.participants.attendees.forEach(uid => currentlyAssignedUids.add(uid)); + // }); + + // const offlineAssignedUsers = Array.from(currentlyAssignedUids).filter(uid => { + // const user = defaultContent[uid]; + // return !user || user.offline || user.type !== 'rtc'; // }); + // // Remove offline users from breakout rooms if any found + // if (offlineAssignedUsers.length > 0) { + // console.log('Removing offline users from breakout rooms:', offlineAssignedUsers); + // dispatch({ + // type: BreakoutGroupActionTypes.REMOVE_OFFLINE_USERS, + // payload: { + // offlineUserUids: offlineAssignedUsers, + // }, + // }); + // } + + // Update unassigned participants dispatch({ type: BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS, payload: { unassignedParticipants: filteredParticipants, }, }); - }, [activeUids, localUid, dispatch, state.breakoutSessionId]); + }, [defaultContent, activeUids, localUid, dispatch, state.breakoutSessionId]); + + // Increment version when breakout group assignments change + useEffect(() => { + setBreakoutRoomVersion(prev => prev + 1); + }, [state.breakoutGroups]); // Check if there is already an active breakout session // We can call this to trigger sync events @@ -644,16 +710,16 @@ const BreakoutRoomProvider = ({ useCallback(async (): Promise => { // Skip API call if roomId is not available or if API update is in progress if (!joinRoomId?.host && !joinRoomId?.attendee) { - console.log('supriya-polling: Skipping - no roomId available'); + console.log('supriya-api: Skipping GET no roomId available'); return false; } if (isBreakoutUpdateInFlight) { - console.log('supriya-polling: Skipping - API update in progress'); + console.log('supriya-api upsert in progress: Skipping GET'); return false; } console.log( - 'supriya-state-sync calling checkIfBreakoutRoomSessionExistsAPI', + 'supriya-api calling checkIfBreakoutRoomSessionExistsAPI', joinRoomId, isHostRef.current, ); @@ -686,7 +752,6 @@ const BreakoutRoomProvider = ({ 'X-Session-Id': logger.getSessionId(), }, }); - // 🛡️ Guard against component unmount after fetch if (!isMountedRef.current) { logger.log( @@ -728,7 +793,7 @@ const BreakoutRoomProvider = ({ } const data = await response.json(); - + console.log('supriya-api-get response', data.sts, data); // 🛡️ Guard against component unmount after JSON parsing if (!isMountedRef.current) { logger.log( @@ -753,6 +818,17 @@ const BreakoutRoomProvider = ({ }, ); + // Skip events older than the last processed timestamp + if (data?.sts && data?.sts <= lastProcessedTimestampRef.current) { + console.log( + 'supriya-api-get skipping dispatch as out of date/order ', + { + timestamp: data?.sts, + lastProcessed: lastProcessedTimestampRef.current, + }, + ); + return; + } dispatch({ type: BreakoutGroupActionTypes.SYNC_STATE, payload: { @@ -763,6 +839,8 @@ const BreakoutRoomProvider = ({ switchRoom: data?.switch_room ?? true, }, }); + lastProcessedTimestampRef.current = data.sts || Date.now(); + return true; } @@ -780,24 +858,18 @@ const BreakoutRoomProvider = ({ } }, [isBreakoutUpdateInFlight, dispatch, joinRoomId, store.token]); - // Polling for sync event - const pollBreakoutGetAPI = useCallback(async () => { - if (isHostRef.current && stateRef.current.breakoutSessionId) { + useEffect(() => { + const loadInitialData = async () => { await checkIfBreakoutRoomSessionExistsAPI(); - } - }, [checkIfBreakoutRoomSessionExistsAPI]); + }; + const timeoutId = setTimeout(() => { + loadInitialData(); + }, 1200); - // Automatic interval management with cleanup only host will poll - useEffect(() => { - if ( - isHostRef.current && - !isPollingPaused && - stateRef.current.breakoutSessionId - ) { - const interval = setInterval(pollBreakoutGetAPI, 2000); - return () => clearInterval(interval); - } - }, [isPollingPaused, pollBreakoutGetAPI]); + return () => { + clearTimeout(timeoutId); + }; + }, []); const upsertBreakoutRoomAPI = useCallback( async (type: 'START' | 'UPDATE' = 'START', retryCount = 0) => { @@ -841,8 +913,11 @@ const BreakoutRoomProvider = ({ assignment_type: stateRef.current.assignmentStrategy, breakout_room: type === 'START' - ? getSanitizedPayload(initialBreakoutGroups) - : getSanitizedPayload(stateRef.current.breakoutGroups), + ? getSanitizedPayload(initialBreakoutGroups, mainRoomRTMUsers) + : getSanitizedPayload( + stateRef.current.breakoutGroups, + mainRoomRTMUsers, + ), }; // Only add join_room_id if attendee has called this api(during join room) @@ -908,6 +983,7 @@ const BreakoutRoomProvider = ({ throw new Error(`Breakout room creation failed: ${msg}`); } else { const data = await response.json(); + console.log('supriya-api-upsert response', data.sts, data); // 🛡️ Guard against component unmount after JSON parsing if (!isMountedRef.current) { @@ -938,12 +1014,6 @@ const BreakoutRoomProvider = ({ payload: {sessionId: data.session_id}, }); } - if (data?.breakout_room) { - dispatch({ - type: BreakoutGroupActionTypes.UPDATE_GROUPS_IDS, - payload: data.breakout_room, - }); - } } } catch (err) { const latency = Date.now() - startReqTs; @@ -1015,6 +1085,7 @@ const BreakoutRoomProvider = ({ dispatch, selfJoinRoomId, joinRoomId.attendee, + mainRoomRTMUsers, ], ); @@ -1087,6 +1158,14 @@ const BreakoutRoomProvider = ({ }; const handleAssignParticipants = (strategy: RoomAssignmentStrategy) => { + if (stateRef.current.breakoutGroups.length === 0) { + Toast.show({ + type: 'info', + text1: 'No breakout rooms found.', + visibilityTime: 3000, + }); + return; + } if (!acquireOperationLock(`ASSIGN_${strategy}`)) { return; } @@ -1101,6 +1180,9 @@ const BreakoutRoomProvider = ({ if (strategy === RoomAssignmentStrategy.AUTO_ASSIGN) { dispatch({ type: BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, + payload: { + localUid, + }, }); } if (strategy === RoomAssignmentStrategy.MANUAL_ASSIGN) { @@ -1297,7 +1379,7 @@ const BreakoutRoomProvider = ({ ); } }, - [localUid], + [localUid, breakoutRoomVersion], ); const getCurrentRoom = useCallback((): BreakoutGroup | null => { @@ -1307,46 +1389,43 @@ const BreakoutRoomProvider = ({ group.participants.attendees.includes(localUid), ); return userRoom ?? null; - }, [localUid]); + }, [localUid, breakoutRoomVersion]); // Permissions useEffect(() => { + const current = stateRef.current; + const currentlyInRoom = isUserInRoom(); - const hasAvailableRooms = stateRef.current.breakoutGroups.length > 0; - const allowAttendeeSwitch = stateRef.current.canUserSwitchRoom; + const hasAvailableRooms = current.breakoutGroups.length > 0; + const allowAttendeeSwitch = current.canUserSwitchRoom; const nextPermissions: BreakoutRoomPermissions = { canJoinRoom: - !currentlyInRoom && - hasAvailableRooms && - (isHostRef.current || allowAttendeeSwitch), + hasAvailableRooms && (isHostRef.current || allowAttendeeSwitch), canExitRoom: currentlyInRoom, canSwitchBetweenRooms: currentlyInRoom && hasAvailableRooms && (isHostRef.current || allowAttendeeSwitch), - canScreenshare: currentlyInRoom ? canIPresent : true, - canRaiseHands: !isHostRef.current && !!stateRef.current.breakoutSessionId, - canSeeRaisedHands: isHostRef.current, - canAssignParticipants: isHostRef.current, - canHostManageMainRoom: isHostRef.current && !currentlyInRoom, + canScreenshare: isHostRef.current + ? true + : currentlyInRoom + ? canIPresent + : true, + canRaiseHands: + !isHostRef.current && !!current.breakoutSessionId && currentlyInRoom, + canSeeRaisedHands: true, + canAssignParticipants: isHostRef.current && !currentlyInRoom, + canHostManageMainRoom: isHostRef.current, canCreateRooms: isHostRef.current, canMoveUsers: isHostRef.current, canCloseRooms: - isHostRef.current && - hasAvailableRooms && - !!stateRef.current.breakoutSessionId, + isHostRef.current && hasAvailableRooms && !!current.breakoutSessionId, canMakePresenter: isHostRef.current, }; setPermissions(nextPermissions); - }, [ - state.breakoutGroups, - state.canUserSwitchRoom, - state.breakoutSessionId, - isUserInRoom, - canIPresent, - ]); + }, [breakoutRoomVersion, canIPresent]); const joinRoom = ( toRoomId: string, @@ -1386,7 +1465,9 @@ const BreakoutRoomProvider = ({ }); moveUserIntoGroup(user, toRoomId); - setSelfJoinRoomId(toRoomId); + if (!isHostRef.current) { + setSelfJoinRoomId(toRoomId); + } }; const exitRoom = useCallback( @@ -1582,60 +1663,6 @@ const BreakoutRoomProvider = ({ : []; }; - const getRoomMemberDropdownOptions = (memberUid: UidType) => { - const options: MemberDropdownOption[] = []; - // Find which room the user is currently in - - const memberUser = defaultContentRef.current[memberUid]; - if (!memberUser) { - return options; - } - - const getCurrentUserRoom = (uid: UidType) => { - return stateRef.current.breakoutGroups.find( - group => - group.participants.hosts.includes(uid) || - group.participants.attendees.includes(uid), - ); - }; - const currentRoom = getCurrentUserRoom(memberUid); - // Move to Main Room option - options.push({ - icon: 'double-up-arrow', - type: 'move-to-main', - title: 'Move to Main Room', - onOptionPress: () => moveUserToMainRoom(memberUser), - }); - - // Move to other breakout rooms (exclude current room) - stateRef.current.breakoutGroups - .filter(group => group.id !== currentRoom?.id) - .forEach(group => { - options.push({ - type: 'move-to-room', - icon: 'move-up', - title: `Shift to ${group.name}`, - roomId: group.id, - roomName: group.name, - onOptionPress: () => moveUserIntoGroup(memberUser, group.id), - }); - }); - - // Make presenter option (only for hosts) - if (isHostRef.current) { - const userIsPresenting = isUserPresenting(memberUid); - const title = userIsPresenting ? 'Stop presenter' : 'Make a Presenter'; - const action = userIsPresenting ? 'stop' : 'start'; - options.push({ - type: 'make-presenter', - icon: 'promote-filled', - title, - onOptionPress: () => makePresenter(memberUser, action), - }); - } - return options; - }; - const isUserPresenting = useCallback( (uid?: UidType) => { if (uid !== undefined) { @@ -1741,6 +1768,65 @@ const BreakoutRoomProvider = ({ setPresenters([]); }, []); + const getRoomMemberDropdownOptions = useCallback( + (memberUid: UidType) => { + const options: MemberDropdownOption[] = []; + // Find which room the user is currently in + + const memberUser = defaultContentRef.current[memberUid]; + if (!memberUser) { + return options; + } + + const currentRoom = stateRef.current.breakoutGroups.find( + group => + group.participants.hosts.includes(memberUid) || + group.participants.attendees.includes(memberUid), + ); + console.log( + 'supriya-currentRoom', + currentRoom, + memberUid, + JSON.stringify(stateRef.current.breakoutGroups), + ); + // Move to Main Room option + options.push({ + icon: 'double-up-arrow', + type: 'move-to-main', + title: 'Move to Main Room', + onOptionPress: () => moveUserToMainRoom(memberUser), + }); + // Move to other breakout rooms (exclude current room) + stateRef.current.breakoutGroups + .filter(group => group.id !== currentRoom?.id) + .forEach(group => { + options.push({ + type: 'move-to-room', + icon: 'move-up', + title: `Shift to ${group.name}`, + roomId: group.id, + roomName: group.name, + onOptionPress: () => moveUserIntoGroup(memberUser, group.id), + }); + }); + + // Make presenter option (only for hosts) + if (isHostRef.current) { + const userIsPresenting = isUserPresenting(memberUid); + const title = userIsPresenting ? 'Stop presenter' : 'Make a Presenter'; + const action = userIsPresenting ? 'stop' : 'start'; + options.push({ + type: 'make-presenter', + icon: 'promote-filled', + title, + onOptionPress: () => makePresenter(memberUser, action), + }); + } + return options; + }, + [isUserPresenting, isHostRef.current, presenters, breakoutRoomVersion], + ); + // Raise Hand // Send raise hand event via RTM const sendRaiseHandEvent = useCallback( @@ -1835,7 +1921,31 @@ const BreakoutRoomProvider = ({ ); const handleBreakoutRoomSyncState = useCallback( - (data: BreakoutRoomSyncStateEventPayload['data']['data']) => { + (payload: BreakoutRoomSyncStateEventPayload['data'], timestamp) => { + console.log( + 'supriya-api-sync response', + timestamp, + JSON.stringify(payload), + ); + + // Skip events older than the last processed timestamp + if (timestamp && timestamp <= lastProcessedTimestampRef.current) { + console.log('supriya-api-sync Skipping old breakout room sync event', { + timestamp, + lastProcessed: lastProcessedTimestampRef.current, + }); + return; + } + + const {srcuid, data} = payload; + console.log('supriya-event flow step 2', srcuid); + console.log('supriya-event uids', srcuid, localUid); + + // if (srcuid === localUid) { + // console.log('supriya-event flow skipping'); + + // return; + // } const {session_id, switch_room, breakout_room, assignment_type} = data; console.log('supriya-state-sync new data: ', data); console.log('supriya-state-sync old data: ', stateRef.current); @@ -1859,6 +1969,7 @@ const BreakoutRoomProvider = ({ } // 🛡️ BEFORE snapshot - using stateRef to avoid stale closure const prevGroups = stateRef.current.breakoutGroups; + console.log('supriya-event sync prevGroups: ', prevGroups); const prevSwitchRoom = stateRef.current.canUserSwitchRoom; // Helpers to find membership @@ -1874,48 +1985,32 @@ const BreakoutRoomProvider = ({ })?.id ?? null; const prevRoomId = findUserRoomId(localUid, prevGroups); // before + console.log('supriya-event sync prevRoomId: ', prevRoomId); + const nextRoomId = findUserRoomId(localUid, breakout_room); + console.log('supriya-event sync nextRoomId: ', nextRoomId); // Show notifications based on changes // 1. Switch room enabled notification + const senderName = getDisplayName(srcuid); if (switch_room && !prevSwitchRoom) { console.log('supriya-toast 1'); showDeduplicatedToast('switch-room-toggle', { leadingIconName: 'info', type: 'info', - text1: 'Breakout rooms are now open. Please choose a room to join.', + text1: `Host:${senderName} has opened breakout rooms.`, + text2: 'Please choose a room to join.', visibilityTime: 3000, }); } // 2. User joined a room (compare previous and current state) - if (!prevRoomId && nextRoomId) { - console.log('supriya-toast 2', prevRoomId, nextRoomId); - const currentRoom = breakout_room.find(r => r.id === nextRoomId); - - showDeduplicatedToast(`joined-room-${nextRoomId}`, { - type: 'success', - text1: `You've joined ${currentRoom?.name || 'a breakout room'}.`, - visibilityTime: 3000, - }); - } - - // 3. User was moved to a different room by host - // Moved to a different room (before: A, after: B, A≠B) - if (prevRoomId && nextRoomId && prevRoomId !== nextRoomId) { - console.log('supriya-toast 3', prevRoomId, nextRoomId); - const afterRoom = breakout_room.find(r => r.id === nextRoomId); - showDeduplicatedToast(`moved-to-room-${nextRoomId}`, { - type: 'info', - text1: `You've been moved to ${afterRoom.name} by the host.`, - visibilityTime: 4000, - }); + // The notification for this comes from the main room channel_join event + if (prevRoomId === nextRoomId) { + // No logic } - - // 4. User was moved to main room + // 3. User was moved to main room if (prevRoomId && !nextRoomId) { - console.log('supriya-toast 4', prevRoomId, nextRoomId); - const prevRoom = prevGroups.find(r => r.id === prevRoomId); // Distinguish "room closed" vs "moved to main" const roomStillExists = breakout_room.some(r => r.id === prevRoomId); @@ -1924,22 +2019,22 @@ const BreakoutRoomProvider = ({ showDeduplicatedToast(`current-room-closed-${prevRoomId}`, { leadingIconName: 'alert', type: 'error', - text1: `${ - prevRoom?.name || 'Your room' - } is currently closed. Returning to main room.`, - visibilityTime: 5000, + text1: `Host: ${senderName} has closed "${ + prevRoom?.name || '' + }" room. `, + text2: 'Returning to main room...', + visibilityTime: 3000, }); } else { showDeduplicatedToast(`moved-to-main-${prevRoomId}`, { leadingIconName: 'arrow-up', type: 'info', - text1: "You've returned to the main room.", + text1: `Host: ${senderName} has moved you to main room.`, visibilityTime: 3000, }); } // Exit breakout room and return to main room - exitRoom(prevRoomId, true); - return; + return exitRoom(prevRoomId, true); } // 5. All breakout rooms closed @@ -1952,21 +2047,21 @@ const BreakoutRoomProvider = ({ showDeduplicatedToast('all-rooms-closed', { leadingIconName: 'close', type: 'info', - text1: - 'Breakout rooms are now closed. Returning to the main room...', + text1: `Host: ${senderName} has closed all breakout rooms.`, + text2: 'Returning to the main room...', visibilityTime: 3000, }); - exitRoom(prevRoomId, true); + return exitRoom(prevRoomId, true); } else { // User was already in main room - just notify about closure showDeduplicatedToast('all-rooms-closed', { leadingIconName: 'close', type: 'info', - text1: 'All breakout rooms have been closed.', + text1: `Host: ${senderName} has closed all breakout rooms`, visibilityTime: 4000, }); + return; } - return; } // 6) Room renamed (compare per-room names) @@ -1975,7 +2070,7 @@ const BreakoutRoomProvider = ({ if (after && after.name !== prevRoom.name) { showDeduplicatedToast(`room-renamed-${after.id}`, { type: 'info', - text1: `${prevRoom.name} has been renamed to '${after.name}'.`, + text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`, visibilityTime: 3000, }); } @@ -1991,6 +2086,8 @@ const BreakoutRoomProvider = ({ rooms: breakout_room, }, }); + // Update the last processed timestamp after successful processing + lastProcessedTimestampRef.current = timestamp || Date.now(); }, [ dispatch, @@ -1998,6 +2095,7 @@ const BreakoutRoomProvider = ({ localUid, showDeduplicatedToast, isAnotherHostOperating, + getDisplayName, ], ); @@ -2092,7 +2190,6 @@ const BreakoutRoomProvider = ({ const debouncedUpsertAPI = useDebouncedCallback( async (type: 'START' | 'UPDATE', operationName?: string) => { setBreakoutUpdateInFlight(true); - setIsPollingPaused(true); try { console.log( @@ -2153,7 +2250,6 @@ const BreakoutRoomProvider = ({ } } finally { setBreakoutUpdateInFlight(false); - setIsPollingPaused(false); } }, 500, @@ -2206,7 +2302,7 @@ const BreakoutRoomProvider = ({ if (shouldCallAPI) { debouncedUpsertAPI('UPDATE', lastOperationName); } - }, [dispatch, lastAction]); + }, [lastAction]); return ( {children} diff --git a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx index d4483600f..54741b2ed 100644 --- a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx +++ b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx @@ -137,13 +137,13 @@ const BreakoutRoomEventsConfigure: React.FC = ({ 'BREAKOUT_ROOM_SYNC_STATE event recevied', evtData, ); - const {payload} = evtData; + const {ts, payload} = evtData; const data: BreakoutRoomSyncStateEventPayload = JSON.parse(payload); if (data.data.act === 'SYNC_STATE') { console.log( 'supriya-state-sync ********* BREAKOUT_ROOM_SYNC_STATE event triggered ***************', ); - handleBreakoutRoomSyncStateRef.current(data.data.data); + handleBreakoutRoomSyncStateRef.current(data.data, ts); } }; diff --git a/template/src/components/breakout-room/hoc/BreakoutRoomNameRenderer.tsx b/template/src/components/breakout-room/hoc/BreakoutRoomNameRenderer.tsx new file mode 100644 index 000000000..54c98f889 --- /dev/null +++ b/template/src/components/breakout-room/hoc/BreakoutRoomNameRenderer.tsx @@ -0,0 +1,68 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useMemo} from 'react'; +import {useLocation} from '../../Router'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import {useLocalUid} from '../../../../agora-rn-uikit'; + +export interface BreakoutRoomInfo { + isInBreakoutMode: boolean; + breakoutRoomName: string; +} + +interface BreakoutRoomNameRendererProps { + children: (breakoutRoomInfo: BreakoutRoomInfo) => React.ReactNode; +} + +const BreakoutRoomNameRenderer: React.FC = ({ + children, +}) => { + const location = useLocation(); + const localUid = useLocalUid(); + const {breakoutGroups = [], breakoutRoomVersion} = useBreakoutRoom(); + + const breakoutRoomInfo = useMemo(() => { + let breakoutRoomName = ''; + let isInBreakoutMode = false; + let currentRoom = null; + + try { + const searchParams = new URLSearchParams(location.search); + isInBreakoutMode = searchParams.get('breakout') === 'true'; + + if (isInBreakoutMode) { + currentRoom = breakoutGroups?.find( + group => + group.participants?.hosts?.includes(localUid) || + group.participants?.attendees?.includes(localUid), + ); + + if (currentRoom?.name) { + breakoutRoomName = currentRoom.name; + } + } + } catch (error) { + // Safely handle cases where breakout context is not available + console.log('BreakoutRoomNameRenderer: Breakout context not available'); + } + + return { + isInBreakoutMode, + breakoutRoomName, + }; + }, [location.search, localUid, breakoutRoomVersion]); + + return <>{children(breakoutRoomInfo)}; +}; + +export default BreakoutRoomNameRenderer; diff --git a/template/src/components/breakout-room/state/reducer.ts b/template/src/components/breakout-room/state/reducer.ts index 7a5946804..e473d95a1 100644 --- a/template/src/components/breakout-room/state/reducer.ts +++ b/template/src/components/breakout-room/state/reducer.ts @@ -148,6 +148,9 @@ export type BreakoutRoomAction = } | { type: typeof BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS; + payload: { + localUid: UidType; + }; } | { type: typeof BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS; @@ -186,8 +189,8 @@ export const breakoutRoomReducer = ( id: group.id, name: group.name, participants: { - hosts: group?.participants?.hosts ?? [], - attendees: group?.participants?.attendees ?? [], + hosts: [...new Set(group?.participants?.hosts ?? [])], + attendees: [...new Set(group?.participants?.attendees ?? [])], }, })), }; @@ -294,16 +297,24 @@ export const breakoutRoomReducer = ( {hosts: UidType[]; attendees: UidType[]} >(); - // Initialize empty arrays for each room + // Initialize with existing participants for each room state.breakoutGroups.forEach(room => { - roomAssignments.set(room.id, {hosts: [], attendees: []}); + roomAssignments.set(room.id, { + hosts: [...room.participants.hosts], + attendees: [...room.participants.attendees], + }); }); let assignedParticipantUids: UidType[] = []; // AUTO ASSIGN Simple round-robin assignment (no capacity limits) + // Exclude local user from auto assignment + const participantsToAssign = state.unassignedParticipants.filter( + participant => participant.uid !== action.payload.localUid + ); + let roomIndex = 0; const roomIds = state.breakoutGroups.map(room => room.id); - state.unassignedParticipants.forEach(participant => { + participantsToAssign.forEach(participant => { const currentRoomId = roomIds[roomIndex]; const roomAssignment = roomAssignments.get(currentRoomId)!; // Assign participant based on their isHost status (string "true"/"false") diff --git a/template/src/components/breakout-room/state/types.ts b/template/src/components/breakout-room/state/types.ts index 0a6bd0914..4247fff47 100644 --- a/template/src/components/breakout-room/state/types.ts +++ b/template/src/components/breakout-room/state/types.ts @@ -30,6 +30,7 @@ export interface BreakoutChannelJoinEventPayload { }; }; act: 'CHAN_JOIN'; // e.g., "CHAN_JOIN" + srcuid: number; }; } @@ -41,7 +42,8 @@ export interface BreakoutRoomSyncStateEventPayload { breakout_room: BreakoutGroup[]; assignment_type: RoomAssignmentStrategy; }; - act: 'SYNC_STATE'; // e.g., "CHAN_JOIN" + act: 'SYNC_STATE'; + srcuid: number; }; } export interface BreakoutRoomAnnouncementEventPayload { @@ -49,20 +51,3 @@ export interface BreakoutRoomAnnouncementEventPayload { timestamp: string; announcement: string; } -// | {type: 'DELETE_GROUP'; payload: {groupId: string}} -// | { -// type: 'ADD_PARTICIPANT'; -// payload: {uid: UidType; groupId: string; isHost: boolean}; -// } -// | { -// type: 'MOVE_PARTICIPANT'; -// payload: { -// uid: UidType; -// fromGroupId: string; -// toGroupId: string; -// isHost: boolean; -// }; -// } -// | {type: 'RESET_ALL_PARTICIPANTS'} -// | {type: 'SET_GROUPS'; payload: BreakoutRoomInfo[]} -// | {type: 'RESET_ALL'}; diff --git a/template/src/components/breakout-room/ui/BreakoutMeetingTitle.tsx b/template/src/components/breakout-room/ui/BreakoutMeetingTitle.tsx new file mode 100644 index 000000000..58aabac03 --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutMeetingTitle.tsx @@ -0,0 +1,60 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React from 'react'; +import {View, Text, StyleSheet} from 'react-native'; +import BreakoutRoomNameRenderer from '../hoc/BreakoutRoomNameRenderer'; +import ImageIcon from '../../../atoms/ImageIcon'; +import ThemeConfig from '../../../theme'; +import {trimText} from '../../../utils/common'; + +const BreakoutMeetingTitle: React.FC = () => { + return ( + + {({breakoutRoomName}) => + trimText(breakoutRoomName) ? ( + + + + {trimText(breakoutRoomName)} + + + ) : null + } + + ); +}; + +const styles = StyleSheet.create({ + breakoutRoomNameView: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + fontFamily: ThemeConfig.FontFamily.sansPro, + }, + breakoutRoomNameText: { + fontSize: ThemeConfig.FontSize.tiny, + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium, + fontWeight: '600', + fontFamily: ThemeConfig.FontFamily.sansPro, + }, +}); + +export default BreakoutMeetingTitle; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index baae4ef54..4b05ac38d 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -19,8 +19,6 @@ import UserAvatar from '../../../atoms/UserAvatar'; import ImageIcon from '../../../atoms/ImageIcon'; import {BreakoutGroup} from '../state/reducer'; import {useContent} from 'customization-api'; -import {videoRoomUserFallbackText} from '../../../language/default-labels/videoCallScreenLabels'; -import {useString} from '../../../utils/useString'; import UserActionMenuOptionsOptions from '../../participants/UserActionMenuOptions'; import BreakoutRoomActionMenu from './BreakoutRoomActionMenu'; import TertiaryButton from '../../../atoms/TertiaryButton'; @@ -28,7 +26,9 @@ import BreakoutRoomAnnouncementModal from './BreakoutRoomAnnouncementModal'; import {useModal} from '../../../utils/useModal'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; import BreakoutRoomRenameModal from './BreakoutRoomRenameModal'; +import {useMainRoomUserDisplayName} from '../../../rtm/hooks/useMainRoomUserDisplayName'; import {useRoomInfo} from '../../room-info/useRoomInfo'; +import {useRTMGlobalState} from '../../../rtm/RTMGlobalStateProvider'; const BreakoutRoomGroupSettings: React.FC = () => { const { @@ -53,7 +53,9 @@ const BreakoutRoomGroupSettings: React.FC = () => { // Render room card const {defaultContent} = useContent(); - const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)(); + const {mainRoomRTMUsers} = useRTMGlobalState(); + // Use hook to get display names with fallback to main room users + const getDisplayName = useMainRoomUserDisplayName(); const memberMoreMenuRefs = useRef<{[key: string]: any}>({}); const { modalOpen: isAnnoucementModalOpen, @@ -79,10 +81,6 @@ const BreakoutRoomGroupSettings: React.FC = () => { })); }; - const getName = (uid: UidType) => { - return defaultContent[uid]?.name || remoteUserDefaultLabel; - }; - const [expandedRooms, setExpandedRooms] = useState>(new Set()); const toggleRoomExpansion = (roomId: string) => { @@ -96,6 +94,14 @@ const BreakoutRoomGroupSettings: React.FC = () => { }; const renderMember = (memberUId: UidType) => { + // Hide offline users from UI - check mainRoomRTMUsers for offline status + const rtmMemberData = mainRoomRTMUsers[memberUId]; + + // If user is offline in RTM data, don't render them + if (rtmMemberData && rtmMemberData?.offline) { + return null; + } + // Create or get ref for this specific member if (!memberMoreMenuRefs.current[memberUId]) { memberMoreMenuRefs.current[memberUId] = React.createRef(); @@ -111,12 +117,12 @@ const BreakoutRoomGroupSettings: React.FC = () => { - {getName(memberUId)} + {getDisplayName(memberUId)} diff --git a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx index c4d1588f9..3456a31d2 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx @@ -1,145 +1,56 @@ -import React, {useEffect, useState, useCallback} from 'react'; +import React, {useMemo} from 'react'; import {View, Text, StyleSheet} from 'react-native'; -import {useRTMCore} from '../../../rtm/RTMCoreProvider'; +import {useRTMGlobalState} from '../../../rtm/RTMGlobalStateProvider'; import ThemeConfig from '../../../theme'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; import UserAvatar from '../../../atoms/UserAvatar'; -import {type GetOnlineUsersResponse} from 'agora-react-native-rtm'; +import Tooltip from '../../../atoms/Tooltip'; +import {useString} from '../../../utils/useString'; +import {videoRoomUserFallbackText} from '../../../language/default-labels/videoCallScreenLabels'; -interface OnlineUser { - userId: string; - name?: string; +interface MainRoomUser { + uid: number; + name: string; } -const getUserNameFromAttributes = ( - attributes: any, - fallbackUserId: string, -): string => { - try { - const nameAttribute = attributes?.items?.find( - item => item.key === 'name', - )?.value; - if (!nameAttribute) { - return fallbackUserId; - } - - const firstParse = JSON.parse(nameAttribute); - if (firstParse?.payload) { - const secondParse = JSON.parse(firstParse.payload); - return secondParse?.name || fallbackUserId; - } - return firstParse?.name || fallbackUserId; - } catch (e) { - return fallbackUserId; - } -}; - const BreakoutRoomMainRoomUsers: React.FC = () => { - const {client} = useRTMCore(); - const {mainChannelId, breakoutGroups} = useBreakoutRoom(); - const [usersWithNames, setUsersWithNames] = useState([]); - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(null); + const {mainRoomRTMUsers} = useRTMGlobalState(); + const {breakoutGroups, breakoutRoomVersion} = useBreakoutRoom(); + const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)(); // Get all assigned users from breakout rooms - const getAssignedUsers = useCallback(() => { - const assigned = new Set(); + const assignedUserUids = useMemo(() => { + const assigned = new Set(); breakoutGroups.forEach(group => { - group.participants.hosts.forEach(uid => assigned.add(String(uid))); - group.participants.attendees.forEach(uid => assigned.add(String(uid))); + group.participants.hosts.forEach(uid => assigned.add(uid)); + group.participants.attendees.forEach(uid => assigned.add(uid)); }); return assigned; - }, [breakoutGroups]); - - // Fetch attributes only when online users change - useEffect(() => { - const fetchUserAttributes = async () => { - if (!client) { - setUsersWithNames([]); - return; - } - - setIsLoading(true); - setError(null); + }, [breakoutRoomVersion]); - try { - const onlineUsers: GetOnlineUsersResponse = - await client.presence.getOnlineUsers(mainChannelId, 1); + // Filter main room users to only show those not assigned to breakout rooms + const mainRoomOnlyUsers = useMemo(() => { + const users: MainRoomUser[] = []; - const users = await Promise.all( - Array.from(onlineUsers.occupants).map(async member => { - try { - const attributes = await client.storage.getUserMetadata({ - userId: member.userId, - }); - const username = getUserNameFromAttributes( - attributes, - member.userId, - ); - return { - userId: member.userId, - name: username, - }; - } catch (e) { - console.warn( - `Failed to get attributes for user ${member.userId}:`, - e, - ); - return { - userId: member.userId, - name: 'User', - }; - } - }), - ); + Object.entries(mainRoomRTMUsers).forEach(([uidStr, userData]) => { + const uid = parseInt(uidStr, 10); - setUsersWithNames(users); - } catch (fetchError) { - console.error('Failed to fetch user attributes:', fetchError); - setError('Failed to fetch user information'); - } finally { - setIsLoading(false); + // Skip if user is assigned to a breakout room + if (assignedUserUids.has(uid)) { + return; } - }; - - fetchUserAttributes(); - }, [client, mainChannelId]); - - // Filter out users who are assigned to breakout rooms - const mainRoomOnlyUsers = usersWithNames.filter(user => { - const assignedUsers = getAssignedUsers(); - return !assignedUsers.has(user.userId); - }); - if (!mainChannelId) { - return ( - - - Main channel not available - - - ); - } - - if (isLoading) { - return ( - - - Loading main room users... - - - ); - } + // Only include RTC users who are online + if (userData.type === 'rtc' && !userData.offline) { + users.push({ + uid, + name: userData.name || remoteUserDefaultLabel, + }); + } + }); - if (error) { - return ( - - - Error: {error} - - - ); - } + return users; + }, [mainRoomRTMUsers, assignedUserUids, breakoutRoomVersion]); return ( @@ -147,11 +58,18 @@ const BreakoutRoomMainRoomUsers: React.FC = () => { Main Room ({mainRoomOnlyUsers.length}) {mainRoomOnlyUsers.map(user => ( - - + { + return ( + + ); + }} /> ))} @@ -217,5 +135,6 @@ const style = StyleSheet.create({ lineHeight: 12, fontWeight: '600', color: $config.BACKGROUND_COLOR, + display: 'flex', }, }); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx b/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx index 662139bf5..6eb34b09b 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx @@ -112,6 +112,7 @@ const styles = StyleSheet.create({ lineHeight: 12, fontWeight: '600', color: $config.BACKGROUND_COLOR, + display: 'flex', }, emptyStateText: { color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, diff --git a/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx b/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx index 861541786..83bd80ae2 100644 --- a/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx +++ b/template/src/components/breakout-room/ui/ExitBreakoutRoomIconButton.tsx @@ -17,6 +17,7 @@ import ToolbarMenuItem from '../../../atoms/ToolbarMenuItem'; import {useToolbarProps} from '../../../atoms/ToolbarItem'; import {useActionSheet} from '../../../utils/useActionSheet'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import BreakoutRoomNameRenderer from '../hoc/BreakoutRoomNameRenderer'; export interface ScreenshareButtonProps { render?: (onPress: () => void) => JSX.Element; @@ -32,36 +33,46 @@ const ExitBreakoutRoomIconButton = (props: ScreenshareButtonProps) => { exitRoom(); }; - let iconButtonProps: IconButtonProps = { - onPress: onPressCustom || onPress, - iconProps: { - name: 'close-room', - tintColor: $config.SECONDARY_ACTION_COLOR, - }, - btnTextProps: { - textColor: $config.FONT_COLOR, - text: showLabel ? label || 'Exit Room' : '', - }, - }; + return ( + + {({breakoutRoomName}) => { + const displayText = breakoutRoomName + ? `Exit ${breakoutRoomName}` + : 'Exit Room'; + + let iconButtonProps: IconButtonProps = { + onPress: onPressCustom || onPress, + iconProps: { + name: 'close-room', + tintColor: $config.SECONDARY_ACTION_COLOR, + }, + btnTextProps: { + textColor: $config.FONT_COLOR, + text: showLabel ? label || displayText : '', + }, + }; - if (isOnActionSheet) { - iconButtonProps.btnTextProps.textStyle = { - color: $config.FONT_COLOR, - marginTop: 8, - fontSize: 12, - fontWeight: '400', - fontFamily: 'Source Sans Pro', - textAlign: 'center', - }; - } - iconButtonProps.isOnActionSheet = isOnActionSheet; + if (isOnActionSheet) { + iconButtonProps.btnTextProps.textStyle = { + color: $config.FONT_COLOR, + marginTop: 8, + fontSize: 12, + fontWeight: '400', + fontFamily: 'Source Sans Pro', + textAlign: 'center', + }; + } + iconButtonProps.isOnActionSheet = isOnActionSheet; - return props?.render ? ( - props.render(onPress) - ) : isToolbarMenuItem ? ( - - ) : ( - + return props?.render ? ( + props.render(onPress) + ) : isToolbarMenuItem ? ( + + ) : ( + + ); + }} + ); }; diff --git a/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx b/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx index 41673f109..70367d4d0 100644 --- a/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx +++ b/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx @@ -8,7 +8,11 @@ import ImageIcon from '../../../atoms/ImageIcon'; import Checkbox from '../../../atoms/Checkbox'; import Dropdown from '../../../atoms/Dropdown'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; -import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; +import { + ContentInterface, + UidType, + useLocalUid, +} from '../../../../agora-rn-uikit'; import TertiaryButton from '../../../atoms/TertiaryButton'; import { ManualParticipantAssignment, @@ -41,14 +45,20 @@ function ParticipantRow({ rooms, onAssignmentChange, onSelectionChange, + localUid, }: { participant: {uid: UidType; user: ContentInterface}; assignment: ManualParticipantAssignment; rooms: {label: string; value: string}[]; onAssignmentChange: (uid: UidType, roomId: string | null) => void; onSelectionChange: (uid: UidType) => void; + localUid: UidType; }) { const selectedValue = assignment?.roomId || 'unassigned'; + const displayName = + participant?.uid === localUid + ? `${participant?.user?.name} (me)` + : participant?.user?.name; return ( @@ -57,7 +67,7 @@ function ParticipantRow({ disabled={false} checked={assignment?.isSelected || false} onChange={() => onSelectionChange(participant.uid)} - label={participant.user.name} + label={displayName} /> @@ -86,6 +96,7 @@ export default function ParticipantManualAssignmentModal( props: ParticipantManualAssignmentModalProps, ) { const {setModalOpen} = props; + const localUid = useLocalUid(); const { getAllRooms, unassignedParticipants, @@ -295,6 +306,7 @@ export default function ParticipantManualAssignmentModal( rooms={rooms} onAssignmentChange={handleRoomDropdownChange} onSelectionChange={toggleParticipantSelection} + localUid={localUid} /> ); }} diff --git a/template/src/components/chat-messages/useChatMessages.tsx b/template/src/components/chat-messages/useChatMessages.tsx index 07c26984b..6ef9cca0b 100644 --- a/template/src/components/chat-messages/useChatMessages.tsx +++ b/template/src/components/chat-messages/useChatMessages.tsx @@ -329,12 +329,15 @@ const ChatMessagesProvider = (props: ChatMessagesProviderProps) => { //TODO: check why need - const updateRenderListState = ( - uid: number, - data: Partial, - ) => { - dispatch({type: 'UpdateRenderList', value: [uid, data]}); - }; + // If this is needed use syncUserState + // + // const updateRenderListState = ( + // uid: number, + // data: Partial, + // ) => { + // dispatch({type: 'UpdateRenderList', value: [uid, data]}); + // }; + // const addMessageToStore = (uid: UidType, body: messageInterface) => { setMessageStore((m: messageStoreInterface[]) => { diff --git a/template/src/components/participants/UserActionMenuOptions.tsx b/template/src/components/participants/UserActionMenuOptions.tsx index 22ce6f262..7bd9e3b3d 100644 --- a/template/src/components/participants/UserActionMenuOptions.tsx +++ b/template/src/components/participants/UserActionMenuOptions.tsx @@ -760,7 +760,7 @@ export default function UserActionMenuOptionsOptions( currentLayout, spotlightUid, from, - breakoutRoomPresenters, + getRoomMemberDropdownOptions, ]); const {width: globalWidth, height: globalHeight} = useWindowDimensions(); diff --git a/template/src/components/precall/joinWaitingRoomBtn.native.tsx b/template/src/components/precall/joinWaitingRoomBtn.native.tsx index 5abb48992..5ce305da9 100644 --- a/template/src/components/precall/joinWaitingRoomBtn.native.tsx +++ b/template/src/components/precall/joinWaitingRoomBtn.native.tsx @@ -39,6 +39,7 @@ import { waitingRoomHostNotJoined, waitingRoomUsersInCall, } from '../../language/default-labels/videoCallScreenLabels'; +import ChatContext from '../ChatContext'; export interface PreCallJoinWaitingRoomBtnProps { render?: ( @@ -58,6 +59,7 @@ const JoinWaitingRoomBtn = (props: PreCallJoinWaitingRoomBtnProps) => { const waitingRoomUsersInCallText = useString(waitingRoomUsersInCall); let pollingTimeout = React.useRef(null); const {rtcProps} = useContext(PropsContext); + const {syncUserState} = useContext(ChatContext); const {setCallActive, callActive} = usePreCall(); const username = useGetName(); const {isJoinDataFetched, isInWaitingRoom} = useRoomInfo(); @@ -139,10 +141,11 @@ const JoinWaitingRoomBtn = (props: PreCallJoinWaitingRoomBtnProps) => { if (callActive) return; // on approve/reject response from host, waiting room permission is reset // update waitinng room status on uid - dispatch({ - type: 'UpdateRenderList', - value: [localUid, {isInWaitingRoom: false}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [localUid, {isInWaitingRoom: false}], + // }); + syncUserState(localUid, {isInWaitingRoom: false}); if (approved) { setRoomInfo(prev => { @@ -219,10 +222,11 @@ const JoinWaitingRoomBtn = (props: PreCallJoinWaitingRoomBtnProps) => { }); // add the waitingRoomStatus to the uid - dispatch({ - type: 'UpdateRenderList', - value: [localUid, {isInWaitingRoom: true}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [localUid, {isInWaitingRoom: true}], + // }); + syncUserState(localUid, {isInWaitingRoom: true}); // join request API to server, server will send RTM message to all hosts regarding request from this user, requestServerToJoinRoom(); diff --git a/template/src/components/precall/joinWaitingRoomBtn.tsx b/template/src/components/precall/joinWaitingRoomBtn.tsx index 8d5be8efd..52cb8176c 100644 --- a/template/src/components/precall/joinWaitingRoomBtn.tsx +++ b/template/src/components/precall/joinWaitingRoomBtn.tsx @@ -45,6 +45,7 @@ import { waitingRoomHostNotJoined, waitingRoomUsersInCall, } from '../../language/default-labels/videoCallScreenLabels'; +import ChatContext from '../ChatContext'; const audio = new Audio( 'https://dl.dropboxusercontent.com/s/1cdwpm3gca9mlo0/kick.mp3', @@ -84,7 +85,7 @@ const JoinWaitingRoomBtn = (props: PreCallJoinWaitingRoomBtnProps) => { const localUid = useLocalUid(); const {dispatch} = useContext(DispatchContext); - + const {syncUserState} = useContext(ChatContext); const [buttonText, setButtonText] = React.useState( waitingRoomButton({ ready: isInWaitingRoom, @@ -144,10 +145,11 @@ const JoinWaitingRoomBtn = (props: PreCallJoinWaitingRoomBtnProps) => { if (callActive) return; // on approve/reject response from host, waiting room permission is reset // update waitinng room status on uid - dispatch({ - type: 'UpdateRenderList', - value: [localUid, {isInWaitingRoom: false}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [localUid, {isInWaitingRoom: false}], + // }); + syncUserState(localUid, {isInWaitingRoom: false}); if (approved) { setRoomInfo(prev => { @@ -241,10 +243,12 @@ const JoinWaitingRoomBtn = (props: PreCallJoinWaitingRoomBtnProps) => { //setCallActive(true); // add the waitingRoomStatus to the uid - dispatch({ - type: 'UpdateRenderList', - value: [localUid, {isInWaitingRoom: true}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [localUid, {isInWaitingRoom: true}], + // }); + syncUserState(localUid, {isInWaitingRoom: true}); + // Enter waiting rooom; setRoomInfo(prev => { return {...prev, isInWaitingRoom: true}; @@ -281,7 +285,7 @@ const JoinWaitingRoomBtn = (props: PreCallJoinWaitingRoomBtnProps) => { const title = buttonText; const onPress = () => onSubmit(); const disabled = $config.ENABLE_WAITING_ROOM_AUTO_REQUEST - ? !hasHostJoined || isInWaitingRoom || username?.trim() === '' + ? !hasHostJoined || isInWaitingRoom || username?.trim() === '' : isInWaitingRoom || username?.trim() === ''; return props?.render ? ( props.render(onPress, title, disabled) diff --git a/template/src/components/useUserPreference.tsx b/template/src/components/useUserPreference.tsx index 598949ee2..64219d794 100644 --- a/template/src/components/useUserPreference.tsx +++ b/template/src/components/useUserPreference.tsx @@ -67,7 +67,7 @@ const UserPreferenceProvider = (props: { const {dispatch} = useContext(DispatchContext); const [uids, setUids] = useState({}); const {store, setStore} = useContext(StorageContext); - const {hasUserJoinedRTM} = useContext(ChatContext); + const {hasUserJoinedRTM, syncUserState} = useContext(ChatContext); const getInitialUsername = () => store?.displayName ? store.displayName : ''; const [displayName, setDisplayName] = useState(getInitialUsername()); @@ -91,7 +91,12 @@ const UserPreferenceProvider = (props: { ); const keys = Object.keys(users); if (users && keys && keys?.length) { - updateRenderListState(screenShareUidToUpdate, { + // updateRenderListState(screenShareUidToUpdate, { + // name: getScreenShareName( + // users[parseInt(keys[0])]?.name || userText, + // ), + // }); + syncUserState(screenShareUidToUpdate, { name: getScreenShareName( users[parseInt(keys[0])]?.name || userText, ), @@ -111,7 +116,13 @@ const UserPreferenceProvider = (props: { const value = JSON.parse(data?.payload); if (value) { if (value?.uid) { - updateRenderListState(value?.uid, { + // updateRenderListState(value?.uid, { + // name: + // String(value?.uid)[0] === '1' + // ? pstnUserLabel + // : value?.name || userText, + // }); + syncUserState(value?.uid, { name: String(value?.uid)[0] === '1' ? pstnUserLabel @@ -142,7 +153,11 @@ const UserPreferenceProvider = (props: { } } if (value?.screenShareUid) { - updateRenderListState(value?.screenShareUid, { + // updateRenderListState(value?.screenShareUid, { + // name: getScreenShareName(value?.name || userText), + // type: 'screenshare', + // }); + syncUserState(value?.screenShareUid, { name: getScreenShareName(value?.name || userText), type: 'screenshare', }); @@ -164,8 +179,13 @@ const UserPreferenceProvider = (props: { }); //update local state for user and screenshare - updateRenderListState(localUid, {name: displayName || userText}); - updateRenderListState(screenShareUid, { + // updateRenderListState(localUid, {name: displayName || userText}); + // updateRenderListState(screenShareUid, { + // name: getScreenShareName(displayName || userText), + // type: 'screenshare', + // }); + syncUserState(localUid, {name: displayName || userText}); + syncUserState(screenShareUid, { name: getScreenShareName(displayName || userText), type: 'screenshare', }); @@ -199,12 +219,13 @@ const UserPreferenceProvider = (props: { } }, [displayName, hasUserJoinedRTM, callActive, isHost]); - const updateRenderListState = ( - uid: number, - data: Partial, - ) => { - dispatch({type: 'UpdateRenderList', value: [uid, data]}); - }; + // Below method is now replaced with syncUserState + // const updateRenderListState = ( + // uid: number, + // data: Partial, + // ) => { + // dispatch({type: 'UpdateRenderList', value: [uid, data]}); + // }; return ( { const {video: localVideoStatus} = useLocalUserInfo(); const isLocalVideoON = localVideoStatus === ToggleState.enabled; + const {syncUserPreferences} = useRtm(); + const { rtcProps: {callActive}, } = useContext(PropsContext); @@ -161,6 +164,21 @@ const VBProvider: React.FC = ({children}) => { } }, [vbMode, selectedImage, saveVB, previewVideoTrack, isLocalVideoON]); + /* Sync VB preferences to RTM (only saves in main room) */ + React.useEffect(() => { + try { + syncUserPreferences({ + virtualBackground: { + type: + vbMode === 'blur' ? 'blur' : vbMode === 'image' ? 'image' : 'none', + ...(vbMode === 'image' && selectedImage && {imageUrl: selectedImage}), + }, + }); + } catch (error) { + console.warn('Failed to sync VB preference:', error); + } + }, [vbMode, selectedImage, syncUserPreferences]); + /* Fetch Saved Images from IndexDB to show in VBPanel */ React.useEffect(() => { const fetchData = async () => { diff --git a/template/src/pages/VideoCall.tsx b/template/src/pages/VideoCall.tsx index 4254c5adf..625b69247 100644 --- a/template/src/pages/VideoCall.tsx +++ b/template/src/pages/VideoCall.tsx @@ -24,6 +24,7 @@ import { import styles from '../components/styles'; import {useParams, useHistory} from '../components/Router'; import RtmConfigure from '../components/RTMConfigure'; +import RTMConfigureMainRoomProvider from '../rtm/RTMConfigureMainRoomProvider'; import DeviceConfigure from '../components/DeviceConfigure'; import Logo from '../subComponents/Logo'; import {useHasBrandLogo, isMobileUA, isWebInternal} from '../utils/common'; @@ -82,8 +83,8 @@ import {UserActionMenuProvider} from '../components/useUserActionMenu'; import Toast from '../../react-native-toast-message'; import {AuthErrorCodes} from '../utils/common'; import {BreakoutRoomProvider} from '../components/breakout-room/context/BreakoutRoomContext'; -import BreakoutRoomMainEventsConfigure from '../components/breakout-room/events/BreakoutRoomMainEventsConfigure'; import BreakoutRoomEventsConfigure from '../components/breakout-room/events/BreakoutRoomEventsConfigure'; +import {RTM_ROOMS} from '../rtm/constants'; interface VideoCallProps { callActive: boolean; @@ -159,98 +160,100 @@ const VideoCall = (videoCallProps: VideoCallProps) => { - - - - - - - - - - - - {!isMobileUA() && ( - - )} - - - - {/* */} - - {callActive ? ( - - - - + + + + + + + + + + + + {!isMobileUA() && ( + + )} + + + + {/* - */} + + {callActive ? ( + + + + - - - - - - - ) : $config.PRECALL ? ( - - - - ) : ( - <> - )} - - {/* */} - - - - - - - - - - - - - - + + + + + + + + ) : $config.PRECALL ? ( + + + + ) : ( + <> + )} + + {/* */} + + + + + + + + + + + + + + + diff --git a/template/src/pages/video-call/BreakoutVideoCallContent.tsx b/template/src/pages/video-call/BreakoutVideoCallContent.tsx index ae5e72577..04ca6c957 100644 --- a/template/src/pages/video-call/BreakoutVideoCallContent.tsx +++ b/template/src/pages/video-call/BreakoutVideoCallContent.tsx @@ -18,6 +18,7 @@ import { RtcPropsInterface, } from '../../../agora-rn-uikit'; import RtmConfigure from '../../components/RTMConfigure'; +import RTMConfigureBreakoutRoomProvider from '../../rtm/RTMConfigureBreakoutRoomProvider'; import DeviceConfigure from '../../components/DeviceConfigure'; import {isMobileUA} from '../../utils/common'; import {LiveStreamContextProvider} from '../../components/livestream'; @@ -53,8 +54,7 @@ import { VideoCallContentProps, } from './VideoCallContent'; import BreakoutRoomEventsConfigure from '../../components/breakout-room/events/BreakoutRoomEventsConfigure'; -import {useRTMCore} from '../../rtm/RTMCoreProvider'; -import RTMEngine from '../../rtm/RTMEngine'; +import {RTM_ROOMS} from '../../rtm/constants'; interface BreakoutVideoCallContentProps extends VideoCallContentProps { rtcProps: RtcPropsInterface; @@ -83,36 +83,6 @@ const BreakoutVideoCallContent: React.FC = ({ screenShareToken: breakoutChannelDetails?.screenShareToken || '', }); - const {client, isLoggedIn} = useRTMCore(); - - useEffect(() => { - // Cleanup on unmount - if (client && isLoggedIn && rtcProps.channel) { - console.log( - `Breakout room unmounting, subsribing to: ${rtcProps.channel}`, - ); - try { - client.subscribe(rtcProps.channel); - RTMEngine.getInstance().addChannel(rtcProps.channel, false); - } catch (error) { - console.error('Failed to unsubscribe on unmount:', error); - } - } - return () => { - if (rtcProps.channel) { - console.log( - `Breakout room unmounting, unsubscribing from: ${rtcProps.channel}`, - ); - try { - client.unsubscribe(rtcProps.channel); - RTMEngine.getInstance().removeChannel(rtcProps.channel); - } catch (error) { - console.error('Failed to unsubscribe on unmount:', error); - } - } - }; - }, [client, isLoggedIn, rtcProps.channel]); - return ( = ({ - - - - - - - - - - - - {!isMobileUA() && ( - - )} - - - - - - - - - + + + + + + + + + + + + {!isMobileUA() && ( + + )} + + + + + + + + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/template/src/pages/video-call/VideoCallContent.tsx b/template/src/pages/video-call/VideoCallContent.tsx index c045c4edb..745b8b44f 100644 --- a/template/src/pages/video-call/VideoCallContent.tsx +++ b/template/src/pages/video-call/VideoCallContent.tsx @@ -14,12 +14,13 @@ import React, {useState, useEffect, useRef, useCallback} from 'react'; import {useParams, useLocation, useHistory} from '../../components/Router'; import events from '../../rtm-events-api'; import {BreakoutChannelJoinEventPayload} from '../../components/breakout-room/state/types'; -import {CallbacksInterface, RtcPropsInterface} from 'agora-rn-uikit'; +import {CallbacksInterface, RtcPropsInterface} from '../../../agora-rn-uikit'; import VideoCall from '../VideoCall'; import BreakoutVideoCallContent from './BreakoutVideoCallContent'; import {BreakoutRoomEventNames} from '../../components/breakout-room/events/constants'; import BreakoutRoomTransition from '../../components/breakout-room/ui/BreakoutRoomTransition'; import Toast from '../../../react-native-toast-message'; +import {useMainRoomUserDisplayName} from '../../rtm/hooks/useMainRoomUserDisplayName'; export interface BreakoutChannelDetails { channel: string; @@ -50,6 +51,9 @@ const VideoCallContent: React.FC = props => { const breakoutTimeoutRef = useRef | null>(null); + const mainRoomLocalUid = props.rtcProps.uid; + const getDisplayName = useMainRoomUserDisplayName(); + // Breakout channel details (populated by RTM events) const [breakoutChannelDetails, setBreakoutChannelDetails] = useState(null); @@ -57,7 +61,7 @@ const VideoCallContent: React.FC = props => { // Track transition direction for better UX const [transitionDirection, setTransitionDirection] = useState< 'enter' | 'exit' - >('enter'); + >('exit'); // Listen for breakout room join events useEffect(() => { @@ -95,11 +99,18 @@ const VideoCallContent: React.FC = props => { })); breakoutTimeoutRef.current = null; }, 800); - + let joinMessage = ''; + const sourceUid = data?.data?.srcuid; + const senderName = getDisplayName(sourceUid); + if (sourceUid === mainRoomLocalUid) { + joinMessage = `You have joined room "${room_name}".`; + } else { + joinMessage = `Host: ${senderName} has moved you to room "${room_name}".`; + } setTimeout(() => { Toast.show({ type: 'success', - text1: `You’ve been added to ${room_name} by the host.`, + text1: joinMessage, visibilityTime: 3000, }); }, 500); @@ -122,7 +133,7 @@ const VideoCallContent: React.FC = props => { handleBreakoutJoin, ); }; - }, [phrase]); + }, [phrase, getDisplayName, mainRoomLocalUid]); // Cleanup on unmount useEffect(() => { diff --git a/template/src/pages/video-call/VideoCallStateWrapper.tsx b/template/src/pages/video-call/VideoCallStateWrapper.tsx index 326329086..2ac3eb6e7 100644 --- a/template/src/pages/video-call/VideoCallStateWrapper.tsx +++ b/template/src/pages/video-call/VideoCallStateWrapper.tsx @@ -46,6 +46,8 @@ import Toast from '../../../react-native-toast-message'; import {RTMCoreProvider} from '../../rtm/RTMCoreProvider'; import {videoView} from '../../../theme.json'; import VideoCallContent from './VideoCallContent'; +import RTMGlobalStateProvider from '../../rtm/RTMGlobalStateProvider'; +import UserGlobalPreferenceProvider from '../../components/UserGlobalPreferenceProvider'; export enum RnEncryptionEnum { /** @@ -416,14 +418,18 @@ const VideoCallStateWrapper = () => { isHost: rtcProps.role === ClientRoleType.ClientRoleBroadcaster, rtmToken: rtcProps.rtm, }}> - + + + + + ) : ( diff --git a/template/src/rtm-events-api/Events.ts b/template/src/rtm-events-api/Events.ts index 2a47ff3e1..f970b724b 100644 --- a/template/src/rtm-events-api/Events.ts +++ b/template/src/rtm-events-api/Events.ts @@ -133,7 +133,9 @@ class Events { 'case 1 executed - sending in channel', ); try { - const targetChannelId = channelId || RTMEngine.getInstance().channelUid; + const targetChannelId = + channelId || RTMEngine.getInstance().getActiveChannel(); + console.log('supriya targetChannelId', targetChannelId); logger.debug( LogSource.Events, 'CUSTOM_EVENTS', @@ -248,7 +250,7 @@ class Events { } const rtmEngine: RTMClient = RTMEngine.getInstance().engine; - const targetChannelId = RTMEngine.getInstance().channelUid; + const targetChannelId = RTMEngine.getInstance().getActiveChannel(); if (!targetChannelId || targetChannelId.trim() === '') { throw new Error('Channel ID is not set. Cannot send channel messages.'); } diff --git a/template/src/rtm/RTMConfigure-v2.tsx b/template/src/rtm/RTMConfigure-v2.tsx deleted file mode 100644 index 87436f157..000000000 --- a/template/src/rtm/RTMConfigure-v2.tsx +++ /dev/null @@ -1,774 +0,0 @@ -/* -******************************************** - Copyright © 2021 Agora Lab, Inc., all rights reserved. - AppBuilder and all associated components, source code, APIs, services, and documentation - (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be - accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. - Use without a license or in violation of any license terms and conditions (including use for - any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more - information visit https://appbuilder.agora.io. -********************************************* -*/ - -import React, {useState, useContext, useEffect, useRef} from 'react'; -import { - type GetChannelMetadataResponse, - type GetOnlineUsersResponse, - type MessageEvent, - type PresenceEvent, - type StorageEvent, - type GetUserMetadataResponse, - type SetOrUpdateUserMetadataOptions, - type Metadata, -} from 'agora-react-native-rtm'; -import { - ContentInterface, - DispatchContext, - PropsContext, - useLocalUid, -} from '../../agora-rn-uikit'; -import ChatContext from '../components/ChatContext'; -import {Platform} from 'react-native'; -import {backOff} from 'exponential-backoff'; -import {isAndroid} from '../utils/common'; -import {useContent} from 'customization-api'; -import { - safeJsonParse, - timeNow, - hasJsonStructure, - getMessageTime, - get32BitUid, -} from '../rtm/utils'; -import {EventUtils, EventsQueue} from '../rtm-events'; -import {PersistanceLevel} from '../rtm-events-api'; -import {filterObject} from '../utils'; -import SDKEvents from '../utils/SdkEvents'; -import isSDK from '../utils/isSDK'; -import { - WaitingRoomStatus, - useRoomInfo, -} from '../components/room-info/useRoomInfo'; -import LocalEventEmitter, { - LocalEventsEnum, -} from '../rtm-events-api/LocalEvents'; -import {controlMessageEnum} from '../components/ChatContext'; -import {LogSource, logger} from '../logger/AppBuilderLogger'; -import {RECORDING_BOT_UID} from '../utils/constants'; -import { - nativeChannelTypeMapping, - nativePresenceEventTypeMapping, - nativeStorageEventTypeMapping, -} from '../../bridge/rtm/web/Types'; -import {useRTMCore} from './RTMCoreProvider'; -import RTMEngine from './RTMEngine'; - -export enum UserType { - ScreenShare = 'screenshare', -} - -const eventTimeouts = new Map>(); - -interface RTMConfigureProps { - children: React.ReactNode; - channelName: string; - callActive: boolean; -} - -const RTMConfigure = (props: RTMConfigureProps) => { - const {channelName, callActive, children} = props; - const {client, isLoggedIn} = useRTMCore(); - const localUid = useLocalUid(); - const {dispatch} = useContext(DispatchContext); - const {defaultContent, activeUids} = useContent(); - const { - waitingRoomStatus, - data: {isHost}, - } = useRoomInfo(); - - const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); - const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); - const [onlineUsersCount, setTotalOnlineUsers] = useState(0); - const timerValueRef: any = useRef(5); - const rtmInitTimstamp = useRef(new Date().getTime()); - - // Subscribe to main channel - traditional approach for core functionality - useEffect(() => { - if (!isLoggedIn || !channelName || !client || !callActive) { - return; - } - const subscribeToMainChannel = async () => { - try { - logger.log( - LogSource.AgoraSDK, - 'RTMConfigure', - `Subscribing to main channel: ${channelName}`, - ); - - await client.subscribe(channelName, { - withMessage: true, - withPresence: true, - withMetadata: true, - withLock: false, - }); - RTMEngine.getInstance().addChannel(channelName); - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM setChannelId as subscribe is successful', - channelName, - ); - - logger.debug( - LogSource.SDK, - 'Event', - 'Emitting rtm joined', - channelName, - ); - // @ts-ignore - SDKEvents.emit('_rtm-joined', rtcProps.channel); - timerValueRef.current = 5; - await getMembers(); - await readAllChannelAttributes(); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - '❌ Main channel subscription failed:', - error, - ); - setTimeout(async () => { - // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); - subscribeToMainChannel(); - }, timerValueRef.current * 1000); - } - }; - - const runQueuedEvents = async () => { - try { - while (!EventsQueue.isEmpty()) { - const currEvt = EventsQueue.dequeue(); - await eventDispatcher(currEvt.data, `${currEvt.uid}`, currEvt.ts); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Error running queued events', - error, - ); - } - }; - timerValueRef.current = 5; - subscribeToMainChannel(); - setHasUserJoinedRTM(true); - await runQueuedEvents(); - setIsInitialQueueCompleted(true); - return () => { - // Cleanup: unsubscribe from main channel - if (client && channelName) { - client.unsubscribe(channelName).catch(error => { - logger.warn( - LogSource.AgoraSDK, - 'RTMConfigure', - `Failed to unsubscribe from ${channelName}:`, - error, - ); - }); - logger.log( - LogSource.AgoraSDK, - 'RTMConfigure', - `🔌 Unsubscribed from main channel: ${channelName}`, - ); - } - }; - }, [isLoggedIn, channelName, client, callActive]); - - /** - * State refs for event callbacks - */ - const isHostRef = useRef({isHost: isHost}); - useEffect(() => { - isHostRef.current.isHost = isHost; - }, [isHost]); - - const waitingRoomStatusRef = useRef({waitingRoomStatus: waitingRoomStatus}); - useEffect(() => { - waitingRoomStatusRef.current.waitingRoomStatus = waitingRoomStatus; - }, [waitingRoomStatus]); - - const activeUidsRef = useRef({activeUids: activeUids}); - useEffect(() => { - activeUidsRef.current.activeUids = activeUids; - }, [activeUids]); - - const defaultContentRef = useRef({defaultContent: defaultContent}); - useEffect(() => { - defaultContentRef.current.defaultContent = defaultContent; - }, [defaultContent]); - - // Cleanup timeouts on unmount - const isRTMMounted = useRef(true); - useEffect(() => { - return () => { - isRTMMounted.current = false; - for (const timeout of eventTimeouts.values()) { - clearTimeout(timeout); - } - eventTimeouts.clear(); - }; - }, []); - - // Set online users count - useEffect(() => { - setTotalOnlineUsers( - Object.keys( - filterObject( - defaultContent, - ([k, v]) => - v?.type === 'rtc' && - !v.offline && - activeUidsRef.current.activeUids.indexOf(v?.uid) !== -1, - ), - ).length, - ); - }, [defaultContent]); - - // Handle channel joined - fetch initial data - const handleChannelJoined = async () => { - try { - await Promise.all([getMembers(), readAllChannelAttributes()]); - await runQueuedEvents(); - setIsInitialQueueCompleted(true); - logger.log( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Channel initialization completed', - ); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Channel initialization failed', - error, - ); - } - }; - - const getMembers = async () => { - if (!client || !channelName) { - return; - } - try { - const data: GetOnlineUsersResponse = await client.presence.getOnlineUsers( - channelName, - 1, - ); - logger.log( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Online users fetched', - data, - ); - - await Promise.all( - data.occupants?.map(async member => { - try { - const backoffAttributes = await fetchUserAttributesWithBackoffRetry( - member.userId, - ); - await processUserUidAttributes(backoffAttributes, member.userId); - - // Add user attributes to queue for processing - backoffAttributes?.items?.forEach(item => { - try { - if (hasJsonStructure(item.value as string)) { - const eventData = { - evt: item.key, - value: item.value, - }; - EventsQueue.enqueue({ - data: eventData, - uid: member.userId, - ts: timeNow(), - }); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - `Failed to process user attribute for ${member.userId}`, - error, - ); - } - }); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - `Could not retrieve data for ${member.userId}`, - error, - ); - } - }) || [], - ); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Failed to get members', - error, - ); - } - }; - - const readAllChannelAttributes = async () => { - if (!client || !channelName) { - return; - } - try { - const data: GetChannelMetadataResponse = - await client.storage.getChannelMetadata(channelName, 1); - - for (const item of data.items) { - try { - const {key, value, authorUserId, updateTs} = item; - if (hasJsonStructure(value as string)) { - const evtData = { - evt: key, - value, - }; - EventsQueue.enqueue({ - data: evtData, - uid: authorUserId, - ts: updateTs, - }); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - `Failed to process channel attribute: ${JSON.stringify(item)}`, - error, - ); - } - } - logger.log( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Channel attributes read successfully', - data, - ); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Failed to read channel attributes', - error, - ); - } - }; - - const fetchUserAttributesWithBackoffRetry = async ( - userId: string, - ): Promise => { - if (!client) throw new Error('RTM client not available'); - - return backOff( - async () => { - const attr: GetUserMetadataResponse = - await client.storage.getUserMetadata({ - userId: userId, - }); - - if (!attr || !attr.items || attr.items.length === 0) { - throw new Error('No attributes found'); - } - - return attr; - }, - { - retry: (e, idx) => { - logger.debug( - LogSource.AgoraSDK, - 'RTMConfigure', - `Retrying user attributes fetch for ${userId}, attempt ${idx}`, - e, - ); - return true; - }, - }, - ); - }; - - const processUserUidAttributes = async ( - attr: GetUserMetadataResponse, - userId: string, - ) => { - try { - const uid = parseInt(userId, 10); - const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); - const isHostItem = attr?.items?.find(item => item.key === 'isHost'); - const screenUid = screenUidItem?.value - ? parseInt(screenUidItem.value, 10) - : undefined; - - // Update user data in RTC - const userData = { - screenUid: screenUid, - type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', - uid, - offline: false, - isHost: isHostItem?.value === 'true', - lastMessageTimeStamp: 0, - }; - updateRenderListState(uid, userData); - - // Update screenshare data in RTC - if (screenUid) { - const screenShareUser = { - type: UserType.ScreenShare, - parentUid: uid, - }; - updateRenderListState(screenUid, screenShareUser); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - `Failed to process user data for ${userId}`, - error, - ); - } - }; - - const updateRenderListState = ( - uid: number, - data: Partial, - ) => { - dispatch({type: 'UpdateRenderList', value: [uid, data]}); - }; - - const eventDispatcher = async ( - data: { - evt: string; - value: string; - feat?: string; - etyp?: string; - }, - sender: string, - ts: number, - ) => { - let evt = '', - value = ''; - - if (data?.feat === 'BREAKOUT_ROOM') { - const outputData = { - evt: `${data.feat}_${data.etyp}`, - payload: JSON.stringify({ - data: data.data, - action: data.act, - }), - persistLevel: 1, - source: 'core', - }; - const formattedData = JSON.stringify(outputData); - evt = data.feat + '_' + data.etyp; - value = formattedData; - } else if (data?.feat === 'WAITING_ROOM') { - if (data?.etyp === 'REQUEST') { - const outputData = { - evt: `${data.feat}_${data.etyp}`, - payload: JSON.stringify({ - attendee_uid: data.data.data.attendee_uid, - attendee_screenshare_uid: data.data.data.attendee_screenshare_uid, - }), - persistLevel: 1, - source: 'core', - }; - const formattedData = JSON.stringify(outputData); - evt = data.feat + '_' + data.etyp; - value = formattedData; - } - if (data?.etyp === 'RESPONSE') { - const outputData = { - evt: `${data.feat}_${data.etyp}`, - payload: JSON.stringify({ - approved: data.data.data.approved, - channelName: data.data.data.channel_name, - mainUser: data.data.data.mainUser, - screenShare: data.data.data.screenShare, - whiteboard: data.data.data.whiteboard, - chat: data.data.data?.chat, - }), - persistLevel: 1, - source: 'core', - }; - const formattedData = JSON.stringify(outputData); - evt = data.feat + '_' + data.etyp; - value = formattedData; - } - } else { - if ( - $config.ENABLE_WAITING_ROOM && - !isHostRef.current?.isHost && - waitingRoomStatusRef.current?.waitingRoomStatus !== - WaitingRoomStatus.APPROVED - ) { - if ( - data.evt === controlMessageEnum.muteAudio || - data.evt === controlMessageEnum.muteVideo - ) { - return; - } else { - evt = data.evt; - value = data.value; - } - } else { - evt = data.evt; - value = data.value; - } - } - - try { - let parsedValue; - try { - parsedValue = typeof value === 'string' ? JSON.parse(value) : value; - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Failed to parse event value', - error, - ); - return; - } - - const {payload, persistLevel, source} = parsedValue; - - // Set local attributes if needed - if (persistLevel === PersistanceLevel.Session && client) { - const rtmAttribute = {key: evt, value: value}; - const options: SetOrUpdateUserMetadataOptions = { - userId: `${localUid}`, - }; - await client.storage.setUserMetadata( - { - items: [rtmAttribute], - }, - options, - ); - } - - // Emit the event - EventUtils.emitEvent(evt, source, {payload, persistLevel, sender, ts}); - - // Handle name events with timeout - if (evt === 'name') { - if (eventTimeouts.has(sender)) { - clearTimeout(eventTimeouts.get(sender)!); - } - const timeout = setTimeout(() => { - if (!isRTMMounted.current) { - return; - } - EventUtils.emitEvent(evt, source, { - payload, - persistLevel, - sender, - ts, - }); - eventTimeouts.delete(sender); - }, 200); - eventTimeouts.set(sender, timeout); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Error dispatching event', - error, - ); - } - }; - - // Event listeners setup - useEffect(() => { - if (!client) { - return; - } - - const handleStorageEvent = (storage: StorageEvent) => { - if ( - storage.eventType === nativeStorageEventTypeMapping.SET || - storage.eventType === nativeStorageEventTypeMapping.UPDATE - ) { - try { - if (storage.data?.items && Array.isArray(storage.data.items)) { - storage.data.items.forEach(item => { - try { - if (!item || !item.key) return; - const {key, value, authorUserId, updateTs} = item; - const timestamp = getMessageTime(updateTs); - const sender = Platform.OS - ? get32BitUid(authorUserId) - : parseInt(authorUserId, 10); - eventDispatcher({evt: key, value}, `${sender}`, timestamp); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Failed to process storage item', - error, - ); - } - }); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Error handling storage event', - error, - ); - } - } - }; - - const handlePresenceEvent = async (presence: PresenceEvent) => { - if (`${localUid}` === presence.publisher) { - return; - } - - if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { - logger.log( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Remote user joined', - presence, - ); - try { - const backoffAttributes = await fetchUserAttributesWithBackoffRetry( - presence.publisher, - ); - await processUserUidAttributes(backoffAttributes, presence.publisher); - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Failed to process joined user', - error, - ); - } - } - - if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { - logger.log( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Remote user left', - presence, - ); - const uid = presence?.publisher - ? parseInt(presence.publisher, 10) - : undefined; - if (uid) { - SDKEvents.emit('_rtm-left', uid); - updateRenderListState(uid, {offline: true}); - } - } - }; - - const handleMessageEvent = (message: MessageEvent) => { - if (`${localUid}` === message.publisher) { - return; - } - - if (message.channelType === nativeChannelTypeMapping.MESSAGE) { - const { - publisher: uid, - channelName: msgChannelName, - message: text, - timestamp: ts, - } = message; - - // Whiteboard upload handling - if (parseInt(uid, 10) === 1010101) { - const [err, res] = safeJsonParse(text); - if (!err && res?.data?.data?.images) { - LocalEventEmitter.emit( - LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, - res.data.data.images, - ); - } - return; - } - - // Regular messages - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Failed to parse message', - err, - ); - return; - } - - const timestamp = getMessageTime(ts); - const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); - - if (msgChannelName === channelName) { - eventDispatcher(msg, `${sender}`, timestamp); - } - } - - if (message.channelType === nativeChannelTypeMapping.USER) { - const {publisher: peerId, timestamp: ts, message: text} = message; - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.AgoraSDK, - 'RTMConfigure', - 'Failed to parse user message', - err, - ); - return; - } - - const timestamp = getMessageTime(ts); - const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); - eventDispatcher(msg, `${sender}`, timestamp); - } - }; - - // Add event listeners - client.addEventListener('storage', handleStorageEvent); - client.addEventListener('presence', handlePresenceEvent); - client.addEventListener('message', handleMessageEvent); - - return () => { - // Remove event listeners - client.removeEventListener('storage', handleStorageEvent); - client.removeEventListener('presence', handlePresenceEvent); - client.removeEventListener('message', handleMessageEvent); - }; - }, [client, channelName, localUid]); - - return ( - - {children} - - ); -}; - -export default RTMConfigure; diff --git a/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx b/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx new file mode 100644 index 000000000..f92877706 --- /dev/null +++ b/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx @@ -0,0 +1,973 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the “Materials”) are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.’s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, { + useState, + useContext, + useEffect, + useRef, + createContext, + useCallback, +} from 'react'; +import { + type GetChannelMetadataResponse, + type GetOnlineUsersResponse, + type MessageEvent, + type PresenceEvent, + type SetOrUpdateUserMetadataOptions, + type StorageEvent, + type GetUserMetadataResponse, +} from 'agora-react-native-rtm'; +import { + ContentInterface, + DispatchContext, + useLocalUid, +} from '../../agora-rn-uikit'; +import ChatContext from '../components/ChatContext'; +import {Platform} from 'react-native'; +import {backOff} from 'exponential-backoff'; +import {isAndroid, isIOS} from '../utils/common'; +import {useContent} from 'customization-api'; +import { + safeJsonParse, + timeNow, + hasJsonStructure, + getMessageTime, + get32BitUid, +} from '../rtm/utils'; +import {EventUtils, EventsQueue} from '../rtm-events'; +import {PersistanceLevel} from '../rtm-events-api'; +import RTMEngine from '../rtm/RTMEngine'; +import {filterObject} from '../utils'; +import SDKEvents from '../utils/SdkEvents'; +import {useAsyncEffect} from '../utils/useAsyncEffect'; +import { + WaitingRoomStatus, + useRoomInfo, +} from '../components/room-info/useRoomInfo'; +import LocalEventEmitter, { + LocalEventsEnum, +} from '../rtm-events-api/LocalEvents'; +import {controlMessageEnum} from '../components/ChatContext'; +import {LogSource, logger} from '../logger/AppBuilderLogger'; +import {RECORDING_BOT_UID} from '../utils/constants'; +import { + nativeChannelTypeMapping, + nativePresenceEventTypeMapping, + nativeStorageEventTypeMapping, +} from '../../bridge/rtm/web/Types'; +import {useRTMCore} from '../rtm/RTMCoreProvider'; +import {RTM_ROOMS} from './constants'; +import {useUserGlobalPreferences} from '../components/UserGlobalPreferenceProvider'; +import {ToggleState} from '../../agora-rn-uikit'; +import useMuteToggleLocal from '../utils/useMuteToggleLocal'; +import {useRtc} from 'customization-api'; + +export enum UserType { + ScreenShare = 'screenshare', +} + +const eventTimeouts = new Map>(); + +// RTM Breakout Room Context +export interface RTMBreakoutRoomData { + hasUserJoinedRTM: boolean; + isInitialQueueCompleted: boolean; + onlineUsersCount: number; + rtmInitTimstamp: number; + syncUserState: (uid: number, data: Partial) => void; +} + +const RTMBreakoutRoomContext = createContext({ + hasUserJoinedRTM: false, + isInitialQueueCompleted: false, + onlineUsersCount: 0, + rtmInitTimstamp: 0, + syncUserState: () => {}, +}); + +export const useRTMConfigureBreakout = () => { + const context = useContext(RTMBreakoutRoomContext); + if (!context) { + throw new Error( + 'useRTMConfigureBreakout must be used within RTMConfigureBreakoutRoomProvider', + ); + } + return context; +}; + +interface RTMConfigureBreakoutRoomProviderProps { + callActive: boolean; + children: React.ReactNode; + channelName: string; +} + +const RTMConfigureBreakoutRoomProvider = ( + props: RTMConfigureBreakoutRoomProviderProps, +) => { + const rtmInitTimstamp = new Date().getTime(); + const localUid = useLocalUid(); + const {callActive, channelName} = props; + const {dispatch} = useContext(DispatchContext); + const {defaultContent, activeUids} = useContent(); + const { + waitingRoomStatus, + data: {isHost}, + } = useRoomInfo(); + const {applyUserPreferences, syncUserPreferences} = + useUserGlobalPreferences(); + const toggleMute = useMuteToggleLocal(); + const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); + const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); + const [onlineUsersCount, setTotalOnlineUsers] = useState(0); + const timerValueRef: any = useRef(5); + // Track RTM connection state (equivalent to v1.5x connectionState check) + const {client, isLoggedIn, registerCallbacks, unregisterCallbacks} = + useRTMCore(); + const {rtcTracksReady} = useRtc(); + + /** + * inside event callback state won't have latest value. + * so creating ref to access the state + */ + const isHostRef = useRef({isHost: isHost}); + useEffect(() => { + isHostRef.current.isHost = isHost; + }, [isHost]); + + const waitingRoomStatusRef = useRef({waitingRoomStatus: waitingRoomStatus}); + useEffect(() => { + waitingRoomStatusRef.current.waitingRoomStatus = waitingRoomStatus; + }, [waitingRoomStatus]); + + const activeUidsRef = useRef({activeUids: activeUids}); + useEffect(() => { + activeUidsRef.current.activeUids = activeUids; + }, [activeUids]); + + const defaultContentRef = useRef(defaultContent); + useEffect(() => { + defaultContentRef.current = defaultContent; + }, [defaultContent]); + + // Eventdispatcher timeout refs clean + const isRTMMounted = useRef(true); + useEffect(() => { + return () => { + isRTMMounted.current = false; + // Clear all pending timeouts on unmount + for (const timeout of eventTimeouts.values()) { + clearTimeout(timeout); + } + eventTimeouts.clear(); + }; + }, []); + + // Apply user preferences when breakout room mounts + useEffect(() => { + if (rtcTracksReady) { + console.log('supriya-permissions', defaultContentRef.current[localUid]); + applyUserPreferences(defaultContentRef.current[localUid], toggleMute); + } + }, [rtcTracksReady]); + + // Sync current audio/video state audio video changes + useEffect(() => { + const userData = defaultContent[localUid]; + if (rtcTracksReady && userData) { + console.log('UP: syncing userData: ', userData); + const preferences = { + audioMuted: userData.audio === ToggleState.disabled, + videoMuted: userData.video === ToggleState.disabled, + }; + console.log('UP: saved preferences: ', preferences); + syncUserPreferences(preferences); + } + }, [defaultContent, localUid, syncUserPreferences, rtcTracksReady]); + + const syncUserState = useCallback( + (uid: number, data: Partial) => { + dispatch({type: 'UpdateRenderList', value: [uid, data]}); + }, + [dispatch], + ); + + // Set online users + React.useEffect(() => { + setTotalOnlineUsers( + Object.keys( + filterObject( + defaultContent, + ([k, v]) => + v?.type === 'rtc' && + !v.offline && + activeUidsRef.current.activeUids.indexOf(v?.uid) !== -1, + ), + ).length, + ); + }, [defaultContent]); + + const init = async () => { + await subscribeChannel(); + setHasUserJoinedRTM(true); + await runQueuedEvents(); + setIsInitialQueueCompleted(true); + logger.log(LogSource.AgoraSDK, 'Log', 'RTM queued events finished running'); + }; + + const subscribeChannel = async () => { + try { + if (RTMEngine.getInstance().allChannels.includes(channelName)) { + logger.debug( + LogSource.AgoraSDK, + 'Log', + '🚫 RTM already subscribed channel skipping', + channelName, + ); + } else { + await client.subscribe(channelName, { + withMessage: true, + withPresence: true, + withMetadata: true, + withLock: false, + }); + logger.log(LogSource.AgoraSDK, 'API', 'RTM subscribeChannel', { + data: channelName, + }); + + // Set channel ID AFTER successful subscribe (like v1.5x) + console.log('setting primary channel', channelName); + RTMEngine.getInstance().addChannel(RTM_ROOMS.BREAKOUT, channelName); + RTMEngine.getInstance().setActiveChannel(RTM_ROOMS.BREAKOUT); + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM setChannelId as subscribe is successful', + channelName, + ); + logger.debug( + LogSource.SDK, + 'Event', + 'Emitting rtm joined', + channelName, + ); + // @ts-ignore + SDKEvents.emit('_rtm-joined', channelName); + timerValueRef.current = 5; + await getMembers(); + await readAllChannelAttributes(); + logger.log( + LogSource.AgoraSDK, + 'Log', + 'RTM readAllChannelAttributes and getMembers done', + ); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + 'RTM subscribeChannel failed..Trying again', + {error}, + ); + setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); + subscribeChannel(); + }, timerValueRef.current * 1000); + } + }; + + const getMembers = async () => { + try { + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM presence.getOnlineUsers(getMembers) start', + ); + await client.presence + .getOnlineUsers(channelName, 1) + .then(async (data: GetOnlineUsersResponse) => { + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM presence.getOnlineUsers data received', + data, + ); + await Promise.all( + data.occupants?.map(async member => { + try { + const backoffAttributes = + await fetchUserAttributesWithBackoffRetry(member.userId); + + await processUserUidAttributes( + backoffAttributes, + member.userId, + ); + // setting screenshare data + // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) + // isActive to identify all active screenshare users in the call + backoffAttributes?.items?.forEach(item => { + try { + if (hasJsonStructure(item.value as string)) { + const data = { + evt: item.key, // Use item.key instead of key + value: item.value, // Use item.value instead of value + }; + // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events + EventsQueue.enqueue({ + data: data, + uid: member.userId, + ts: timeNow(), + }); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + `RTM Failed to process user attribute item for ${ + member.userId + }: ${JSON.stringify(item)}`, + {error}, + ); + // Continue processing other items + } + }); + } catch (e) { + logger.error( + LogSource.AgoraSDK, + 'Log', + `RTM Could not retrieve name of ${member.userId}`, + {error: e}, + ); + } + }), + ); + logger.debug( + LogSource.AgoraSDK, + 'Log', + 'RTM fetched all data and user attr...RTM init done', + ); + }); + timerValueRef.current = 5; + } catch (error) { + setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); + await getMembers(); + }, timerValueRef.current * 1000); + } + }; + + const readAllChannelAttributes = async () => { + try { + await client.storage + .getChannelMetadata(channelName, 1) + .then(async (data: GetChannelMetadataResponse) => { + for (const item of data.items) { + try { + const {key, value, authorUserId, updateTs} = item; + if (hasJsonStructure(value as string)) { + const evtData = { + evt: key, + value, + }; + // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events + EventsQueue.enqueue({ + data: evtData, + uid: authorUserId, + ts: updateTs, + }); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + `RTM Failed to process channel attribute item: ${JSON.stringify( + item, + )}`, + {error}, + ); + // Continue processing other items + } + } + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM storage.getChannelMetadata data received', + data, + ); + }); + timerValueRef.current = 5; + } catch (error) { + setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + timerValueRef.current = Math.min(timerValueRef.current * 2, 30); + await readAllChannelAttributes(); + }, timerValueRef.current * 1000); + } + }; + + const fetchUserAttributesWithBackoffRetry = async ( + userId: string, + ): Promise => { + return backOff( + async () => { + logger.log( + LogSource.AgoraSDK, + 'API', + `RTM fetching getUserMetadata for member ${userId}`, + ); + + const attr: GetUserMetadataResponse = + await client.storage.getUserMetadata({ + userId: userId, + }); + + if (!attr || !attr.items) { + logger.log( + LogSource.AgoraSDK, + 'API', + 'RTM attributes for member not found', + ); + throw attr; + } + + logger.log( + LogSource.AgoraSDK, + 'API', + `RTM getUserMetadata for member ${userId} received`, + {attr}, + ); + + if (attr.items && attr.items.length > 0) { + return attr; + } else { + throw attr; + } + }, + { + retry: (e, idx) => { + logger.debug( + LogSource.AgoraSDK, + 'Log', + `RTM [retrying] Attempt ${idx}. Fetching ${userId}'s attributes`, + e, + ); + return true; + }, + }, + ); + }; + + const processUserUidAttributes = async ( + attr: GetUserMetadataResponse, + userId: string, + ) => { + try { + console.log('[user attributes]:', {attr}); + const uid = parseInt(userId, 10); + const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); + const isHostItem = attr?.items?.find(item => item.key === 'isHost'); + const nameItem = attr?.items?.find(item => item.key === 'name'); + const screenUid = screenUidItem?.value + ? parseInt(screenUidItem.value, 10) + : undefined; + + let userName = ''; + if (nameItem?.value) { + try { + const parsedValue = JSON.parse(nameItem.value); + const payloadString = parsedValue.payload; + if (payloadString) { + const payload = JSON.parse(payloadString); + userName = payload.name; + } + } catch (parseError) {} + } + + //start - updating user data in rtc + const userData = { + screenUid: screenUid, + //below thing for livestreaming + type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', + uid, + name: userName, + offline: false, + isHost: isHostItem?.value || false, + lastMessageTimeStamp: 0, + }; + console.log('new user joined', uid, userData); + syncUserState(uid, userData); + //end- updating user data in rtc + + //start - updating screenshare data in rtc + if (screenUid) { + const screenShareUser = { + type: UserType.ScreenShare, + parentUid: uid, + }; + syncUserState(screenUid, screenShareUser); + } + //end - updating screenshare data in rtc + } catch (e) { + logger.error( + LogSource.AgoraSDK, + 'Event', + `RTM Failed to process user data for ${userId}`, + {error: e}, + ); + } + }; + + const runQueuedEvents = async () => { + try { + while (!EventsQueue.isEmpty()) { + const currEvt = EventsQueue.dequeue(); + await eventDispatcher(currEvt.data, `${currEvt.uid}`, currEvt.ts); + } + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while running queue events', + {error}, + ); + } + }; + + const eventDispatcher = async ( + data: { + evt: string; + value: string; + feat?: string; + etyp?: string; + }, + sender: string, + ts: number, + ) => { + console.log( + LogSource.Events, + 'CUSTOM_EVENTS', + 'inside eventDispatcher ', + data, + ); + + let evt = '', + value = ''; + + if (data?.feat === 'BREAKOUT_ROOM') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + data: data.data, + action: data.act, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } else if (data?.feat === 'WAITING_ROOM') { + if (data?.etyp === 'REQUEST') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + attendee_uid: data.data.data.attendee_uid, + attendee_screenshare_uid: data.data.data.attendee_screenshare_uid, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } + if (data?.etyp === 'RESPONSE') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + approved: data.data.data.approved, + channelName: data.data.data.channel_name, + mainUser: data.data.data.mainUser, + screenShare: data.data.data.screenShare, + whiteboard: data.data.data.whiteboard, + chat: data.data.data?.chat, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } + } else { + if ( + $config.ENABLE_WAITING_ROOM && + !isHostRef.current?.isHost && + waitingRoomStatusRef.current?.waitingRoomStatus !== + WaitingRoomStatus.APPROVED + ) { + if ( + data.evt === controlMessageEnum.muteAudio || + data.evt === controlMessageEnum.muteVideo + ) { + return; + } else { + evt = data.evt; + value = data.value; + } + } else { + evt = data.evt; + value = data.value; + } + } + + try { + let parsedValue; + try { + parsedValue = typeof value === 'string' ? JSON.parse(value) : value; + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'RTM Failed to parse event value in event dispatcher:', + {error}, + ); + return; + } + const {payload, persistLevel, source} = parsedValue; + // Step 1: Set local attributes + if (persistLevel === PersistanceLevel.Session) { + const rtmAttribute = {key: evt, value: value}; + const options: SetOrUpdateUserMetadataOptions = { + userId: `${localUid}`, + }; + await client.storage.setUserMetadata( + { + items: [rtmAttribute], + }, + options, + ); + } + // Step 2: Emit the event + console.log(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: ', evt); + EventUtils.emitEvent(evt, source, {payload, persistLevel, sender, ts}); + // Because async gets evaluated in a different order when in an sdk + if (evt === 'name') { + // 1. Cancel existing timeout for this sender + if (eventTimeouts.has(sender)) { + clearTimeout(eventTimeouts.get(sender)!); + } + // 2. Create new timeout with tracking + const timeout = setTimeout(() => { + // 3. Guard against unmounted component + if (!isRTMMounted.current) { + return; + } + EventUtils.emitEvent(evt, source, { + payload, + persistLevel, + sender, + ts, + }); + // 4. Clean up after execution + eventTimeouts.delete(sender); + }, 200); + // 5. Track the timeout for cleanup + eventTimeouts.set(sender, timeout); + } + } catch (error) { + console.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while emiting event:', + {error}, + ); + } + }; + + // Register listeners when client is created + useEffect(() => { + if (!client) { + return; + } + + const handleStorageEvent = (storage: StorageEvent) => { + // when remote user sets/updates metadata - 3 + if ( + storage.eventType === nativeStorageEventTypeMapping.SET || + storage.eventType === nativeStorageEventTypeMapping.UPDATE + ) { + const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; + const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; + logger.log( + LogSource.AgoraSDK, + 'Event', + `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, + storage, + ); + try { + if (storage.data?.items && Array.isArray(storage.data.items)) { + storage.data.items.forEach(item => { + try { + if (!item || !item.key) { + logger.warn( + LogSource.Events, + 'CUSTOM_EVENTS', + 'Invalid storage item:', + item, + ); + return; + } + + const {key, value, authorUserId, updateTs} = item; + const timestamp = getMessageTime(updateTs); + const sender = Platform.OS + ? get32BitUid(authorUserId) + : parseInt(authorUserId, 10); + eventDispatcher( + { + evt: key, + value, + }, + `${sender}`, + timestamp, + ); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + `Failed to process storage item: ${JSON.stringify(item)}`, + {error}, + ); + } + }); + } + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + }; + + const handlePresenceEvent = async (presence: PresenceEvent) => { + if (`${localUid}` === presence.publisher) { + return; + } + if (presence.channelName !== channelName) { + console.log( + 'supriya event recevied in channel', + presence.channelName, + channelName, + ); + return; + } + // remoteJoinChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { + logger.log( + LogSource.AgoraSDK, + 'Event', + 'RTM presenceEvent of type [3 - remoteJoin] (channelMemberJoined)', + ); + const backoffAttributes = await fetchUserAttributesWithBackoffRetry( + presence.publisher, + ); + await processUserUidAttributes(backoffAttributes, presence.publisher); + } + // remoteLeaveChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { + logger.log( + LogSource.AgoraSDK, + 'Event', + 'RTM presenceEvent of type [4 - remoteLeave] (channelMemberLeft)', + presence, + ); + // Chat of left user becomes undefined. So don't cleanup + const uid = presence?.publisher + ? parseInt(presence.publisher, 10) + : undefined; + + if (!uid) { + return; + } + SDKEvents.emit('_rtm-left', uid); + // updating the rtc data + syncUserState(uid, { + offline: true, + }); + } + }; + + const handleMessageEvent = (message: MessageEvent) => { + console.log('supriya current message channel: ', channelName); + console.log('supriya message event is', message); + // message - 1 (channel) + if (message.channelType === nativeChannelTypeMapping.MESSAGE) { + // here the channel name will be the channel name + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', + message, + ); + const { + publisher: uid, + channelName, + message: text, + timestamp: ts, + } = message; + //whiteboard upload + if (parseInt(uid, 10) === 1010101) { + const [err, res] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + if (res?.data?.data?.images) { + LocalEventEmitter.emit( + LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, + res?.data?.data?.images, + ); + } + } else { + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + } + + // message - 3 (user) + if (message.channelType === nativeChannelTypeMapping.USER) { + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [3- USER] (messageReceived)', + message, + ); + // here the (message.channelname) channel name will be the to UID + const {publisher: peerId, timestamp: ts, message: text} = message; + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + + const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); + + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + }; + + registerCallbacks(channelName, { + storage: handleStorageEvent, + presence: handlePresenceEvent, + message: handleMessageEvent, + }); + + return () => { + unregisterCallbacks(channelName); + }; + }, [client, channelName]); + + const unsubscribeAndCleanup = async (channel: string) => { + if (!callActive || !isLoggedIn) { + return; + } + try { + client.unsubscribe(channel); + RTMEngine.getInstance().removeChannel(channel); + logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); + if (isIOS() || isAndroid()) { + EventUtils.clear(); + } + setHasUserJoinedRTM(false); + setIsInitialQueueCompleted(false); + logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); + } catch (unsubscribeError) { + console.log('supriya error while unsubscribing: ', unsubscribeError); + } + }; + + useAsyncEffect(async () => { + try { + if (client && isLoggedIn && callActive) { + await init(); + } + } catch (error) { + logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); + } + return async () => { + if (client) { + await unsubscribeAndCleanup(channelName); + } + }; + }, [isLoggedIn, callActive, channelName, client]); + + const contextValue: RTMBreakoutRoomData = { + hasUserJoinedRTM, + isInitialQueueCompleted, + onlineUsersCount, + rtmInitTimstamp, + syncUserState, + }; + + return ( + + {props.children} + + ); +}; + +export default RTMConfigureBreakoutRoomProvider; diff --git a/template/src/rtm/RTMConfigureMainRoomProvider.tsx b/template/src/rtm/RTMConfigureMainRoomProvider.tsx new file mode 100644 index 000000000..f675d810e --- /dev/null +++ b/template/src/rtm/RTMConfigureMainRoomProvider.tsx @@ -0,0 +1,674 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, { + useState, + useContext, + useEffect, + useRef, + createContext, + useCallback, +} from 'react'; +import { + type GetChannelMetadataResponse, + type GetOnlineUsersResponse, + type LinkStateEvent, + type MessageEvent, + type Metadata, + type PresenceEvent, + type SetOrUpdateUserMetadataOptions, + type StorageEvent, + type RTMClient, + type GetUserMetadataResponse, +} from 'agora-react-native-rtm'; +import { + ContentInterface, + DispatchContext, + PropsContext, + UidType, + useLocalUid, +} from '../../agora-rn-uikit'; +import {Platform} from 'react-native'; +import {isAndroid, isIOS, isWebInternal} from '../utils/common'; +import {useContent} from 'customization-api'; +import {safeJsonParse, getMessageTime, get32BitUid} from './utils'; +import {EventUtils, EventsQueue} from '../rtm-events'; +import RTMEngine from './RTMEngine'; +import {filterObject} from '../utils'; +import SDKEvents from '../utils/SdkEvents'; +import isSDK from '../utils/isSDK'; +import {useAsyncEffect} from '../utils/useAsyncEffect'; +import { + WaitingRoomStatus, + useRoomInfo, +} from '../components/room-info/useRoomInfo'; +import LocalEventEmitter, { + LocalEventsEnum, +} from '../rtm-events-api/LocalEvents'; +import {controlMessageEnum} from '../components/ChatContext'; +import {LogSource, logger} from '../logger/AppBuilderLogger'; +import { + nativeChannelTypeMapping, + nativeStorageEventTypeMapping, +} from '../../bridge/rtm/web/Types'; +import {useRTMCore} from './RTMCoreProvider'; +import {RTMUserData, useRTMGlobalState} from './RTMGlobalStateProvider'; +import {useUserGlobalPreferences} from '../components/UserGlobalPreferenceProvider'; +import {ToggleState} from '../../agora-rn-uikit'; +import useMuteToggleLocal from '../utils/useMuteToggleLocal'; +import {RTM_ROOMS} from './constants'; +import {useRtc} from 'customization-api'; + +const eventTimeouts = new Map>(); + +// RTM Main Room Context +export interface RTMMainRoomData { + hasUserJoinedRTM: boolean; + isInitialQueueCompleted: boolean; + onlineUsersCount: number; + rtmInitTimstamp: number; + syncUserState: (uid: number, data: Partial) => void; +} + +const RTMMainRoomContext = createContext({ + hasUserJoinedRTM: false, + isInitialQueueCompleted: false, + onlineUsersCount: 0, + rtmInitTimstamp: 0, + syncUserState: () => {}, +}); + +export const useRTMConfigureMain = () => { + const context = useContext(RTMMainRoomContext); + if (!context) { + throw new Error( + 'useRTMConfigureMain must be used within RTMConfigureMainRoomProvider', + ); + } + return context; +}; + +interface RTMConfigureMainRoomProviderProps { + callActive: boolean; + channelName: string; + children: React.ReactNode; +} + +const RTMConfigureMainRoomProvider: React.FC< + RTMConfigureMainRoomProviderProps +> = ({callActive, channelName, children}) => { + const rtmInitTimstamp = new Date().getTime(); + const {dispatch} = useContext(DispatchContext); + const {defaultContent, activeUids} = useContent(); + const { + waitingRoomStatus, + data: {isHost}, + } = useRoomInfo(); + const localUid = useLocalUid(); + const {applyUserPreferences, syncUserPreferences} = + useUserGlobalPreferences(); + const toggleMute = useMuteToggleLocal(); + const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); + const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); + const [onlineUsersCount, setTotalOnlineUsers] = useState(0); + + // RTM + const {client, isLoggedIn} = useRTMCore(); + const {mainRoomRTMUsers, setMainRoomRTMUsers} = useRTMGlobalState(); + + // Main channel message registration (RTMConfigureMainRoom is always for main channel) + const { + registerMainChannelMessageHandler, + unregisterMainChannelMessageHandler, + registerMainChannelStorageHandler, + unregisterMainChannelStorageHandler, + } = useRTMGlobalState(); + + /** + * inside event callback state won't have latest value. + * so creating ref to access the state + */ + const isHostRef = useRef({isHost: isHost}); + useEffect(() => { + isHostRef.current.isHost = isHost; + }, [isHost]); + + const waitingRoomStatusRef = useRef({waitingRoomStatus: waitingRoomStatus}); + useEffect(() => { + waitingRoomStatusRef.current.waitingRoomStatus = waitingRoomStatus; + }, [waitingRoomStatus]); + + const activeUidsRef = useRef({activeUids: activeUids}); + useEffect(() => { + activeUidsRef.current.activeUids = activeUids; + }, [activeUids]); + + const defaultContentRef = useRef(defaultContent); + + useEffect(() => { + defaultContentRef.current = defaultContent; + }, [defaultContent]); + + const {rtcTracksReady} = useRtc(); + + // Set main room as active channel when this provider mounts again active + useEffect(() => { + const rtmEngine = RTMEngine.getInstance(); + if (rtmEngine.hasChannel(RTM_ROOMS.MAIN)) { + rtmEngine.setActiveChannel(RTM_ROOMS.MAIN); + } + }, []); + + // Apply user preferences when main room mounts + useEffect(() => { + if (rtcTracksReady) { + console.log( + 'UP: trackesready', + JSON.stringify(defaultContentRef.current[localUid]), + ); + applyUserPreferences(defaultContentRef.current[localUid], toggleMute); + } + }, [rtcTracksReady]); + + // Sync current audio/video state audio video changes + useEffect(() => { + const userData = defaultContent[localUid]; + if (rtcTracksReady && userData) { + console.log('UP: syncing userData: ', userData); + const preferences = { + audioMuted: userData.audio === ToggleState.disabled, + videoMuted: userData.video === ToggleState.disabled, + }; + console.log('UP: saved preferences: ', preferences); + syncUserPreferences(preferences); + } + }, [defaultContent, localUid, syncUserPreferences, rtcTracksReady]); + + // Eventdispatcher timeout refs clean + const isRTMMounted = useRef(true); + useEffect(() => { + return () => { + isRTMMounted.current = false; + // Clear all pending timeouts on unmount + for (const timeout of eventTimeouts.values()) { + clearTimeout(timeout); + } + eventTimeouts.clear(); + }; + }, []); + + // Main room specific syncUserState function + const syncUserState = useCallback( + (uid: number, data: any) => { + // Extract only RTM-related fields that are actually passed + const rtmData: Partial = {}; + + // Only add fields if they exist in the passed data + if ('name' in data) { + rtmData.name = data.name; + } + if ('screenUid' in data) { + rtmData.screenUid = data.screenUid; + } + if ('offline' in data) { + rtmData.offline = data.offline; + } + if ('lastMessageTimeStamp' in data) { + rtmData.lastMessageTimeStamp = data.lastMessageTimeStamp; + } + if ('isInWaitingRoom' in data) { + rtmData.isInWaitingRoom = data.isInWaitingRoom; + } + if ('isHost' in data) { + rtmData.isHost = data.isHost; + } + if ('type' in data) { + rtmData.type = data.type; + } + if ('parentUid' in data) { + rtmData.parentUid = data.parentUid; + } + if ('uid' in data) { + rtmData.uid = data.uid; + } + // Only update if we have RTM data to update + if (Object.keys(rtmData).length > 0) { + setMainRoomRTMUsers(prev => { + return { + ...prev, + [uid]: { + ...prev[uid], + ...rtmData, + }, + }; + }); + } + }, + [setMainRoomRTMUsers], + ); + + // Set online users + React.useEffect(() => { + setTotalOnlineUsers( + Object.keys( + filterObject( + defaultContent, + ([k, v]) => + v?.type === 'rtc' && + !v.offline && + activeUidsRef.current.activeUids.indexOf(v?.uid) !== -1, + ), + ).length, + ); + }, [defaultContent]); + + useEffect(() => { + Object.entries(mainRoomRTMUsers).forEach(([uidStr, rtmUser]) => { + const uid = parseInt(uidStr, 10); + + // Create only RTM data + const userData: RTMUserData = { + // RTM data + name: rtmUser.name || '', + screenUid: rtmUser.screenUid || 0, + offline: !!rtmUser.offline, + lastMessageTimeStamp: rtmUser.lastMessageTimeStamp || 0, + isInWaitingRoom: rtmUser?.isInWaitingRoom || false, + isHost: rtmUser.isHost, + type: rtmUser.type, + }; + + // Dispatch directly for each user + dispatch({type: 'UpdateRenderList', value: [uid, userData]}); + }); + }, [mainRoomRTMUsers, dispatch]); + + const init = async () => { + setHasUserJoinedRTM(true); + await runQueuedEvents(); + setIsInitialQueueCompleted(true); + }; + + const runQueuedEvents = async () => { + try { + while (!EventsQueue.isEmpty()) { + const currEvt = EventsQueue.dequeue(); + await eventDispatcher(currEvt.data, `${currEvt.uid}`, currEvt.ts); + } + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while running queue events', + {error}, + ); + } + }; + + const eventDispatcher = async ( + data: { + evt: string; + value: string; + feat?: string; + etyp?: string; + }, + sender: string, + ts: number, + ) => { + console.log( + LogSource.Events, + 'CUSTOM_EVENTS', + 'inside eventDispatcher ', + data, + ); + + let evt = '', + value = ''; + + if (data?.feat === 'BREAKOUT_ROOM') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + data: data.data, + action: data.act, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } else if (data?.feat === 'WAITING_ROOM') { + if (data?.etyp === 'REQUEST') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + attendee_uid: data.data.data.attendee_uid, + attendee_screenshare_uid: data.data.data.attendee_screenshare_uid, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } + if (data?.etyp === 'RESPONSE') { + const outputData = { + evt: `${data.feat}_${data.etyp}`, + payload: JSON.stringify({ + approved: data.data.data.approved, + channelName: data.data.data.channel_name, + mainUser: data.data.data.mainUser, + screenShare: data.data.data.screenShare, + whiteboard: data.data.data.whiteboard, + chat: data.data.data?.chat, + }), + persistLevel: 1, + source: 'core', + }; + const formattedData = JSON.stringify(outputData); + evt = data.feat + '_' + data.etyp; + value = formattedData; + } + } else { + if ( + $config.ENABLE_WAITING_ROOM && + !isHostRef.current?.isHost && + waitingRoomStatusRef.current?.waitingRoomStatus !== + WaitingRoomStatus.APPROVED + ) { + if ( + data.evt === controlMessageEnum.muteAudio || + data.evt === controlMessageEnum.muteVideo + ) { + return; + } else { + evt = data.evt; + value = data.value; + } + } else { + evt = data.evt; + value = data.value; + } + } + + try { + let parsedValue; + try { + parsedValue = typeof value === 'string' ? JSON.parse(value) : value; + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'RTM Failed to parse event value in event dispatcher:', + {error}, + ); + return; + } + const {payload, persistLevel, source} = parsedValue; + + // Step 2: Emit the event (no metadata persistence - handled by RTMGlobalStateProvider) + console.log(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: ', evt); + EventUtils.emitEvent(evt, source, {payload, persistLevel, sender, ts}); + // Because async gets evaluated in a different order when in an sdk + if (evt === 'name') { + // 1. Cancel existing timeout for this sender + if (eventTimeouts.has(sender)) { + clearTimeout(eventTimeouts.get(sender)!); + } + // 2. Create new timeout with tracking + const timeout = setTimeout(() => { + // 3. Guard against unmounted component + if (!isRTMMounted.current) { + return; + } + EventUtils.emitEvent(evt, source, { + payload, + persistLevel, + sender, + ts, + }); + // 4. Clean up after execution + eventTimeouts.delete(sender); + }, 200); + // 5. Track the timeout for cleanup + eventTimeouts.set(sender, timeout); + } + } catch (error) { + console.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while emiting event:', + {error}, + ); + } + }; + + // Register listeners when client is created + useEffect(() => { + if (!client) { + return; + } + + const handleMainChannelStorageEvent = (storage: StorageEvent) => { + // when remote user sets/updates metadata - 3 + if ( + storage.eventType === nativeStorageEventTypeMapping.SET || + storage.eventType === nativeStorageEventTypeMapping.UPDATE + ) { + const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; + const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; + logger.log( + LogSource.AgoraSDK, + 'Event', + `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, + storage, + ); + try { + if (storage.data?.items && Array.isArray(storage.data.items)) { + storage.data.items.forEach(item => { + try { + if (!item || !item.key) { + logger.warn( + LogSource.Events, + 'CUSTOM_EVENTS', + 'Invalid storage item:', + item, + ); + return; + } + + const {key, value, authorUserId, updateTs} = item; + const timestamp = getMessageTime(updateTs); + const sender = Platform.OS + ? get32BitUid(authorUserId) + : parseInt(authorUserId, 10); + eventDispatcher( + { + evt: key, + value, + }, + `${sender}`, + timestamp, + ); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + `Failed to process storage item: ${JSON.stringify(item)}`, + {error}, + ); + } + }); + } + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + }; + + const handleMainChannelMessageEvent = (message: MessageEvent) => { + console.log('supriya current message channel: ', channelName); + console.log('supriya message event is', message); + // message - 1 (channel) + if (message.channelType === nativeChannelTypeMapping.MESSAGE) { + // here the channel name will be the channel name + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', + message, + ); + const { + publisher: uid, + channelName, + message: text, + timestamp: ts, + } = message; + //whiteboard upload + if (parseInt(uid, 10) === 1010101) { + const [err, res] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + if (res?.data?.data?.images) { + LocalEventEmitter.emit( + LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, + res?.data?.data?.images, + ); + } + } else { + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + } + + // message - 3 (user) + if (message.channelType === nativeChannelTypeMapping.USER) { + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [3- USER] (messageReceived)', + message, + ); + // here the (message.channelname) channel name will be the to UID + const {publisher: peerId, timestamp: ts, message: text} = message; + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + + const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); + + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + }; + + // Register with RTMGlobalStateProvider for main channel message handling + registerMainChannelMessageHandler(handleMainChannelMessageEvent); + registerMainChannelStorageHandler(handleMainChannelStorageEvent); + console.log( + 'RTMConfigureMainRoom: Registered main channel message handler', + ); + + return () => { + unregisterMainChannelMessageHandler(); + unregisterMainChannelStorageHandler(); + console.log( + 'RTMConfigureMainRoom: Unregistered main channel message handler', + ); + }; + }, [client, channelName]); + + useAsyncEffect(async () => { + try { + if (isLoggedIn && callActive) { + await init(); + } + } catch (error) { + logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); + } + return async () => { + logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); + if (isIOS() || isAndroid()) { + EventUtils.clear(); + } + setHasUserJoinedRTM(false); + setIsInitialQueueCompleted(false); + logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); + }; + }, [isLoggedIn, callActive, channelName]); + + // Provide context data to children + const contextValue: RTMMainRoomData = { + hasUserJoinedRTM, + isInitialQueueCompleted, + onlineUsersCount, + rtmInitTimstamp, + syncUserState, + }; + + return ( + + {children} + + ); +}; + +export default RTMConfigureMainRoomProvider; diff --git a/template/src/rtm/RTMCoreProvider.tsx b/template/src/rtm/RTMCoreProvider.tsx index 1f3d7385b..3fa3ccbe9 100644 --- a/template/src/rtm/RTMCoreProvider.tsx +++ b/template/src/rtm/RTMCoreProvider.tsx @@ -18,7 +18,6 @@ import type { } from 'agora-react-native-rtm'; import {UidType} from '../../agora-rn-uikit'; import RTMEngine from '../rtm/RTMEngine'; -import {nativePresenceEventTypeMapping} from '../../bridge/rtm/web/Types'; import {isWeb, isWebInternal} from '../utils/common'; import isSDK from '../utils/isSDK'; @@ -66,7 +65,7 @@ export const RTMCoreProvider: React.FC = ({ userInfo, children, }) => { - const [client, setClient] = useState(null); // Use state instead + const [client, setClient] = useState(null); const [isLoggedIn, setIsLoggedIn] = useState(false); const [connectionState, setConnectionState] = useState(0); console.log('supriya-rtm connectionState: ', connectionState); @@ -161,7 +160,7 @@ export const RTMCoreProvider: React.FC = ({ } const handleGlobalStorageEvent = (storage: StorageEvent) => { console.log( - 'supriya-rtm-global ********************** ---StorageEvent event: ', + 'rudra-core-client ********************** ---StorageEvent event: ', storage, ); // Distribute to all registered callbacks @@ -178,7 +177,7 @@ export const RTMCoreProvider: React.FC = ({ const handleGlobalPresenceEvent = (presence: PresenceEvent) => { console.log( - 'supriya-rtm-global @@@@@@@@@@@@@@@@@@@@@@@ ---PresenceEvent: ', + 'rudra-core-client @@@@@@@@@@@@@@@@@@@@@@@ ---PresenceEvent: ', presence, ); // Distribute to all registered callbacks @@ -195,7 +194,7 @@ export const RTMCoreProvider: React.FC = ({ const handleGlobalMessageEvent = (message: MessageEvent) => { console.log( - 'supriya-rtm-global ######################## ---MessageEvent event: ', + 'rudra-core-client ######################## ---MessageEvent event: ', message, ); // Distribute to all registered callbacks @@ -236,9 +235,9 @@ export const RTMCoreProvider: React.FC = ({ if (!rtmClient) { throw new Error('Failed to create RTM client'); } - setClient(rtmClient); // Set client after successful setup - - // 3. Global linkState listener + // 3. Set client after successful setup + setClient(rtmClient); + // 4 .Global linkState listener const onLink = async (evt: LinkStateEvent) => { setConnectionState(evt.currentState); if (evt.currentState === 0 /* DISCONNECTED */) { @@ -258,10 +257,9 @@ export const RTMCoreProvider: React.FC = ({ rtmClient.addEventListener('linkState', onLink); try { - // 4. Client Login + // 5. Client Login if (stableUserInfo.rtmToken) { await loginToRTM(rtmClient, stableUserInfo.rtmToken); - // 5. Set user attributes after successful login await setAttribute(rtmClient, stableUserInfo); setIsLoggedIn(true); } diff --git a/template/src/rtm/RTMEngine.ts b/template/src/rtm/RTMEngine.ts index 202dd38b4..f38395c1b 100644 --- a/template/src/rtm/RTMEngine.ts +++ b/template/src/rtm/RTMEngine.ts @@ -16,13 +16,15 @@ import { type RTMClient, } from 'agora-react-native-rtm'; import {isAndroid, isIOS} from '../utils/common'; +import {RTM_ROOMS} from './constants'; class RTMEngine { private _engine?: RTMClient; private localUID: string = ''; - private primaryChannelId: string = ''; - // track multiple subscribed channels - private channels: Set = new Set(); + // track multiple named channels (e.g., "main": "channelId", "breakout": "channelId") + private channelMap: Map = new Map(); + // track current active channel for default operations + private activeChannelName: string = RTM_ROOMS.MAIN; private static _instance: RTMEngine | null = null; private constructor() { @@ -62,51 +64,79 @@ class RTMEngine { } } - addChannel(channelID: string, primary?: boolean) { + addChannel(name: string, channelID: string) { + if (!name || typeof name !== 'string' || name.trim() === '') { + throw new Error('addChannel: name must be a non-empty string'); + } if ( !channelID || typeof channelID !== 'string' || channelID.trim() === '' ) { - throw new Error( - 'addSecondaryChannel: channelID must be a non-empty string', - ); - } - this.channels.add(channelID); - if (primary) { - this.primaryChannelId = channelID; + throw new Error('addChannel: channelID must be a non-empty string'); } + this.channelMap.set(name, channelID); + this.setActiveChannel(name); } - removeChannel(channelID: string) { - if (this.channels.has(channelID)) { - this.channels.delete(channelID); - if (channelID === this.primaryChannelId) { - this.primaryChannelId = ''; - } - } + removeChannel(name: string) { + this.channelMap.delete(name); } get localUid() { return this.localUID; } - get channelUid() { - return this.primaryChannelId; + getChannel(name?: string): string { + // Default to active channel if no name provided + const channelName = name || this.activeChannelName; + console.log('supriya channelName: ', this.channelMap.get(channelName)); + return this.channelMap.get(channelName) || ''; + } + + get allChannels(): string[] { + return Array.from(this.channelMap.values()).filter( + channel => channel.trim() !== '', + ); + } + + hasChannel(name: string): boolean { + return this.channelMap.has(name); + } + + getChannelNames(): string[] { + return Array.from(this.channelMap.keys()); + } + + /** Set the active channel for default operations */ + setActiveChannel(name: string): void { + if (!name || typeof name !== 'string' || name.trim() === '') { + throw new Error('setActiveChannel: name must be a non-empty string'); + } + if (!this.hasChannel(name)) { + throw new Error( + `setActiveChannel: Channel '${name}' not found. Add it first with addChannel().`, + ); + } + this.activeChannelName = name; + console.log( + `RTMEngine: Active channel set to '${name}' (${this.getChannel(name)})`, + ); } - get primaryChannel() { - return this.primaryChannelId; + /** Get the current active channel ID */ + getActiveChannel(): string { + return this.getChannel(this.activeChannelName); } - get allChannels() { - const channels = []; - this.channels.forEach(channel => channels.push(channel)); - return channels.filter(channel => channel.trim() !== ''); + /** Get the current active channel name */ + getActiveChannelName(): string { + return this.activeChannelName; } - hasChannel(channelID: string): boolean { - return this.channels.has(channelID); + /** Check if the specified channel is currently active */ + isActiveChannel(name: string): boolean { + return this.activeChannelName === name; } /** Engine readiness flag */ @@ -190,10 +220,10 @@ class RTMEngine { return; } await this.destroyClientInstance(); - this.primaryChannelId = ''; - this.channels.clear(); + this.channelMap.clear(); // Reset state this.localUID = ''; + this.activeChannelName = RTM_ROOMS.MAIN; this._engine = undefined; RTMEngine._instance = null; } catch (error) { diff --git a/template/src/rtm/RTMGlobalStateProvider.tsx b/template/src/rtm/RTMGlobalStateProvider.tsx new file mode 100644 index 000000000..f03dc4073 --- /dev/null +++ b/template/src/rtm/RTMGlobalStateProvider.tsx @@ -0,0 +1,759 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useState, useEffect, useRef} from 'react'; +import { + type GetChannelMetadataResponse, + type GetOnlineUsersResponse, + type GetUserMetadataResponse, + type PresenceEvent, + type StorageEvent, + type SetOrUpdateUserMetadataOptions, + type MessageEvent, +} from 'agora-react-native-rtm'; +import {backOff} from 'exponential-backoff'; +import {timeNow, hasJsonStructure} from '../rtm/utils'; +import {EventsQueue} from '../rtm-events'; +import {PersistanceLevel} from '../rtm-events-api'; +import RTMEngine from '../rtm/RTMEngine'; +import {LogSource, logger} from '../logger/AppBuilderLogger'; +import {RECORDING_BOT_UID} from '../utils/constants'; +import {useRTMCore} from './RTMCoreProvider'; +import { + ContentInterface, + RtcPropsInterface, + UidType, + useLocalUid, +} from '../../agora-rn-uikit'; +import { + nativePresenceEventTypeMapping, + nativeStorageEventTypeMapping, +} from '../../bridge/rtm/web/Types'; +import {RTM_ROOMS} from './constants'; + +export enum UserType { + ScreenShare = 'screenshare', +} + +// RTM-specific user data interface +export interface RTMUserData { + uid?: UidType; + screenUid?: number; // Screen sharing UID reference (stored in RTM user metadata) + parentUid?: UidType; // Only available for screenshare + type: 'rtc' | 'screenshare' | 'bot'; + name?: string; // User's display name (stored in RTM user metadata) + offline: boolean; // User online/offline status (managed through RTM presence events) + lastMessageTimeStamp: number; // Timestamp of last message (RTM message tracking) + isInWaitingRoom?: boolean; // Waiting room status (RTM-based feature state) + isHost: string; // Host privileges (stored in RTM user metadata as 'isHost') +} + +const eventTimeouts = new Map>(); + +interface RTMGlobalStateProviderProps { + children: React.ReactNode; + mainChannelRtcProps: Partial; +} + +// Context for message and storage handler registration +const RTMGlobalStateContext = React.createContext<{ + mainRoomRTMUsers: {[uid: number]: RTMUserData}; + setMainRoomRTMUsers: React.Dispatch< + React.SetStateAction<{[uid: number]: RTMUserData}> + >; + registerMainChannelMessageHandler: ( + handler: (message: MessageEvent) => void, + ) => void; + unregisterMainChannelMessageHandler: () => void; + registerMainChannelStorageHandler: ( + handler: (storage: StorageEvent) => void, + ) => void; + unregisterMainChannelStorageHandler: () => void; +}>({ + mainRoomRTMUsers: {}, + setMainRoomRTMUsers: () => {}, + registerMainChannelMessageHandler: () => {}, + unregisterMainChannelMessageHandler: () => {}, + registerMainChannelStorageHandler: () => {}, + unregisterMainChannelStorageHandler: () => {}, +}); + +const RTMGlobalStateProvider: React.FC = ({ + children, + mainChannelRtcProps, +}) => { + const mainChannelName = mainChannelRtcProps.channel; + const localUid = useLocalUid(); + const {client, isLoggedIn, registerCallbacks, unregisterCallbacks} = + useRTMCore(); + // Main room RTM users (RTM-specific data only) + const [mainRoomRTMUsers, setMainRoomRTMUsers] = useState<{ + [uid: number]: RTMUserData; + }>({}); + + const hasInitRef = useRef(false); + // Timeout Refs + const subscribeTimerRef: any = useRef(5); + const channelAttributesTimerRef: any = useRef(5); + const membersTimerRef: any = useRef(5); + const subscribeTimeoutRef = useRef | null>( + null, + ); + const channelAttributesTimeoutRef = useRef | null>(null); + const membersTimeoutRef = useRef | null>(null); + + const isRTMMounted = useRef(true); + + // Message handler registration for main channel + const messageHandlerRef = useRef<((message: MessageEvent) => void) | null>( + null, + ); + const registerMainChannelMessageHandler = ( + handler: (message: MessageEvent) => void, + ) => { + console.log( + 'rudra-core-client: RTM registering main channel message handler', + ); + if (messageHandlerRef.current) { + console.warn( + 'RTMGlobalStateProvider: Overwriting an existing main channel message handler!', + ); + } + messageHandlerRef.current = handler; + }; + + const unregisterMainChannelMessageHandler = () => { + console.log( + 'rudra-core-client: RTM unregistering main channel message handler', + ); + messageHandlerRef.current = null; + }; + + // Storage handler registration for main channel + const storageHandlerRef = useRef<((storage: StorageEvent) => void) | null>( + null, + ); + + const registerMainChannelStorageHandler = ( + handler: (storage: StorageEvent) => void, + ) => { + console.log( + 'rudra-core-client: RTM registering main channel storage handler', + ); + if (storageHandlerRef.current) { + console.warn( + 'RTMGlobalStateProvider: Overwriting an existing main channel storage handler!', + ); + } + storageHandlerRef.current = handler; + }; + + const unregisterMainChannelStorageHandler = () => { + console.log( + 'rudra-core-client: RTM unregistering main channel storage handler', + ); + storageHandlerRef.current = null; + }; + + useEffect(() => { + return () => { + isRTMMounted.current = false; + // Clear all pending timeouts on unmount + for (const timeout of eventTimeouts.values()) { + clearTimeout(timeout); + } + eventTimeouts.clear(); + + // Clear timer-based retry timeouts + if (subscribeTimeoutRef.current) { + clearTimeout(subscribeTimeoutRef.current); + subscribeTimeoutRef.current = null; + } + if (channelAttributesTimeoutRef.current) { + clearTimeout(channelAttributesTimeoutRef.current); + channelAttributesTimeoutRef.current = null; + } + if (membersTimeoutRef.current) { + clearTimeout(membersTimeoutRef.current); + membersTimeoutRef.current = null; + } + }; + }, []); + + const init = async () => { + try { + console.log('rudra-core-client: Starting RTM init for main channel'); + await subscribeChannel(); + await getMembersWithAttributes(); + await getChannelAttributes(); + console.log('rudra-core-client: RTM init completed successfully'); + } catch (error) { + console.log('rudra-core-client: RTM init failed', error); + } + }; + + const subscribeChannel = async () => { + try { + if (RTMEngine.getInstance().allChannels.includes(mainChannelName)) { + console.log('rudra- main channel already subsribed'); + } else { + console.log('rudra- subscribing...'); + await client.subscribe(mainChannelName, { + withMessage: true, + withPresence: true, + withMetadata: true, + withLock: false, + }); + console.log('rudra- subscribed main channel', mainChannelName); + + RTMEngine.getInstance().addChannel(RTM_ROOMS.MAIN, mainChannelName); + subscribeTimerRef.current = 5; + // Clear any pending retry timeout since we succeeded + if (subscribeTimeoutRef.current) { + clearTimeout(subscribeTimeoutRef.current); + subscribeTimeoutRef.current = null; + } + console.log('rudra- added to rtm engine'); + } + } catch (error) { + console.log( + 'rudra-core-client: RTM subscribeChannel failed..Trying again', + error, + ); + subscribeTimeoutRef.current = setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + subscribeTimerRef.current = Math.min(subscribeTimerRef.current * 2, 30); + subscribeChannel(); + }, subscribeTimerRef.current * 1000); + } + }; + + const getChannelAttributes = async () => { + try { + await client.storage + .getChannelMetadata(mainChannelName, 1) + .then(async (data: GetChannelMetadataResponse) => { + for (const item of data.items) { + try { + const {key, value, authorUserId, updateTs} = item; + if (hasJsonStructure(value as string)) { + const evtData = { + evt: key, + value, + }; + // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events + EventsQueue.enqueue({ + data: evtData, + uid: authorUserId, + ts: updateTs, + }); + } + } catch (error) { + console.log( + 'rudra-core-client: RTM Failed to process channel attribute item', + item, + error, + ); + // Continue processing other items + } + } + console.log( + 'rudra-core-client: RTM storage.getChannelMetadata data received', + data, + ); + }); + channelAttributesTimerRef.current = 5; + // Clear any pending retry timeout since we succeeded + if (channelAttributesTimeoutRef.current) { + clearTimeout(channelAttributesTimeoutRef.current); + channelAttributesTimeoutRef.current = null; + } + } catch (error) { + channelAttributesTimeoutRef.current = setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + channelAttributesTimerRef.current = Math.min( + channelAttributesTimerRef.current * 2, + 30, + ); + await getChannelAttributes(); + }, channelAttributesTimerRef.current * 1000); + } + }; + + const getMembersWithAttributes = async () => { + try { + console.log( + 'rudra-core-client: RTM presence.getOnlineUsers(getMembers) start', + ); + await client.presence + .getOnlineUsers(mainChannelName, 1) + .then(async (data: GetOnlineUsersResponse) => { + console.log( + 'rudra-core-client: RTM presence.getOnlineUsers data received', + data, + ); + await Promise.all( + data.occupants?.map(async member => { + try { + const backoffAttributes = + await fetchUserAttributesWithBackoffRetry(member.userId); + + await processUserUidAttributes( + backoffAttributes, + member.userId, + ); + // setting screenshare data + // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) + // isActive to identify all active screenshare users in the call + backoffAttributes?.items?.forEach(item => { + try { + if (hasJsonStructure(item.value as string)) { + const data = { + evt: item.key, // Use item.key instead of key + value: item.value, // Use item.value instead of value + }; + // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events + EventsQueue.enqueue({ + data: data, + uid: member.userId, + ts: timeNow(), + }); + } + } catch (error) { + console.log( + 'rudra-core-client: RTM Failed to process user attribute item for', + member.userId, + item, + error, + ); + // Continue processing other items + } + }); + } catch (e) { + console.log( + 'rudra-core-client: RTM Could not retrieve name of', + member.userId, + e, + ); + } + }), + ); + membersTimerRef.current = 5; + // Clear any pending retry timeout since we succeeded + if (membersTimeoutRef.current) { + clearTimeout(membersTimeoutRef.current); + membersTimeoutRef.current = null; + } + console.log( + 'rudra-core-client: RTM fetched all data and user attr...RTM init done', + ); + }); + } catch (error) { + membersTimeoutRef.current = setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + membersTimerRef.current = Math.min(membersTimerRef.current * 2, 30); + await getMembersWithAttributes(); + }, membersTimerRef.current * 1000); + } + }; + + const fetchUserAttributesWithBackoffRetry = async ( + userId: string, + ): Promise => { + return backOff( + async () => { + console.log( + 'rudra-core-client: RTM fetching getUserMetadata for member', + userId, + ); + + const attr: GetUserMetadataResponse = + await client.storage.getUserMetadata({ + userId: userId, + }); + + if (!attr || !attr.items) { + console.log('rudra-core-client: RTM attributes for member not found'); + throw attr; + } + + console.log( + 'rudra-core-client: RTM getUserMetadata for member received', + userId, + attr, + ); + + if (attr.items && attr.items.length > 0) { + return attr; + } else { + throw attr; + } + }, + { + retry: (e, idx) => { + console.log( + 'rudra-core-client: RTM [retrying] Attempt', + idx, + 'Fetching', + userId, + 'attributes', + e, + ); + return true; + }, + }, + ); + }; + + const processUserUidAttributes = async ( + attr: GetUserMetadataResponse, + userId: string, + ) => { + try { + console.log('rudra-core-client: [user attributes]:', attr); + const uid = parseInt(userId, 10); + const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); + const isHostItem = attr?.items?.find(item => item.key === 'isHost'); + const nameItem = attr?.items?.find(item => item.key === 'name'); + const screenUid = screenUidItem?.value + ? parseInt(screenUidItem.value, 10) + : undefined; + + let userName = ''; + if (nameItem?.value) { + try { + const parsedValue = JSON.parse(nameItem.value); + const payloadString = parsedValue.payload; + if (payloadString) { + const payload = JSON.parse(payloadString); + userName = payload.name; + } + } catch (parseError) {} + } + + //start - updating RTM user data + const rtmUserData: RTMUserData = { + uid, + type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', + screenUid: screenUid, + name: userName, + offline: false, + isHost: isHostItem?.value || 'false', + lastMessageTimeStamp: 0, + }; + console.log('rudra-core-client: new RTM user joined', uid, rtmUserData); + setMainRoomRTMUsers(prev => ({ + ...prev, + [uid]: {...(prev[uid] || {}), ...rtmUserData}, + })); + //end- updating RTM user data + + //start - updating screenshare RTM data + if (screenUid) { + // @ts-ignore + const screenShareRTMData: RTMUserData = { + type: 'screenshare', + parentUid: uid, + // Note: screenUid itself doesn't need screenUid field, parentUid will be handled in RTC layer + }; + setMainRoomRTMUsers(prev => ({ + ...prev, + [screenUid]: {...(prev[screenUid] || {}), ...screenShareRTMData}, + })); + } + //end - updating screenshare RTM data + } catch (e) { + console.log( + 'rudra-core-client: RTM Failed to process user data for', + userId, + e, + ); + } + }; + + const handleMainChannelPresenceEvent = async (presence: PresenceEvent) => { + console.log( + 'rudra-core-client: RTM presence event received for different channel', + presence.channelName, + 'expected:', + mainChannelName, + ); + if (presence.channelName !== mainChannelName) { + console.log( + 'rudra-core-client: RTM presence event received for different channel', + presence.channelName, + 'expected:', + mainChannelName, + ); + return; + } + + // remoteJoinChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { + console.log( + 'rudra-core-client: RTM main room user joined', + presence.publisher, + ); + try { + const backoffAttributes = await fetchUserAttributesWithBackoffRetry( + presence.publisher, + ); + await processUserUidAttributes(backoffAttributes, presence.publisher); + } catch (error) { + console.log( + 'rudra-core-client: RTM Failed to process user who joined main room', + presence.publisher, + error, + ); + } + } + + // remoteLeaveChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { + console.log( + 'rudra-core-client: RTM main room user left', + presence.publisher, + ); + const uid = presence?.publisher + ? parseInt(presence.publisher, 10) + : undefined; + + if (!uid) { + return; + } + + // Mark user as offline (matching legacy channelMemberLeft behavior) + setMainRoomRTMUsers(prev => { + const updated = {...prev}; + + if (updated[uid]) { + updated[uid] = { + ...updated[uid], + offline: true, + }; + } + + // Also mark screenshare as offline if exists + const screenUid = prev[uid]?.screenUid; + // if (screenUid && updated[screenUid]) { + // updated[screenUid] = { + // ...updated[screenUid], + // offline: true, + // }; + // } + + console.log( + 'rudra-core-client: RTM marked user as offline in main room', + uid, + screenUid ? `and screenshare ${screenUid}` : '', + ); + return updated; + }); + } + }; + + const handleMainChannelStorageEvent = async (storage: StorageEvent) => { + console.log( + 'rudra-core-client: RTM global storage event received', + storage, + ); + + // Only handle SET/UPDATE events for metadata persistence + if ( + storage.eventType === nativeStorageEventTypeMapping.SET || + storage.eventType === nativeStorageEventTypeMapping.UPDATE + ) { + const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; + const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; + console.log( + `rudra-core-client: RTM processing ${eventTypeStr} ${storageTypeStr} metadata`, + ); + + // STEP 1: Handle metadata persistence FIRST (core RTM functionality) + try { + if (storage.data?.items && Array.isArray(storage.data.items)) { + for (const item of storage.data.items) { + try { + if (!item || !item.key) { + console.log( + 'rudra-core-client: RTM invalid storage item:', + item, + ); + continue; + } + + const {key, value, authorUserId, updateTs} = item; + + // Parse the value to check persistLevel + let parsedValue; + try { + parsedValue = + typeof value === 'string' ? JSON.parse(value) : value; + } catch (parseError) { + console.log( + 'rudra-core-client: RTM failed to parse storage event value:', + parseError, + ); + continue; + } + + const {persistLevel} = parsedValue; + + // Handle metadata persistence for Session level events + if (persistLevel === PersistanceLevel.Session) { + console.log( + 'rudra-core-client: RTM setting user metadata for key:', + key, + ); + + const rtmAttribute = {key: key, value: value}; + const options: SetOrUpdateUserMetadataOptions = { + userId: `${localUid}`, + }; + + try { + await client.storage.setUserMetadata( + {items: [rtmAttribute]}, + options, + ); + console.log( + 'rudra-core-client: RTM successfully set user metadata for key:', + key, + ); + } catch (setMetadataError) { + console.log( + 'rudra-core-client: RTM failed to set user metadata:', + setMetadataError, + ); + } + } + } catch (itemError) { + console.log( + 'rudra-core-client: RTM failed to process storage item:', + item, + itemError, + ); + } + } + } + } catch (error) { + console.log( + 'rudra-core-client: RTM error processing storage event:', + error, + ); + } + + // STEP 2: Forward to application logic AFTER metadata persistence + if (storageHandlerRef.current) { + try { + storageHandlerRef.current(storage); + console.log( + 'rudra-core-client: RTM forwarded storage event to registered handler', + ); + } catch (error) { + console.log( + 'rudra-core-client: RTM error forwarding storage event:', + error, + ); + } + } + } + }; + + const handleMainChannelMessageEvent = (message: MessageEvent) => { + console.log( + 'rudra-core-client: RTM main channel message event received', + message, + ); + + // Forward to registered message handler (RTMConfigure) + if (messageHandlerRef.current) { + try { + messageHandlerRef.current(message); + console.log( + 'rudra-core-client: RTM forwarded message event to registered handler', + ); + } catch (error) { + console.log( + 'rudra-core-client: RTM error forwarding message event:', + error, + ); + } + } else { + console.log( + 'rudra-core-client: RTM no message handler registered for main channel', + ); + } + }; + + // Main initialization effect + useEffect(() => { + if (!client || !isLoggedIn || !mainChannelName || hasInitRef.current) { + return; + } + hasInitRef.current = true; + console.log('RTMGlobalStateProvider: Client ready, starting init'); + // Register presence, storage, and message event callbacks for main channel + registerCallbacks(mainChannelName, { + presence: handleMainChannelPresenceEvent, + storage: handleMainChannelStorageEvent, + message: handleMainChannelMessageEvent, + }); + console.log( + 'rudra-core-client: RTM registered presence, storage, and message callbacks for main channel', + ); + init(); + return () => { + console.log('rudra-clean up for global state - call unsubscribe'); + hasInitRef.current = false; + if (mainChannelName) { + unregisterCallbacks(mainChannelName); + if (RTMEngine.getInstance().hasChannel(mainChannelName)) { + client?.unsubscribe(mainChannelName).catch(() => {}); + RTMEngine.getInstance().removeChannel(mainChannelName); + } + console.log( + 'rudra-core-client: RTM unregistered callbacks for main channel', + ); + } + }; + }, [client, isLoggedIn, mainChannelName]); + + return ( + + {children} + + ); +}; + +// Hook to use main channel message registration +export const useRTMGlobalState = () => { + const context = React.useContext(RTMGlobalStateContext); + if (!context) { + throw new Error( + 'useRTMMainChannel must be used within RTMGlobalStateProvider', + ); + } + return context; +}; + +export default RTMGlobalStateProvider; diff --git a/template/src/rtm/constants.ts b/template/src/rtm/constants.ts new file mode 100644 index 000000000..bfa274e18 --- /dev/null +++ b/template/src/rtm/constants.ts @@ -0,0 +1,4 @@ +export enum RTM_ROOMS { + BREAKOUT = 'BREAKOUT', + MAIN = 'MAIN', +} diff --git a/template/src/rtm/hooks/useMainRoomUserDisplayName.ts b/template/src/rtm/hooks/useMainRoomUserDisplayName.ts new file mode 100644 index 000000000..56eb54e86 --- /dev/null +++ b/template/src/rtm/hooks/useMainRoomUserDisplayName.ts @@ -0,0 +1,35 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ +import {videoRoomUserFallbackText} from '../../language/default-labels/videoCallScreenLabels'; +import {useString} from '../../utils/useString'; +import {UidType} from '../../../agora-rn-uikit'; +import {useRTMGlobalState} from '../RTMGlobalStateProvider'; +import {useContent} from 'customization-api'; +/** + * Hook to get user display names with fallback to main room RTM users + * This ensures users in breakout rooms can see names of users in other rooms + */ +export const useMainRoomUserDisplayName = () => { + const {mainRoomRTMUsers} = useRTMGlobalState(); + const {defaultContent} = useContent(); + const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)(); + + return (uid: UidType): string => { + // Priority: Local defaultContent → Global mainRoomRTMUsers → UID fallback + // TODO:SUP add trimText + return ( + defaultContent?.[uid]?.name || + mainRoomRTMUsers?.[uid]?.name || + remoteUserDefaultLabel + ); + }; +}; diff --git a/template/src/subComponents/ChatBubble.tsx b/template/src/subComponents/ChatBubble.tsx index 68ede02d1..14fab0627 100644 --- a/template/src/subComponents/ChatBubble.tsx +++ b/template/src/subComponents/ChatBubble.tsx @@ -52,6 +52,7 @@ import {useChatConfigure} from '../../src/components/chat/chatConfigure'; import Tooltip from '../../src/atoms/Tooltip'; import {MoreMessageOptions} from './chat/ChatQuickActionsMenu'; import {EMessageStatus} from '../../src/ai-agent/components/AgentControls/message'; +import {useMainRoomUserDisplayName} from '../rtm/hooks/useMainRoomUserDisplayName'; type AttachmentBubbleProps = { fileName: string; @@ -362,6 +363,7 @@ const ChatBubble = (props: ChatBubbleProps) => { //commented for v1 release //const remoteUserDefaultLabel = useString('remoteUserDefaultLabel')(); const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)(); + const getDisplayName = useMainRoomUserDisplayName(); const getUsername = () => { if (isLocal) { @@ -370,9 +372,7 @@ const ChatBubble = (props: ChatBubbleProps) => { if (remoteUIConfig?.username) { return trimText(remoteUIConfig?.username); } - return defaultContent[uid]?.name - ? trimText(defaultContent[uid].name) - : remoteUserDefaultLabel; + return getDisplayName(uid); }; return props?.render ? ( diff --git a/template/src/subComponents/ChatContainer.tsx b/template/src/subComponents/ChatContainer.tsx index 994563bfd..d60274b7b 100644 --- a/template/src/subComponents/ChatContainer.tsx +++ b/template/src/subComponents/ChatContainer.tsx @@ -27,7 +27,7 @@ import { } from 'react-native'; import {RFValue} from 'react-native-responsive-fontsize'; import ChatBubble from './ChatBubble'; -import {ChatBubbleProps} from '../components/ChatContext'; +import ChatContext, {ChatBubbleProps} from '../components/ChatContext'; import { DispatchContext, ContentInterface, @@ -71,6 +71,7 @@ const ChatContainer = (props?: { const info1 = useString(groupChatWelcomeContent); const [scrollToEnd, setScrollToEnd] = useState(false); const {dispatch} = useContext(DispatchContext); + const {syncUserState} = useContext(ChatContext); const [grpUnreadCount, setGrpUnreadCount] = useState(0); const [privateUnreadCount, setPrivateUnreadCount] = useState(0); const {defaultContent} = useContent(); @@ -118,16 +119,18 @@ const ChatContainer = (props?: { }); //Once message is seen, reset lastMessageTimeStamp. //so whoever has unread count will show in the top of participant list - updateRenderListState(privateChatUser, {lastMessageTimeStamp: 0}); + // updateRenderListState(privateChatUser, {lastMessageTimeStamp: 0}); + syncUserState(privateChatUser, {lastMessageTimeStamp: 0}); } }, [privateChatUser]); - const updateRenderListState = ( - uid: number, - data: Partial, - ) => { - dispatch({type: 'UpdateRenderList', value: [uid, data]}); - }; + // We will be using syncRoomusers + // const updateRenderListState = ( + // uid: number, + // data: Partial, + // ) => { + // dispatch({type: 'UpdateRenderList', value: [uid, data]}); + // }; const onScroll = event => { setScrollOffset(event.nativeEvent.contentOffset.y); diff --git a/template/src/subComponents/LocalAudioMute.tsx b/template/src/subComponents/LocalAudioMute.tsx index 828501f23..f3b66079b 100644 --- a/template/src/subComponents/LocalAudioMute.tsx +++ b/template/src/subComponents/LocalAudioMute.tsx @@ -85,7 +85,7 @@ function LocalAudioMute(props: LocalAudioMuteProps) { local.permissionStatus === PermissionState.REJECTED || local.permissionStatus === PermissionState.GRANTED_FOR_CAM_ONLY; - const onPress = () => { + const onPress = async () => { logger.log( LogSource.Internals, 'LOCAL_MUTE', @@ -95,7 +95,7 @@ function LocalAudioMute(props: LocalAudioMuteProps) { permissionDenied, }, ); - localMute(MUTE_LOCAL_TYPE.audio); + await localMute(MUTE_LOCAL_TYPE.audio); }; const audioLabel = permissionDenied ? micButtonLabel(I18nDeviceStatus.PERMISSION_DENIED) diff --git a/template/src/subComponents/LocalVideoMute.tsx b/template/src/subComponents/LocalVideoMute.tsx index 14c086012..45582e647 100644 --- a/template/src/subComponents/LocalVideoMute.tsx +++ b/template/src/subComponents/LocalVideoMute.tsx @@ -79,7 +79,7 @@ function LocalVideoMute(props: LocalVideoMuteProps) { ); const lstooltip = useString(livestreamingCameraTooltipText); - const onPress = () => { + const onPress = async () => { //if screensharing is going on native - to turn on video screenshare should be turn off //show confirm popup to stop the screenshare logger.log( @@ -91,7 +91,7 @@ function LocalVideoMute(props: LocalVideoMuteProps) { permissionDenied, }, ); - localMute(MUTE_LOCAL_TYPE.video); + await localMute(MUTE_LOCAL_TYPE.video); }; const isVideoEnabled = local.video === ToggleState.enabled; diff --git a/template/src/subComponents/screenshare/ScreenshareConfigure.native.tsx b/template/src/subComponents/screenshare/ScreenshareConfigure.native.tsx index b94449df4..61ce79867 100644 --- a/template/src/subComponents/screenshare/ScreenshareConfigure.native.tsx +++ b/template/src/subComponents/screenshare/ScreenshareConfigure.native.tsx @@ -319,7 +319,7 @@ export const ScreenshareConfigure = (props: {children: React.ReactNode}) => { 'Trying to start native screenshare', ); if (video) { - localMute(MUTE_LOCAL_TYPE.video); + await localMute(MUTE_LOCAL_TYPE.video); } try { await engine.current.startScreenCapture({ diff --git a/template/src/subComponents/waiting-rooms/WaitingRoomControls.tsx b/template/src/subComponents/waiting-rooms/WaitingRoomControls.tsx index bc82fe6de..5b80d8b16 100644 --- a/template/src/subComponents/waiting-rooms/WaitingRoomControls.tsx +++ b/template/src/subComponents/waiting-rooms/WaitingRoomControls.tsx @@ -14,12 +14,14 @@ import { peoplePanelWaitingRoomRequestApprovalBtnTxt, peoplePanelWaitingRoomRequestDenyBtnTxt, } from '../../../src/language/default-labels/videoCallScreenLabels'; +import ChatContext from '../../components/ChatContext'; const WaitingRoomButton = props => { const {uid, screenUid, isAccept} = props; const {approval} = useWaitingRoomAPI(); const localUid = useLocalUid(); const {dispatch} = useContext(DispatchContext); + const {syncUserState} = useContext(ChatContext); const {waitingRoomRef} = useWaitingRoomContext(); const admintext = useString(peoplePanelWaitingRoomRequestApprovalBtnTxt)(); const denytext = useString(peoplePanelWaitingRoomRequestDenyBtnTxt)(); @@ -40,10 +42,11 @@ const WaitingRoomButton = props => { Toast.hide(); } - dispatch({ - type: 'UpdateRenderList', - value: [uid, {isInWaitingRoom: false}], - }); + // dispatch({ + // type: 'UpdateRenderList', + // value: [uid, {isInWaitingRoom: false}], + // }); + syncUserState(uid, {isInWaitingRoom: false}); if (waitingRoomRef.current) { waitingRoomRef.current[uid] = approved ? 'APPROVED' : 'REJECTED'; diff --git a/template/src/utils/useMuteToggleLocal.ts b/template/src/utils/useMuteToggleLocal.ts index f887abc24..de8248ba1 100644 --- a/template/src/utils/useMuteToggleLocal.ts +++ b/template/src/utils/useMuteToggleLocal.ts @@ -86,14 +86,25 @@ function useMuteToggleLocal() { ); // Enable UI + const newAudioState = + localAudioState === ToggleState.enabled + ? ToggleState.disabled + : ToggleState.enabled; + dispatch({ type: 'LocalMuteAudio', - value: [ - localAudioState === ToggleState.enabled - ? ToggleState.disabled - : ToggleState.enabled, - ], + value: [newAudioState], }); + + // Sync audio preference to RTM (only saves in main room) + try { + syncUserPreferences({ + audioMuted: newAudioState === ToggleState.disabled, + }); + } catch (error) { + console.warn('Failed to sync audio preference:', error); + } + handleQueue(); } catch (e) { dispatch({ @@ -152,14 +163,25 @@ function useMuteToggleLocal() { ); } // Enable UI + const newVideoState = + localVideoState === ToggleState.enabled + ? ToggleState.disabled + : ToggleState.enabled; + dispatch({ type: 'LocalMuteVideo', - value: [ - localVideoState === ToggleState.enabled - ? ToggleState.disabled - : ToggleState.enabled, - ], + value: [newVideoState], }); + + // Sync video preference to RTM (only saves in main room) + try { + syncUserPreferences({ + videoMuted: newVideoState === ToggleState.disabled, + }); + } catch (error) { + console.warn('Failed to sync video preference:', error); + } + handleQueue(); } catch (e) { dispatch({ From 225e8aa65571437e0efa309aca507b6eed2ca972 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 29 Sep 2025 11:57:25 +0530 Subject: [PATCH 09/56] Qa/add feedbacks of 17th (#752) --- template/bridge/rtm/web/index.ts | 30 + .../context/BreakoutRoomContext.tsx | 1479 ++++++++--------- .../events/BreakoutRoomEventsConfigure.tsx | 84 +- .../BreakoutRoomMainEventsConfigure.tsx | 65 - .../components/breakout-room/state/reducer.ts | 54 +- .../ui/BreakoutRoomActionMenu.tsx | 2 +- .../ui/BreakoutRoomAnnouncementModal.tsx | 1 - .../ui/BreakoutRoomGroupSettings.tsx | 106 +- .../ui/BreakoutRoomMainRoomUsers.tsx | 4 +- .../ui/BreakoutRoomMemberActionMenu.tsx | 122 ++ .../ui/BreakoutRoomRaiseHand.tsx | 35 +- .../ui/BreakoutRoomRenameModal.tsx | 51 +- .../breakout-room/ui/BreakoutRoomView.tsx | 24 +- .../chat-messages/useChatMessages.tsx | 3 + .../src/components/chat/chatConfigure.tsx | 4 +- .../controls/useControlPermissionMatrix.tsx | 5 +- .../participants/UserActionMenuOptions.tsx | 22 - .../components/raise-hand/RaiseHandButton.tsx | 50 + .../raise-hand/RaiseHandProvider.tsx | 288 ++++ template/src/components/raise-hand/index.ts | 14 + .../room-info/useCurrentRoomInfo.tsx | 42 + .../room-info/useSetBreakoutRoomInfo.tsx | 67 + .../default-labels/videoCallScreenLabels.ts | 2 +- template/src/pages/VideoCall.tsx | 29 +- ...oCallContent.tsx => BreakoutVideoCall.tsx} | 47 +- .../src/pages/video-call/VideoCallContent.tsx | 46 +- template/src/rtm-events-api/Events.ts | 76 +- template/src/rtm-events/constants.ts | 37 +- .../rtm/RTMConfigureBreakoutRoomProvider.tsx | 852 +++++----- .../src/rtm/RTMConfigureMainRoomProvider.tsx | 496 +++--- template/src/rtm/RTMCoreProvider.tsx | 79 +- template/src/rtm/RTMEngine.ts | 34 +- template/src/rtm/RTMGlobalStateProvider.tsx | 635 ++++--- template/src/rtm/constants.ts | 11 + template/src/rtm/rtm-presence-utils.ts | 339 ++++ template/src/rtm/utils.ts | 86 +- template/src/subComponents/ChatContainer.tsx | 9 +- .../chat/ChatAnnouncementView.tsx | 67 + .../src/subComponents/chat/ChatSendButton.tsx | 1 + .../screenshare/ScreenshareButton.tsx | 12 +- template/src/utils/useEndCall.ts | 1 - 41 files changed, 3219 insertions(+), 2192 deletions(-) delete mode 100644 template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx create mode 100644 template/src/components/breakout-room/ui/BreakoutRoomMemberActionMenu.tsx create mode 100644 template/src/components/raise-hand/RaiseHandButton.tsx create mode 100644 template/src/components/raise-hand/RaiseHandProvider.tsx create mode 100644 template/src/components/raise-hand/index.ts create mode 100644 template/src/components/room-info/useCurrentRoomInfo.tsx create mode 100644 template/src/components/room-info/useSetBreakoutRoomInfo.tsx rename template/src/pages/video-call/{BreakoutVideoCallContent.tsx => BreakoutVideoCall.tsx} (89%) create mode 100644 template/src/rtm/rtm-presence-utils.ts create mode 100644 template/src/subComponents/chat/ChatAnnouncementView.tsx diff --git a/template/bridge/rtm/web/index.ts b/template/bridge/rtm/web/index.ts index 63c7af031..6bc0aa3b6 100644 --- a/template/bridge/rtm/web/index.ts +++ b/template/bridge/rtm/web/index.ts @@ -9,6 +9,8 @@ import { type GetUserMetadataResponse as NativeGetUserMetadataResponse, type GetChannelMetadataResponse as NativeGetChannelMetadataResponse, type SetOrUpdateUserMetadataOptions as NativeSetOrUpdateUserMetadataOptions, + type RemoveUserMetadataOptions as NativeRemoveUserMetadataOptions, + type RemoveUserMetadataResponse as NativeRemoveUserMetadataResponse, type IMetadataOptions as NativeIMetadataOptions, type StorageEvent as NativeStorageEvent, type PresenceEvent as NativePresenceEvent, @@ -23,6 +25,7 @@ import AgoraRTM, { PublishOptions, ChannelType, MetaDataDetail, + RemoveUserMetadataOptions, } from 'agora-rtm-sdk'; import { linkStatusReasonCodeMapping, @@ -221,6 +224,33 @@ export class RTMWebClient { return nativeResponse; }, + removeUserMetadata: async ( + options?: NativeRemoveUserMetadataOptions, + ): Promise => { + // Build the options object for the web SDK call + const webOptions: RemoveUserMetadataOptions = {}; + + // Add userId if provided (for removing another user's metadata, defaults to self if not provided) + if (options?.userId && typeof options.userId === 'string') { + webOptions.userId = options.userId; + } + + // Convert native Metadata to web MetadataItem[] format if provided + if ( + options?.data && + options.data.items && + Array.isArray(options.data.items) && + options.data.items.length > 0 + ) { + webOptions.data = options.data.items.map(item => ({ + key: item.key, + value: item.value || '', // Require not used for remove.we use keys + })); + } + + return await this.client.storage.removeUserMetadata(webOptions); + }, + setChannelMetadata: async ( channelName: string, channelType: NativeRtmChannelType, diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 5a8d144f7..5aa46d578 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -38,9 +38,9 @@ import { RTMUserData, useRTMGlobalState, } from '../../../rtm/RTMGlobalStateProvider'; +import {useScreenshare} from '../../../subComponents/screenshare/useScreenshare'; const BREAKOUT_LOCK_TIMEOUT_MS = 5000; -const HOST_OPERATION_LOCK_TIMEOUT_MS = 10000; // Emergency timeout for network failures only const HOST_BROADCASTED_OPERATIONS = [ BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, @@ -150,7 +150,6 @@ interface BreakoutRoomPermissions { // Media controls canScreenshare: boolean; canRaiseHands: boolean; - canSeeRaisedHands: boolean; // Room management (host only) canHostManageMainRoom: boolean; canAssignParticipants: boolean; @@ -165,7 +164,6 @@ const defaulBreakoutRoomPermission: BreakoutRoomPermissions = { canSwitchBetweenRooms: false, // Media controls canScreenshare: true, canRaiseHands: false, - canSeeRaisedHands: false, // Room management (host only) canHostManageMainRoom: false, canAssignParticipants: false, @@ -197,16 +195,10 @@ interface BreakoutRoomContextValue { upsertBreakoutRoomAPI: (type: 'START' | 'UPDATE') => Promise; checkIfBreakoutRoomSessionExistsAPI: () => Promise; handleAssignParticipants: (strategy: RoomAssignmentStrategy) => void; - sendAnnouncement: (announcement: string) => void; // Presenters onMakeMePresenter: (action: 'start' | 'stop') => void; presenters: {uid: UidType; timestamp: number}[]; clearAllPresenters: () => void; - // Raised hands - raisedHands: {uid: UidType; timestamp: number}[]; - sendRaiseHandEvent: (action: 'raise' | 'lower') => void; - onRaiseHand: (action: 'raise' | 'lower', uid: UidType) => void; - clearAllRaisedHands: () => void; // State sync handleBreakoutRoomSyncState: ( data: BreakoutRoomSyncStateEventPayload['data'], @@ -227,7 +219,6 @@ interface BreakoutRoomContextValue { // Loading states isBreakoutUpdateInFlight: boolean; // Multi-host coordination - isAnotherHostOperating: boolean; currentOperatingHostName?: string; // State version for forcing re-computation in dependent hooks breakoutRoomVersion: number; @@ -254,16 +245,11 @@ const BreakoutRoomContext = React.createContext({ updateRoomName: () => {}, getAllRooms: () => [], getRoomMemberDropdownOptions: () => [], - sendAnnouncement: () => {}, upsertBreakoutRoomAPI: async () => {}, checkIfBreakoutRoomSessionExistsAPI: async () => false, onMakeMePresenter: () => {}, presenters: [], clearAllPresenters: () => {}, - raisedHands: [], - sendRaiseHandEvent: () => {}, - onRaiseHand: () => {}, - clearAllRaisedHands: () => {}, handleBreakoutRoomSyncState: () => {}, // Multi-host coordination handlers handleHostOperationStart: () => {}, @@ -273,7 +259,6 @@ const BreakoutRoomContext = React.createContext({ // Loading states isBreakoutUpdateInFlight: false, // Multi-host coordination - isAnotherHostOperating: false, currentOperatingHostName: undefined, // State version for forcing re-computation in dependent hooks breakoutRoomVersion: 0, @@ -314,29 +299,43 @@ const BreakoutRoomProvider = ({ ...defaulBreakoutRoomPermission, }); - // Multi-host coordination state - const [isAnotherHostOperating, setIsAnotherHostOperating] = useState(false); + // Store the last operation const [currentOperatingHostName, setCurrentOperatingHostName] = useState< string | undefined >(undefined); // Timestamp tracking for event ordering - const lastProcessedTimestampRef = useRef(0); + const lastSyncedTimestampRef = useRef(0); + const lastSyncedSnapshotRef = useRef<{ + session_id: string; + switch_room: boolean; + assignment_type: string; + breakout_room: BreakoutGroup[]; + } | null>(null); + + // Breakout sync queue (latest-event-wins) + const breakoutSyncQueueRef = useRef<{ + latestTask: { + payload: BreakoutRoomSyncStateEventPayload['data']; + timestamp: number; + } | null; + isProcessing: boolean; + }>({ + latestTask: null, + isProcessing: false, + }); // Join Room pending intent const [selfJoinRoomId, setSelfJoinRoomId] = useState(null); // Presenter + const {isScreenshareActive, stopScreenshare} = useScreenshare(); + const [canIPresent, setICanPresent] = useState(false); const [presenters, setPresenters] = useState< {uid: UidType; timestamp: number}[] >([]); - // Raised hands - const [raisedHands, setRaisedHands] = useState< - {uid: UidType; timestamp: number}[] - >([]); - // State version tracker to force dependent hooks to re-compute const [breakoutRoomVersion, setBreakoutRoomVersion] = useState(0); @@ -346,8 +345,6 @@ const BreakoutRoomProvider = ({ const isHostRef = useRef(isHost); const defaultContentRef = useRef(defaultContent); const isMountedRef = useRef(true); - // Concurrent action protection - track users being moved - const usersBeingMovedRef = useRef>(new Set()); // Enhanced dispatch that tracks user actions const [lastAction, setLastAction] = useState(null); @@ -388,10 +385,6 @@ const BreakoutRoomProvider = ({ // Timeouts const timeoutsRef = useRef>>(new Set()); - // Track host operation timeout for manual clearing - const hostOperationTimeoutRef = useRef | null>( - null, - ); const safeSetTimeout = useCallback((fn: () => void, delay: number) => { const id = setTimeout(() => { @@ -402,10 +395,7 @@ const BreakoutRoomProvider = ({ timeoutsRef.current.add(id); return id; }, []); - const safeClearTimeout = useCallback((id: ReturnType) => { - clearTimeout(id); - timeoutsRef.current.delete(id); - }, []); + // Clear all timeouts useEffect(() => { const snapshot = timeoutsRef.current; @@ -490,34 +480,9 @@ const BreakoutRoomProvider = ({ // Common operation lock for API-triggering actions with multi-host coordination const acquireOperationLock = useCallback( - (operationName: string, showToast = true): boolean => { + (operationName: string): boolean => { // Check if another host is operating console.log('supriya-state-sync acquiring lock step 1'); - if (isAnotherHostOperating) { - console.log('supriya-state-sync isAnotherHostOperating is true'); - - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Operation blocked - another host is operating', - { - blockedOperation: operationName, - operatingHost: currentOperatingHostName, - }, - ); - - if (showToast) { - showDeduplicatedToast(`operation-blocked-host-${operationName}`, { - type: 'info', - text1: `${ - currentOperatingHostName || 'Another host' - } is currently managing breakout rooms`, - text2: 'Please wait for them to finish', - visibilityTime: 3000, - }); - } - return false; - } // Check if API call is in progress if (isBreakoutUpdateInFlight) { @@ -530,14 +495,6 @@ const BreakoutRoomProvider = ({ currentlyInFlight: isBreakoutUpdateInFlight, }, ); - - if (showToast) { - showDeduplicatedToast(`operation-blocked-${operationName}`, { - type: 'info', - text1: 'Please wait for current operation to complete', - visibilityTime: 3000, - }); - } return false; } @@ -546,6 +503,7 @@ const BreakoutRoomProvider = ({ 'supriya-state-sync broadcasting host operation start', operationName, ); + setBreakoutUpdateInFlight(true); broadcastHostOperationStart(operationName); logger.log( @@ -558,68 +516,11 @@ const BreakoutRoomProvider = ({ }, [ isBreakoutUpdateInFlight, - isAnotherHostOperating, - currentOperatingHostName, - showDeduplicatedToast, broadcastHostOperationStart, + setBreakoutUpdateInFlight, ], ); - // Individual user lock: so that same user is not moved from two different actions - const acquireUserLock = (uid: UidType, operation: string): boolean => { - if (usersBeingMovedRef.current.has(uid)) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Concurrent action blocked - user already being moved', - { - uid, - operation, - currentlyBeingMoved: Array.from(usersBeingMovedRef.current), - }, - ); - return false; - } - - usersBeingMovedRef.current.add(uid); - - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - `User lock acquired for ${operation}`, - {uid, operation}, - ); - - // 🛡️ Auto-release lock after timeout to prevent deadlocks - safeSetTimeout(() => { - if (usersBeingMovedRef.current.has(uid)) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Auto-releasing user lock after timeout', - {uid, operation, timeoutMs: BREAKOUT_LOCK_TIMEOUT_MS}, - ); - usersBeingMovedRef.current.delete(uid); - } - }, BREAKOUT_LOCK_TIMEOUT_MS); - - return true; - }; - - const releaseUserLock = (uid: UidType, operation: string): void => { - const wasLocked = usersBeingMovedRef.current.has(uid); - usersBeingMovedRef.current.delete(uid); - - if (wasLocked) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - `User lock released for ${operation}`, - {uid, operation}, - ); - } - }; - // Update unassigned participants and remove offline users from breakout rooms useEffect(() => { if (!stateRef.current?.breakoutSessionId) { @@ -710,16 +611,16 @@ const BreakoutRoomProvider = ({ useCallback(async (): Promise => { // Skip API call if roomId is not available or if API update is in progress if (!joinRoomId?.host && !joinRoomId?.attendee) { - console.log('supriya-api: Skipping GET no roomId available'); + console.log('supriya-sync-queue: Skipping GET no roomId available'); return false; } if (isBreakoutUpdateInFlight) { - console.log('supriya-api upsert in progress: Skipping GET'); + console.log('supriya-sync-queue upsert in progress: Skipping GET'); return false; } console.log( - 'supriya-api calling checkIfBreakoutRoomSessionExistsAPI', + 'supriya-sync-queue calling checkIfBreakoutRoomSessionExistsAPI', joinRoomId, isHostRef.current, ); @@ -752,7 +653,7 @@ const BreakoutRoomProvider = ({ 'X-Session-Id': logger.getSessionId(), }, }); - // 🛡️ Guard against component unmount after fetch + // Guard against component unmount after fetch if (!isMountedRef.current) { logger.log( LogSource.Internals, @@ -778,7 +679,9 @@ const BreakoutRoomProvider = ({ requestId, }, ); - + if (!response.ok) { + throw new Error(`Failed with status ${response.status}`); + } if (response.status === 204) { logger.log( LogSource.Internals, @@ -788,62 +691,18 @@ const BreakoutRoomProvider = ({ return false; } - if (!response.ok) { - throw new Error(`Failed with status ${response.status}`); - } - const data = await response.json(); console.log('supriya-api-get response', data.sts, data); - // 🛡️ Guard against component unmount after JSON parsing - if (!isMountedRef.current) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Session sync cancelled - component unmounted after parsing', - {requestId}, - ); - return false; - } if (data?.session_id) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Session synced successfully', - { - sessionId: data.session_id, - roomCount: data?.breakout_room?.length || 0, - assignmentType: data?.assignment_type, - switchRoom: data?.switch_room, - }, - ); - - // Skip events older than the last processed timestamp - if (data?.sts && data?.sts <= lastProcessedTimestampRef.current) { - console.log( - 'supriya-api-get skipping dispatch as out of date/order ', - { - timestamp: data?.sts, - lastProcessed: lastProcessedTimestampRef.current, - }, - ); - return; - } - dispatch({ - type: BreakoutGroupActionTypes.SYNC_STATE, - payload: { - sessionId: data.session_id, - rooms: data?.breakout_room || [], - assignmentStrategy: - data?.assignment_type || RoomAssignmentStrategy.NO_ASSIGN, - switchRoom: data?.switch_room ?? true, - }, + logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Session exits', { + sessionId: data.session_id, + roomCount: data?.breakout_room?.length || 0, + assignmentType: data?.assignment_type, + switchRoom: data?.switch_room, }); - lastProcessedTimestampRef.current = data.sts || Date.now(); - return true; } - return false; } catch (error) { const latency = Date.now() - startTime; @@ -856,20 +715,39 @@ const BreakoutRoomProvider = ({ }); return false; } - }, [isBreakoutUpdateInFlight, dispatch, joinRoomId, store.token]); + }, [isBreakoutUpdateInFlight, joinRoomId, store.token]); useEffect(() => { + if (!joinRoomId?.host && !joinRoomId?.attendee) { + return; + } const loadInitialData = async () => { + console.log( + 'supriya-sync-queue checkIfBreakoutRoomSessionExistsAPI called', + ); await checkIfBreakoutRoomSessionExistsAPI(); }; + + // Check if we just transitioned to breakout mode as that we can delay the call + // to check breakout api + const justEnteredBreakout = sessionStorage.getItem( + 'breakout_room_transition', + ); + const delay = justEnteredBreakout ? 3000 : 1200; + + if (justEnteredBreakout) { + sessionStorage.removeItem('breakout_room_transition'); // Clear flag + console.log('Using extended delay for breakout transition'); + } + const timeoutId = setTimeout(() => { loadInitialData(); - }, 1200); + }, delay); return () => { clearTimeout(timeoutId); }; - }, []); + }, [joinRoomId, checkIfBreakoutRoomSessionExistsAPI]); const upsertBreakoutRoomAPI = useCallback( async (type: 'START' | 'UPDATE' = 'START', retryCount = 0) => { @@ -1139,7 +1017,9 @@ const BreakoutRoomProvider = ({ }; const createBreakoutRoomGroup = () => { - if (!acquireOperationLock('CREATE_GROUP')) return; + if (!acquireOperationLock('CREATE_GROUP')) { + return; + } logger.log( LogSource.Internals, @@ -1158,6 +1038,7 @@ const BreakoutRoomProvider = ({ }; const handleAssignParticipants = (strategy: RoomAssignmentStrategy) => { + console.log('supriya-assign', stateRef.current); if (stateRef.current.breakoutGroups.length === 0) { Toast.show({ type: 'info', @@ -1166,6 +1047,29 @@ const BreakoutRoomProvider = ({ }); return; } + + // Check for participants available for assignment based on strategy + const availableParticipants = + strategy === RoomAssignmentStrategy.AUTO_ASSIGN + ? stateRef.current.unassignedParticipants.filter( + participant => participant.uid !== localUid, + ) + : stateRef.current.unassignedParticipants; + + if (availableParticipants.length === 0) { + const message = + strategy === RoomAssignmentStrategy.AUTO_ASSIGN && + stateRef.current.unassignedParticipants.length > 0 + ? 'No other participants to assign. (Host is excluded from auto-assignment)' + : 'No participants left to assign.'; + + Toast.show({ + type: 'info', + text1: message, + visibilityTime: 3000, + }); + return; + } if (!acquireOperationLock(`ASSIGN_${strategy}`)) { return; } @@ -1197,35 +1101,28 @@ const BreakoutRoomProvider = ({ } }; - const moveUserToMainRoom = (user: ContentInterface) => { + const moveUserToMainRoom = (uid: UidType) => { try { - if (!user) { + if (!uid) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Move to main room failed - no user provided', + 'Move to main room failed - no uid provided', ); return; } // 🛡️ Check for API operation conflicts first - if (!acquireOperationLock('MOVE_PARTICIPANT_TO_MAIN', false)) { + if (!acquireOperationLock('MOVE_PARTICIPANT_TO_MAIN')) { return; } - const operation = 'moveToMain'; - - // 🛡️ Check if user is already being moved by another action - if (!acquireUserLock(user.uid, operation)) { - return; // Action blocked due to concurrent operation - } - // 🛡️ Use fresh state to avoid race conditions const currentState = stateRef.current; const currentGroup = currentState.breakoutGroups.find( group => - group.participants.hosts.includes(user.uid) || - group.participants.attendees.includes(user.uid), + group.participants.hosts.includes(uid) || + group.participants.attendees.includes(uid), ); logger.log( @@ -1233,8 +1130,7 @@ const BreakoutRoomProvider = ({ 'BREAKOUT_ROOM', 'Moving user to main room', { - userId: user.uid, - userName: user.name, + userId: uid, fromGroupId: currentGroup?.id, fromGroupName: currentGroup?.name, }, @@ -1244,60 +1140,47 @@ const BreakoutRoomProvider = ({ dispatch({ type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, payload: { - user, + uid, fromGroupId: currentGroup.id, }, }); } - - // 🛡️ Release lock after successful dispatch - releaseUserLock(user.uid, operation); } catch (error) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', 'Error moving user to main room', { - userId: user.uid, - userName: user.name, + userId: uid, error: error.message, }, ); - // 🛡️ Always release lock on error - releaseUserLock(user.uid, 'moveToMain'); } }; - const moveUserIntoGroup = (user: ContentInterface, toGroupId: string) => { + const moveUserIntoGroup = (uid: UidType, toGroupId: string) => { try { - if (!user) { + if (!uid) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Move to group failed - no user provided', + 'Move to group failed - no uid provided', {toGroupId}, ); return; } // 🛡️ Check for API operation conflicts first - if (!acquireOperationLock('MOVE_PARTICIPANT_TO_GROUP', false)) { + if (!acquireOperationLock('MOVE_PARTICIPANT_TO_GROUP')) { return; } - const operation = `moveToGroup-${toGroupId}`; - - // 🛡️ Check if user is already being moved by another action - if (!acquireUserLock(user.uid, operation)) { - return; // Action blocked due to concurrent operation - } - // 🛡️ Use fresh state to avoid race conditions const currentState = stateRef.current; const currentGroup = currentState.breakoutGroups.find( group => - group.participants.hosts.includes(user.uid) || - group.participants.attendees.includes(user.uid), + group.participants.hosts.includes(uid) || + group.participants.attendees.includes(uid), ); const targetGroup = currentState.breakoutGroups.find( group => group.id === toGroupId, @@ -1309,13 +1192,11 @@ const BreakoutRoomProvider = ({ 'BREAKOUT_ROOM', 'Target group not found', { - userId: user.uid, - userName: user.name, + userId: uid, toGroupId, }, ); - // 🛡️ Release lock if target group not found - releaseUserLock(user.uid, operation); + return; } @@ -1324,8 +1205,7 @@ const BreakoutRoomProvider = ({ 'BREAKOUT_ROOM', 'Moving user between groups', { - userId: user.uid, - userName: user.name, + userId: uid, fromGroupId: currentGroup?.id, fromGroupName: currentGroup?.name, toGroupId, @@ -1336,32 +1216,25 @@ const BreakoutRoomProvider = ({ dispatch({ type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, payload: { - user, + uid, fromGroupId: currentGroup?.id, toGroupId, }, }); - - // 🛡️ Release lock after successful dispatch - releaseUserLock(user.uid, operation); } catch (error) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', 'Error moving user to breakout room', { - userId: user.uid, - userName: user.name, + userId: uid, toGroupId, error: error.message, }, ); - // 🛡️ Always release lock on error - releaseUserLock(user.uid, `moveToGroup-${toGroupId}`); } }; - // To check if current user is in a specific room const isUserInRoom = useCallback( (room?: BreakoutGroup): boolean => { if (room) { @@ -1382,56 +1255,61 @@ const BreakoutRoomProvider = ({ [localUid, breakoutRoomVersion], ); - const getCurrentRoom = useCallback((): BreakoutGroup | null => { - const userRoom = stateRef.current.breakoutGroups.find( - group => - group.participants.hosts.includes(localUid) || - group.participants.attendees.includes(localUid), - ); - return userRoom ?? null; - }, [localUid, breakoutRoomVersion]); + const findUserRoomId = (uid: UidType, groups: BreakoutGroup[] = []) => + groups.find(g => { + const hosts = Array.isArray(g?.participants?.hosts) + ? g.participants.hosts + : []; + const attendees = Array.isArray(g?.participants?.attendees) + ? g.participants.attendees + : []; + return hosts.includes(uid) || attendees.includes(uid); + })?.id ?? null; // Permissions useEffect(() => { - const current = stateRef.current; - - const currentlyInRoom = isUserInRoom(); - const hasAvailableRooms = current.breakoutGroups.length > 0; - const allowAttendeeSwitch = current.canUserSwitchRoom; - - const nextPermissions: BreakoutRoomPermissions = { - canJoinRoom: - hasAvailableRooms && (isHostRef.current || allowAttendeeSwitch), - canExitRoom: currentlyInRoom, - canSwitchBetweenRooms: - currentlyInRoom && - hasAvailableRooms && - (isHostRef.current || allowAttendeeSwitch), - canScreenshare: isHostRef.current - ? true - : currentlyInRoom - ? canIPresent - : true, - canRaiseHands: - !isHostRef.current && !!current.breakoutSessionId && currentlyInRoom, - canSeeRaisedHands: true, - canAssignParticipants: isHostRef.current && !currentlyInRoom, - canHostManageMainRoom: isHostRef.current, - canCreateRooms: isHostRef.current, - canMoveUsers: isHostRef.current, - canCloseRooms: - isHostRef.current && hasAvailableRooms && !!current.breakoutSessionId, - canMakePresenter: isHostRef.current, - }; + if (lastSyncedSnapshotRef.current) { + const current = lastSyncedSnapshotRef.current; - setPermissions(nextPermissions); - }, [breakoutRoomVersion, canIPresent]); + const currentlyInRoom = !!findUserRoomId(localUid, current.breakout_room); + const hasAvailableRooms = current.breakout_room?.length > 0; + const allowAttendeeSwitch = current.switch_room; + console.log( + 'supriya-canraisehands', + !isHostRef.current && !!current.session_id && currentlyInRoom, + localUid, + current.breakout_room, + ); + const nextPermissions: BreakoutRoomPermissions = { + canJoinRoom: + hasAvailableRooms && (isHostRef.current || allowAttendeeSwitch), + canExitRoom: isBreakoutMode && currentlyInRoom, + canSwitchBetweenRooms: + currentlyInRoom && + hasAvailableRooms && + (isHostRef.current || allowAttendeeSwitch), + canScreenshare: isHostRef.current + ? true + : currentlyInRoom + ? canIPresent + : true, + canRaiseHands: !isHostRef.current && !!current.session_id, + canAssignParticipants: isHostRef.current && !currentlyInRoom, + canHostManageMainRoom: isHostRef.current, + canCreateRooms: isHostRef.current, + canMoveUsers: isHostRef.current, + canCloseRooms: + isHostRef.current && hasAvailableRooms && !!current.session_id, + canMakePresenter: isHostRef.current, + }; + setPermissions(nextPermissions); + } + }, [breakoutRoomVersion, canIPresent, isBreakoutMode, localUid]); const joinRoom = ( toRoomId: string, permissionAtCallTime = permissions.canJoinRoom, ) => { - // 🛡️ Use permission passed at call time to avoid race conditions if (!permissionAtCallTime) { logger.log( LogSource.Internals, @@ -1445,8 +1323,7 @@ const BreakoutRoomProvider = ({ ); return; } - const user = defaultContentRef.current[localUid]; - if (!user) { + if (!localUid) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', @@ -1458,23 +1335,19 @@ const BreakoutRoomProvider = ({ logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'User joining room', { userId: localUid, - userName: user.name, toRoomId, toRoomName: stateRef.current.breakoutGroups.find(r => r.id === toRoomId) ?.name, }); - moveUserIntoGroup(user, toRoomId); + moveUserIntoGroup(localUid, toRoomId); if (!isHostRef.current) { setSelfJoinRoomId(toRoomId); } }; const exitRoom = useCallback( - async ( - fromRoomId?: string, - permissionAtCallTime = permissions.canExitRoom, - ) => { + async (permissionAtCallTime = permissions.canExitRoom) => { // 🛡️ Use permission passed at call time to avoid race conditions if (!permissionAtCallTime) { logger.log( @@ -1482,7 +1355,6 @@ const BreakoutRoomProvider = ({ 'BREAKOUT_ROOM', 'Exit room blocked - no permission at call time', { - fromRoomId, permissionAtCallTime, currentPermission: permissions.canExitRoom, }, @@ -1491,19 +1363,9 @@ const BreakoutRoomProvider = ({ } const localUser = defaultContentRef.current[localUid]; - const currentRoom = getCurrentRoom(); - const currentRoomId = fromRoomId ? fromRoomId : currentRoom?.id; - - logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'User exiting room', { - userId: localUid, - userName: localUser?.name, - fromRoomId: currentRoomId, - fromRoomName: currentRoom?.name, - hasLocalUser: !!localUser, - }); try { - if (currentRoomId && localUser) { + if (localUser) { // Use breakout-specific exit (doesn't destroy main RTM) await breakoutRoomExit(); @@ -1513,25 +1375,10 @@ const BreakoutRoomProvider = ({ LogSource.Internals, 'BREAKOUT_ROOM', 'Exit room cancelled - component unmounted', - {userId: localUid, fromRoomId: currentRoomId}, + {userId: localUid}, ); return; } - - dispatch({ - type: BreakoutGroupActionTypes.EXIT_GROUP, - payload: { - user: localUser, - fromGroupId: currentRoomId, - }, - }); - - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'User exit room success', - {userId: localUid, fromRoomId: currentRoomId}, - ); } } catch (error) { logger.log( @@ -1540,25 +1387,12 @@ const BreakoutRoomProvider = ({ 'Exit room error - fallback dispatch', { userId: localUid, - fromRoomId: currentRoomId, error: error.message, }, ); - - if (currentRoom && localUser) { - dispatch({ - type: BreakoutGroupActionTypes.EXIT_GROUP, - payload: { - user: localUser, - fromGroupId: currentRoom.id, - }, - }); - } } }, [ - dispatch, - getCurrentRoom, localUid, permissions.canExitRoom, // TODO:SUP move to the method call breakoutRoomExit, @@ -1590,7 +1424,9 @@ const BreakoutRoomProvider = ({ }; const closeAllRooms = () => { - if (!acquireOperationLock('CLOSE_ALL_GROUPS')) return; + if (!acquireOperationLock('CLOSE_ALL_GROUPS')) { + return; + } logger.log( LogSource.Internals, @@ -1613,32 +1449,10 @@ const BreakoutRoomProvider = ({ dispatch({type: BreakoutGroupActionTypes.CLOSE_ALL_GROUPS}); }; - const sendAnnouncement = (announcement: string) => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Sending announcement to all rooms', - { - announcementLength: announcement.length, - roomCount: stateRef.current.breakoutGroups.length, - senderUserId: localUid, - senderUserName: defaultContentRef.current[localUid]?.name, - isHost: isHostRef.current, - }, - ); - - events.send( - BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT, - JSON.stringify({ - uid: localUid, - timestamp: Date.now(), - announcement, - }), - ); - }; - const updateRoomName = (newRoomName: string, roomIdToEdit: string) => { - if (!acquireOperationLock('RENAME_GROUP')) return; + if (!acquireOperationLock('RENAME_GROUP')) { + return; + } const roomToRename = stateRef.current.breakoutGroups.find( r => r.id === roomIdToEdit, @@ -1675,35 +1489,36 @@ const BreakoutRoomProvider = ({ ); // User wants to start presenting - const makePresenter = (user: ContentInterface, action: 'start' | 'stop') => { + const makePresenter = (uid: UidType, action: 'start' | 'stop') => { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', `Make presenter - ${action}`, { - targetUserId: user.uid, - targetUserName: user.name, + targetUserId: uid, action, isHost: isHostRef.current, }, ); - + if (!uid) { + return; + } try { // Host can make someone a presenter events.send( BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, JSON.stringify({ - uid: user.uid, + uid: uid, timestamp: Date.now(), action, }), PersistanceLevel.None, - user.uid, + uid, ); if (action === 'start') { - addPresenter(user.uid); + addPresenter(uid); } else if (action === 'stop') { - removePresenter(user.uid); + removePresenter(uid); } } catch (error) { logger.log( @@ -1711,8 +1526,7 @@ const BreakoutRoomProvider = ({ 'BREAKOUT_ROOM', 'Error making user presenter', { - targetUserId: user.uid, - targetUserName: user.name, + targetUserId: uid, action, error: error.message, }, @@ -1738,31 +1552,37 @@ const BreakoutRoomProvider = ({ } }, []); - const onMakeMePresenter = useCallback((action: 'start' | 'stop') => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - `User became presenter - ${action}`, - ); + const onMakeMePresenter = useCallback( + (action: 'start' | 'stop') => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `User became presenter - ${action}`, + ); - if (action === 'start') { - setICanPresent(true); - // Show toast notification when presenter permission is granted - Toast.show({ - type: 'success', - text1: 'You can now present in this breakout room', - visibilityTime: 3000, - }); - } else if (action === 'stop') { - setICanPresent(false); - // Show toast notification when presenter permission is removed - Toast.show({ - type: 'info', - text1: 'Your presenter access has been removed', - visibilityTime: 3000, - }); - } - }, []); + if (action === 'start') { + setICanPresent(true); + // Show toast notification when presenter permission is granted + Toast.show({ + type: 'success', + text1: 'You can now present in this breakout room', + visibilityTime: 3000, + }); + } else if (action === 'stop') { + if (isScreenshareActive) { + stopScreenshare(); + } + setICanPresent(false); + // Show toast notification when presenter permission is removed + Toast.show({ + type: 'info', + text1: 'Your presenter access has been removed', + visibilityTime: 3000, + }); + } + }, + [isScreenshareActive], + ); const clearAllPresenters = useCallback(() => { setPresenters([]); @@ -1773,8 +1593,7 @@ const BreakoutRoomProvider = ({ const options: MemberDropdownOption[] = []; // Find which room the user is currently in - const memberUser = defaultContentRef.current[memberUid]; - if (!memberUser) { + if (!memberUid) { return options; } @@ -1794,7 +1613,7 @@ const BreakoutRoomProvider = ({ icon: 'double-up-arrow', type: 'move-to-main', title: 'Move to Main Room', - onOptionPress: () => moveUserToMainRoom(memberUser), + onOptionPress: () => moveUserToMainRoom(memberUid), }); // Move to other breakout rooms (exclude current room) stateRef.current.breakoutGroups @@ -1803,14 +1622,21 @@ const BreakoutRoomProvider = ({ options.push({ type: 'move-to-room', icon: 'move-up', - title: `Shift to ${group.name}`, + title: `Shift to : ${group.name}`, roomId: group.id, roomName: group.name, - onOptionPress: () => moveUserIntoGroup(memberUser, group.id), + onOptionPress: () => moveUserIntoGroup(memberUid, group.id), }); }); - // Make presenter option (only for hosts) + // Make presenter option is available only for host + // and if the incoming member is also a host we dont + // need to show this option as they can already present + const isUserHost = + currentRoom?.participants.hosts.includes(memberUid) || false; + if (isUserHost) { + return options; + } if (isHostRef.current) { const userIsPresenting = isUserPresenting(memberUid); const title = userIsPresenting ? 'Stop presenter' : 'Make a Presenter'; @@ -1819,109 +1645,375 @@ const BreakoutRoomProvider = ({ type: 'make-presenter', icon: 'promote-filled', title, - onOptionPress: () => makePresenter(memberUser, action), + onOptionPress: () => makePresenter(memberUid, action), }); } return options; }, - [isUserPresenting, isHostRef.current, presenters, breakoutRoomVersion], + [isUserPresenting, presenters, breakoutRoomVersion], ); - // Raise Hand - // Send raise hand event via RTM - const sendRaiseHandEvent = useCallback( - (action: 'raise' | 'lower') => { + // const handleBreakoutRoomSyncState = useCallback( + // (payload: BreakoutRoomSyncStateEventPayload['data'], timestamp) => { + // console.log( + // 'supriya-api-sync response', + // timestamp, + // JSON.stringify(payload), + // ); + + // // Skip events older than the last processed timestamp + // if (timestamp && timestamp <= lastProcessedTimestampRef.current) { + // console.log('supriya-api-sync Skipping old breakout room sync event', { + // timestamp, + // lastProcessed: lastProcessedTimestampRef.current, + // }); + // return; + // } + + // const {srcuid, data} = payload; + // console.log('supriya-event flow step 2', srcuid); + // console.log('supriya-event uids', srcuid, localUid); + + // // if (srcuid === localUid) { + // // console.log('supriya-event flow skipping'); + + // // return; + // // } + // const {session_id, switch_room, breakout_room, assignment_type} = data; + // console.log('supriya-event-sync new data: ', data); + // console.log('supriya-event-sync old data: ', stateRef.current); + + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // 'Sync state event received', + // { + // sessionId: session_id, + // incomingRoomCount: breakout_room?.length || 0, + // currentRoomCount: stateRef.current.breakoutGroups.length, + // switchRoom: switch_room, + // assignmentType: assignment_type, + // }, + // ); + + // if (isAnotherHostOperating) { + // setIsAnotherHostOperating(false); + // setCurrentOperatingHostName(undefined); + // } + // // 🛡️ BEFORE snapshot - using stateRef to avoid stale closure + // const prevGroups = stateRef.current.breakoutGroups; + // console.log('supriya-event-sync prevGroups: ', prevGroups); + // const prevSwitchRoom = stateRef.current.canUserSwitchRoom; + + // // Helpers to find membership + // const findUserRoomId = (uid: UidType, groups: BreakoutGroup[] = []) => + // groups.find(g => { + // const hosts = Array.isArray(g?.participants?.hosts) + // ? g.participants.hosts + // : []; + // const attendees = Array.isArray(g?.participants?.attendees) + // ? g.participants.attendees + // : []; + // return hosts.includes(uid) || attendees.includes(uid); + // })?.id ?? null; + + // const prevRoomId = findUserRoomId(localUid, prevGroups); + // const nextRoomId = findUserRoomId(localUid, breakout_room); + + // console.log( + // 'supriya-event-sync prevRoomId and nextRoomId: ', + // prevRoomId, + // nextRoomId, + // ); + + // console.log('supriya-event-sync 1: '); + // // Show notifications based on changes + // // 1. Switch room enabled notification + // const senderName = getDisplayName(srcuid); + // if (switch_room && !prevSwitchRoom) { + // console.log('supriya-toast 1'); + // showDeduplicatedToast('switch-room-toggle', { + // leadingIconName: 'open-room', + // type: 'info', + // text1: `Host:${senderName} has opened breakout rooms.`, + // text2: 'Please choose a room to join.', + // visibilityTime: 3000, + // }); + // } + // console.log('supriya-event-sync 2: '); + + // // 2. User joined a room (compare previous and current state) + // // The notification for this comes from the main room channel_join event + // if (prevRoomId === nextRoomId) { + // // No logic + // } + + // console.log('supriya-event-sync 3: '); + + // // 3. User was moved to main room + // if (prevRoomId && !nextRoomId) { + // const prevRoom = prevGroups.find(r => r.id === prevRoomId); + // // Distinguish "room closed" vs "moved to main" + // const roomStillExists = breakout_room.some(r => r.id === prevRoomId); + + // if (!roomStillExists) { + // showDeduplicatedToast(`current-room-closed-${prevRoomId}`, { + // leadingIconName: 'close-room', + // type: 'error', + // text1: `Host: ${senderName} has closed "${ + // prevRoom?.name || '' + // }" room. `, + // text2: 'Returning to main room...', + // visibilityTime: 3000, + // }); + // } else { + // showDeduplicatedToast(`moved-to-main-${prevRoomId}`, { + // leadingIconName: 'arrow-up', + // type: 'info', + // text1: `Host: ${senderName} has moved you to main room.`, + // visibilityTime: 3000, + // }); + // } + // // Exit breakout room and return to main room + // return exitRoom(true); + // } + + // console.log('supriya-event-sync 5: '); + + // // 5. All breakout rooms closed + // if (breakout_room.length === 0 && prevGroups.length > 0) { + // console.log('supriya-toast 5', prevRoomId, nextRoomId); + + // // Show different messages based on user's current location + // if (prevRoomId) { + // // User was in a breakout room - returning to main + // showDeduplicatedToast('all-rooms-closed', { + // leadingIconName: 'close-room', + // type: 'info', + // text1: `Host: ${senderName} has closed all breakout rooms.`, + // text2: 'Returning to the main room...', + // visibilityTime: 3000, + // }); + // return exitRoom(true); + // } else { + // // User was already in main room - just notify about closure + // showDeduplicatedToast('all-rooms-closed', { + // leadingIconName: 'close-room', + // type: 'info', + // text1: `Host: ${senderName} has closed all breakout rooms`, + // visibilityTime: 4000, + // }); + // } + // } + + // console.log('supriya-event-sync 6: '); + + // // 6) Room renamed (compare per-room names) + // prevGroups.forEach(prevRoom => { + // const after = breakout_room.find(r => r.id === prevRoom.id); + // if (after && after.name !== prevRoom.name) { + // showDeduplicatedToast(`room-renamed-${after.id}`, { + // type: 'info', + // text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`, + // visibilityTime: 3000, + // }); + // } + // }); + + // console.log('supriya-event-sync 7: '); + + // // The host clicked on the room to close in which he is a part of + // if (!prevRoomId && !nextRoomId) { + // return exitRoom(true); + // } + // // Finally, apply the authoritative state + // dispatch({ + // type: BreakoutGroupActionTypes.SYNC_STATE, + // payload: { + // sessionId: session_id, + // assignmentStrategy: assignment_type, + // switchRoom: switch_room, + // rooms: breakout_room, + // }, + // }); + // // Update the last processed timestamp after successful processing + // lastProcessedTimestampRef.current = timestamp || Date.now(); + // }, + // [ + // dispatch, + // exitRoom, + // localUid, + // showDeduplicatedToast, + // isAnotherHostOperating, + // getDisplayName, + // ], + // ); + + // Multi-host coordination handlers + const handleHostOperationStart = useCallback( + (operationName: string, hostUid: UidType, hostName: string) => { + // Only process if current user is also a host and it's not their own event + console.log('supriya-state-sync host operation started', operationName); + if (!isHostRef.current || hostUid === localUid) { + return; + } + logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - `Send raise hand event - ${action}`, - {action, userId: localUid}, + 'Another host started operation - locking UI', + {operationName, hostUid, hostName}, ); - const payload = {action, uid: localUid, timestamp: Date.now()}; - events.send( - BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, - JSON.stringify(payload), - ); + // Show toast notification + showDeduplicatedToast(`host-operation-start-${hostUid}`, { + type: 'info', + text1: `${hostName} is managing breakout rooms`, + text2: 'Please wait for them to finish', + visibilityTime: 5000, + }); + setCurrentOperatingHostName(hostName); }, - [localUid], + [localUid, showDeduplicatedToast], ); - // Raised hand management functions - const addRaisedHand = useCallback((uid: UidType) => { - setRaisedHands(prev => { - // Check if already raised to avoid duplicates - const exists = prev.find(hand => hand.uid === uid); - if (exists) { - return prev; + const handleHostOperationEnd = useCallback( + (operationName: string, hostUid: UidType, hostName: string) => { + // Only process if current user is also a host and it's not their own event + console.log('supriya-state-sync host operation ended', operationName); + + if (!isHostRef.current || hostUid === localUid) { + return; } - return [...prev, {uid, timestamp: Date.now()}]; - }); - if (isHostRef.current) { - const userName = defaultContentRef.current[uid]?.name || `User ${uid}`; - Toast.show({ - leadingIconName: 'raise-hand', - type: 'info', - text1: `${userName} raised their hand`, - visibilityTime: 3000, - primaryBtn: null, - secondaryBtn: null, - leadingIcon: null, - }); - } - }, []); - const removeRaisedHand = useCallback((uid: UidType) => { - if (uid) { - setRaisedHands(prev => prev.filter(hand => hand.uid !== uid)); - } - if (isHostRef.current) { - const userName = defaultContentRef.current[uid]?.name || `User ${uid}`; - Toast.show({ - leadingIconName: 'raise-hand', - type: 'info', - text1: `${userName} lowered their hand`, - visibilityTime: 3000, - primaryBtn: null, - secondaryBtn: null, - leadingIcon: null, - }); - } - }, []); + setCurrentOperatingHostName(undefined); + }, + [localUid], + ); - const clearAllRaisedHands = useCallback(() => { - setRaisedHands([]); - }, []); - - // Handle incoming raise hand events (only host sees notifications) - const onRaiseHand = useCallback( - (action: 'raise' | 'lower', uid: UidType) => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - `Received raise hand event - ${action}`, - ); + // Debounced API for performance with multi-host coordination + const debouncedUpsertAPI = useDebouncedCallback( + async (type: 'START' | 'UPDATE', operationName?: string) => { + setBreakoutUpdateInFlight(true); try { - if (action === 'raise') { - addRaisedHand(uid); - } else if (action === 'lower') { - removeRaisedHand(uid); + console.log( + 'supriya-state-sync before calling upsertBreakoutRoomAPI 2007', + ); + + await upsertBreakoutRoomAPI(type); + console.log( + 'supriya-state-sync after calling upsertBreakoutRoomAPI 2007', + ); + console.log('supriya-state-sync operationName', operationName); + + // Broadcast operation end after successful API call + if (operationName) { + console.log( + 'supriya-state-sync broadcasting host operation end', + operationName, + ); + + broadcastHostOperationEnd(operationName); } } catch (error) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Error handling raise hand event', - {action, fromUserId: uid, error: error.message}, + 'API call failed. Reverting to previous state.', + error, ); + + // Broadcast operation end even on failure + if (operationName) { + broadcastHostOperationEnd(operationName); + } + + // // 🔁 Rollback to last valid state + // if ( + // prevStateRef.current && + // validateRollbackState(prevStateRef.current) + // ) { + // baseDispatch({ + // type: BreakoutGroupActionTypes.SYNC_STATE, + // payload: { + // sessionId: prevStateRef.current.breakoutSessionId, + // assignmentStrategy: prevStateRef.current.assignmentStrategy, + // switchRoom: prevStateRef.current.canUserSwitchRoom, + // rooms: prevStateRef.current.breakoutGroups, + // }, + // }); + // showDeduplicatedToast('breakout-api-failure', { + // type: 'error', + // text1: 'Sync failed. Reverted to previous state.', + // }); + // } else { + // showDeduplicatedToast('breakout-api-failure-no-rollback', { + // type: 'error', + // text1: 'Sync failed. Could not rollback safely.', + // }); + // } + } finally { + setBreakoutUpdateInFlight(false); } }, - [addRaisedHand, removeRaisedHand], + 500, ); - const handleBreakoutRoomSyncState = useCallback( - (payload: BreakoutRoomSyncStateEventPayload['data'], timestamp) => { + // Action-based API triggering + useEffect(() => { + if (!lastAction || !lastAction.type) { + return; + } + + // Actions that should trigger API calls + const API_TRIGGERING_ACTIONS = [ + BreakoutGroupActionTypes.CREATE_GROUP, + BreakoutGroupActionTypes.RENAME_GROUP, + BreakoutGroupActionTypes.CLOSE_GROUP, + BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, + BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, + BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, + BreakoutGroupActionTypes.EXIT_GROUP, + ]; + + // Host can always trigger API calls for any action + // Attendees can only trigger API when they self-join a room and switch_room is enabled + const attendeeSelfJoinAllowed = + stateRef.current.canUserSwitchRoom && + lastAction.type === BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP; + + const shouldCallAPI = + API_TRIGGERING_ACTIONS.includes(lastAction.type as any) && + (isHostRef.current || (!isHostRef.current && attendeeSelfJoinAllowed)); + + // Compute lastOperationName based on lastAction + const lastOperationName = HOST_BROADCASTED_OPERATIONS.includes( + lastAction?.type as any, + ) + ? lastAction?.type + : undefined; + + console.log( + 'supriya-state-sync shouldCallAPI', + shouldCallAPI, + lastAction.type, + lastOperationName, + ); + if (shouldCallAPI) { + debouncedUpsertAPI('UPDATE', lastOperationName); + } + }, [lastAction]); + + const _handleBreakoutRoomSyncState = useCallback( + async ( + payload: BreakoutRoomSyncStateEventPayload['data'], + timestamp: number, + ) => { console.log( 'supriya-api-sync response', timestamp, @@ -1929,26 +2021,16 @@ const BreakoutRoomProvider = ({ ); // Skip events older than the last processed timestamp - if (timestamp && timestamp <= lastProcessedTimestampRef.current) { + if (timestamp && timestamp <= lastSyncedTimestampRef.current) { console.log('supriya-api-sync Skipping old breakout room sync event', { timestamp, - lastProcessed: lastProcessedTimestampRef.current, + lastProcessed: lastSyncedTimestampRef.current, }); return; } const {srcuid, data} = payload; - console.log('supriya-event flow step 2', srcuid); - console.log('supriya-event uids', srcuid, localUid); - - // if (srcuid === localUid) { - // console.log('supriya-event flow skipping'); - - // return; - // } const {session_id, switch_room, breakout_room, assignment_type} = data; - console.log('supriya-state-sync new data: ', data); - console.log('supriya-state-sync old data: ', stateRef.current); logger.log( LogSource.Internals, @@ -1963,69 +2045,90 @@ const BreakoutRoomProvider = ({ }, ); - if (isAnotherHostOperating) { - setIsAnotherHostOperating(false); - setCurrentOperatingHostName(undefined); - } - // 🛡️ BEFORE snapshot - using stateRef to avoid stale closure - const prevGroups = stateRef.current.breakoutGroups; - console.log('supriya-event sync prevGroups: ', prevGroups); - const prevSwitchRoom = stateRef.current.canUserSwitchRoom; - - // Helpers to find membership - const findUserRoomId = (uid: UidType, groups: BreakoutGroup[] = []) => - groups.find(g => { - const hosts = Array.isArray(g?.participants?.hosts) - ? g.participants.hosts - : []; - const attendees = Array.isArray(g?.participants?.attendees) - ? g.participants.attendees - : []; - return hosts.includes(uid) || attendees.includes(uid); - })?.id ?? null; - - const prevRoomId = findUserRoomId(localUid, prevGroups); // before - console.log('supriya-event sync prevRoomId: ', prevRoomId); - + // Snapshot before applying + const prevSnapshot = lastSyncedSnapshotRef?.current; + const prevGroups = prevSnapshot?.breakout_room || []; + const prevSwitchRoom = prevSnapshot?.switch_room ?? true; + const prevRoomId = findUserRoomId(localUid, prevGroups); const nextRoomId = findUserRoomId(localUid, breakout_room); - console.log('supriya-event sync nextRoomId: ', nextRoomId); - // Show notifications based on changes - // 1. Switch room enabled notification + // 1. !prevRoomId && nextRoomId = Main → Breakout (joining) + // 2. prevRoomId && nextRoomId && prevRoomId !== nextRoomId = Breakout A → Breakout B (switching) + // 3. prevRoomId && !nextRoomId = Breakout → Main (leaving) + // 4. !prevRoomId && !nextRoomId = Main → Main (no change) + + const userMovedBetweenRooms = + prevRoomId && nextRoomId && prevRoomId !== nextRoomId; + const userLeftBreakoutRoom = prevRoomId && !nextRoomId; + + console.log( + 'supriya-sync-ordering prevRoomId nextRoomId and new breakout_room', + prevRoomId, + nextRoomId, + breakout_room, + ); + const senderName = getDisplayName(srcuid); - if (switch_room && !prevSwitchRoom) { - console.log('supriya-toast 1'); - showDeduplicatedToast('switch-room-toggle', { - leadingIconName: 'info', - type: 'info', - text1: `Host:${senderName} has opened breakout rooms.`, - text2: 'Please choose a room to join.', - visibilityTime: 3000, - }); - } - // 2. User joined a room (compare previous and current state) - // The notification for this comes from the main room channel_join event - if (prevRoomId === nextRoomId) { - // No logic + // ---- SCREEN SHARE CLEANUP ---- + // Stop screen share if user is moving between rooms or leaving breakout + // if ( + // (userMovedBetweenRooms || userLeftBreakoutRoom) && + // isScreenshareActive + // ) { + // console.log( + // 'supriya-sync-ordering: stopping screenshare due to room change', + // ); + // stopScreenshare(); + // } + + // ---- PRIORITY ORDER ---- + // 1. Room closed + if (breakout_room.length === 0 && prevGroups.length > 0) { + console.log('supriya-sync-ordering 1. all room closed: '); + // 1. User is in breakout toom and the exits + if (prevRoomId) { + showDeduplicatedToast('all-rooms-closed', { + leadingIconName: 'close-room', + type: 'info', + text1: `Host: ${senderName} has closed all breakout rooms.`, + text2: 'Returning to the main room...', + visibilityTime: 3000, + }); + // Set transition flag - user will remount in main room and need fresh data + sessionStorage.setItem('breakout_room_transition', 'true'); + return exitRoom(true); + } else { + // 2. User is in main room recevies just notification + showDeduplicatedToast('all-rooms-closed', { + leadingIconName: 'close-room', + type: 'info', + text1: `Host: ${senderName} has closed all breakout rooms`, + visibilityTime: 4000, + }); + } } - // 3. User was moved to main room - if (prevRoomId && !nextRoomId) { + + // 2. User's room deleted (they were in a room → now not) + if (userLeftBreakoutRoom) { + console.log('supriya-sync-ordering 2. they were in a room → now not: '); + const prevRoom = prevGroups.find(r => r.id === prevRoomId); - // Distinguish "room closed" vs "moved to main" const roomStillExists = breakout_room.some(r => r.id === prevRoomId); - + // Case A: Room deleted if (!roomStillExists) { showDeduplicatedToast(`current-room-closed-${prevRoomId}`, { - leadingIconName: 'alert', + leadingIconName: 'close-room', type: 'error', text1: `Host: ${senderName} has closed "${ prevRoom?.name || '' - }" room. `, + }" room.`, text2: 'Returning to main room...', visibilityTime: 3000, }); } else { + // Host removed user from room (handled here) + // (Room still exists for others, but you were unassigned) showDeduplicatedToast(`moved-to-main-${prevRoomId}`, { leadingIconName: 'arrow-up', type: 'info', @@ -2033,50 +2136,56 @@ const BreakoutRoomProvider = ({ visibilityTime: 3000, }); } - // Exit breakout room and return to main room - return exitRoom(prevRoomId, true); + + // Set transition flag - user will remount in main room and need fresh data + sessionStorage.setItem('breakout_room_transition', 'true'); + return exitRoom(true); } - // 5. All breakout rooms closed - if (breakout_room.length === 0 && prevGroups.length > 0) { - console.log('supriya-toast 5', prevRoomId, nextRoomId); + // 3. User moved between breakout rooms + if (userMovedBetweenRooms) { + console.log( + 'supriya-sync-ordering 3. user moved between breakout rooms', + ); + // const prevRoom = prevGroups.find(r => r.id === prevRoomId); + // const nextRoom = breakout_room.find(r => r.id === nextRoomId); + + // showDeduplicatedToast(`user-moved-${prevRoomId}-${nextRoomId}`, { + // leadingIconName: 'arrow-right', + // type: 'info', + // text1: `Host: ${senderName} has moved you to "${ + // nextRoom?.name || nextRoomId + // }".`, + // text2: `From "${prevRoom?.name || prevRoomId}"`, + // visibilityTime: 3000, + // }); + } - // Show different messages based on user's current location - if (prevRoomId) { - // User was in a breakout room - returning to main - showDeduplicatedToast('all-rooms-closed', { - leadingIconName: 'close', - type: 'info', - text1: `Host: ${senderName} has closed all breakout rooms.`, - text2: 'Returning to the main room...', - visibilityTime: 3000, - }); - return exitRoom(prevRoomId, true); - } else { - // User was already in main room - just notify about closure - showDeduplicatedToast('all-rooms-closed', { - leadingIconName: 'close', - type: 'info', - text1: `Host: ${senderName} has closed all breakout rooms`, - visibilityTime: 4000, - }); - return; - } + // 4. Rooms control switched + if (switch_room && !prevSwitchRoom) { + console.log('supriya-sync-ordering 4. switch_room changed: '); + showDeduplicatedToast('switch-room-toggle', { + leadingIconName: 'open-room', + type: 'info', + text1: `Host:${senderName} has opened breakout rooms.`, + text2: 'Please choose a room to join.', + visibilityTime: 3000, + }); } - // 6) Room renamed (compare per-room names) + // 5. Group renamed prevGroups.forEach(prevRoom => { const after = breakout_room.find(r => r.id === prevRoom.id); if (after && after.name !== prevRoom.name) { + console.log('supriya-sync-ordering 5. group renamed '); showDeduplicatedToast(`room-renamed-${after.id}`, { type: 'info', - text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`, + text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`, visibilityTime: 3000, }); } }); - // Finally, apply the authoritative state dispatch({ type: BreakoutGroupActionTypes.SYNC_STATE, payload: { @@ -2086,223 +2195,75 @@ const BreakoutRoomProvider = ({ rooms: breakout_room, }, }); - // Update the last processed timestamp after successful processing - lastProcessedTimestampRef.current = timestamp || Date.now(); - }, - [ - dispatch, - exitRoom, - localUid, - showDeduplicatedToast, - isAnotherHostOperating, - getDisplayName, - ], - ); - - // Multi-host coordination handlers - const handleHostOperationStart = useCallback( - (operationName: string, hostUid: UidType, hostName: string) => { - // Only process if current user is also a host and it's not their own event - console.log('supriya-state-sync host operation started', operationName); - if (!isHostRef.current || hostUid === localUid) { - return; - } - - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Another host started operation - locking UI', - {operationName, hostUid, hostName}, - ); - - setIsAnotherHostOperating(true); - setCurrentOperatingHostName(hostName); - - // Show toast notification - showDeduplicatedToast(`host-operation-start-${hostUid}`, { - type: 'info', - text1: `${hostName} is managing breakout rooms`, - text2: 'Please wait for them to finish', - visibilityTime: 5000, - }); - // Emergency timeout ONLY as last resort (30 seconds for network failures) - const timeoutId = safeSetTimeout(() => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'EMERGENCY: Auto-clearing host operation lock after extended timeout', - { - operationName, - hostUid, - hostName, - timeoutMs: HOST_OPERATION_LOCK_TIMEOUT_MS, - reason: 'Possible network failure or host disconnection', - }, - ); - setIsAnotherHostOperating(false); - setCurrentOperatingHostName(undefined); - hostOperationTimeoutRef.current = null; // Clear the ref since timeout fired - - showDeduplicatedToast(`host-operation-emergency-unlock-${hostUid}`, { - type: 'info', - text1: 'Breakout room controls unlocked', - text2: 'The other host may have disconnected', - visibilityTime: 4000, - }); - }, HOST_OPERATION_LOCK_TIMEOUT_MS); - - // Store the timeout ID so we can clear it if operation ends normally - hostOperationTimeoutRef.current = timeoutId; + // Store the snap of this + lastSyncedSnapshotRef.current = payload.data; + lastSyncedTimestampRef.current = timestamp || Date.now(); }, - [localUid, showDeduplicatedToast, safeSetTimeout], + [dispatch, exitRoom, localUid, showDeduplicatedToast, getDisplayName], ); - const handleHostOperationEnd = useCallback( - (operationName: string, hostUid: UidType, hostName: string) => { - // Only process if current user is also a host and it's not their own event - console.log('supriya-state-sync host operation ended', operationName); - - if (!isHostRef.current || hostUid === localUid) { - return; + /** + * While Event 1 is processing… + * Event 2 arrives (ts=200) and Event 3 arrives (ts=300). + * Both will overwrite latestTask: + * Now, queue.latestTask only holds event 3, because event 2 was replaced before it could be picked up. + */ + + const enqueueBreakoutSyncEvent = useCallback( + (payload: BreakoutRoomSyncStateEventPayload['data'], timestamp: number) => { + const queue = breakoutSyncQueueRef.current; + // Always keep the freshest event only + console.log('supriya-sync-queue 1', queue); + if ( + !queue.latestTask || + (timestamp && timestamp > queue.latestTask.timestamp) + ) { + console.log('supriya-sync-queue 2', queue); + queue.latestTask = {payload, timestamp}; } + console.log('supriya-sync-queue 3', queue.latestTask); - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Another host ended operation - unlocking UI', - {operationName, hostUid, hostName}, - ); - - setIsAnotherHostOperating(false); - setCurrentOperatingHostName(undefined); - - // Clear the emergency timeout since operation ended properly - if (hostOperationTimeoutRef.current) { - safeClearTimeout(hostOperationTimeoutRef.current); - hostOperationTimeoutRef.current = null; - } + processBreakoutSyncQueue(); }, - [localUid], + [], ); - // Debounced API for performance with multi-host coordination - const debouncedUpsertAPI = useDebouncedCallback( - async (type: 'START' | 'UPDATE', operationName?: string) => { - setBreakoutUpdateInFlight(true); - - try { - console.log( - 'supriya-state-sync before calling upsertBreakoutRoomAPI 2007', - ); + const processBreakoutSyncQueue = useCallback(async () => { + const queue = breakoutSyncQueueRef.current; + console.log('supriya-sync-queue 4', queue.latestTask); - await upsertBreakoutRoomAPI(type); - console.log( - 'supriya-state-sync after calling upsertBreakoutRoomAPI 2007', - ); - console.log('supriya-state-sync operationName', operationName); + // 1. If the queue is already being processed by another call, exit immediately. + if (queue.isProcessing) { + console.log('supriya-sync-queue 5 returning '); - // Broadcast operation end after successful API call - if (operationName) { - console.log( - 'supriya-state-sync broadcasting host operation end', - operationName, - ); - - broadcastHostOperationEnd(operationName); - } - } catch (error) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'API call failed. Reverting to previous state.', - error, - ); - - // Broadcast operation end even on failure - if (operationName) { - broadcastHostOperationEnd(operationName); - } - - // 🔁 Rollback to last valid state - if ( - prevStateRef.current && - validateRollbackState(prevStateRef.current) - ) { - baseDispatch({ - type: BreakoutGroupActionTypes.SYNC_STATE, - payload: { - sessionId: prevStateRef.current.breakoutSessionId, - assignmentStrategy: prevStateRef.current.assignmentStrategy, - switchRoom: prevStateRef.current.canUserSwitchRoom, - rooms: prevStateRef.current.breakoutGroups, - }, - }); - showDeduplicatedToast('breakout-api-failure', { - type: 'error', - text1: 'Sync failed. Reverted to previous state.', - }); - } else { - showDeduplicatedToast('breakout-api-failure-no-rollback', { - type: 'error', - text1: 'Sync failed. Could not rollback safely.', - }); - } - } finally { - setBreakoutUpdateInFlight(false); - } - }, - 500, - ); - - // Action-based API triggering - useEffect(() => { - if (!lastAction || !lastAction.type) { return; } - // Actions that should trigger API calls - const API_TRIGGERING_ACTIONS = [ - BreakoutGroupActionTypes.CREATE_GROUP, - BreakoutGroupActionTypes.RENAME_GROUP, - BreakoutGroupActionTypes.CLOSE_GROUP, - BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, - BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, - BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, - BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, - BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, - BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, - BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, - BreakoutGroupActionTypes.EXIT_GROUP, - ]; - - // Host can always trigger API calls for any action - // Attendees can only trigger API when they self-join a room and switch_room is enabled - const attendeeSelfJoinAllowed = - stateRef.current.canUserSwitchRoom && - lastAction.type === BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP; - - const shouldCallAPI = - API_TRIGGERING_ACTIONS.includes(lastAction.type as any) && - (isHostRef.current || (!isHostRef.current && attendeeSelfJoinAllowed)); - - // Compute lastOperationName based on lastAction - const lastOperationName = HOST_BROADCASTED_OPERATIONS.includes( - lastAction?.type as any, - ) - ? lastAction?.type - : undefined; - - console.log( - 'supriya-state-sync shouldCallAPI', - shouldCallAPI, - lastAction.type, - lastOperationName, - ); - if (shouldCallAPI) { - debouncedUpsertAPI('UPDATE', lastOperationName); + try { + // 2. "lock" the queue, so no second process can start. + queue.isProcessing = true; + console.log('supriya-sync-queue 6 lcoked '); + // 3. Loop the queue + while (queue.latestTask) { + const {payload, timestamp} = queue.latestTask; + console.log('supriya-sync-queue 7 ', payload, timestamp); + queue.latestTask = null; + + try { + await _handleBreakoutRoomSyncState(payload, timestamp); + } catch (err) { + console.error('[BreakoutSync] Error processing sync event', err); + // Continue processing other events even if one fails + } + } + } catch (err) { + console.error('[BreakoutSync] Critical error in queue processing', err); + } finally { + // Always unlock the queue, even if there's an error + queue.isProcessing = false; } - }, [lastAction]); + }, []); return ( = ({ - children, - mainChannelName, -}) => { +const BreakoutRoomEventsConfigure: React.FC = ({children}) => { const { onMakeMePresenter, handleBreakoutRoomSyncState, - onRaiseHand, handleHostOperationStart, handleHostOperationEnd, } = useBreakoutRoom(); @@ -33,7 +24,6 @@ const BreakoutRoomEventsConfigure: React.FC = ({ } = useRoomInfo(); const isHostRef = React.useRef(isHost); const localUidRef = React.useRef(localUid); - const onRaiseHandRef = useRef(onRaiseHand); const onMakeMePresenterRef = useRef(onMakeMePresenter); const handleBreakoutRoomSyncStateRef = useRef(handleBreakoutRoomSyncState); const handleHostOperationStartRef = useRef(handleHostOperationStart); @@ -45,9 +35,6 @@ const BreakoutRoomEventsConfigure: React.FC = ({ useEffect(() => { localUidRef.current = localUid; }, [localUid]); - useEffect(() => { - onRaiseHandRef.current = onRaiseHand; - }, [onRaiseHand]); useEffect(() => { onMakeMePresenterRef.current = onMakeMePresenter; }, [onMakeMePresenter]); @@ -81,55 +68,6 @@ const BreakoutRoomEventsConfigure: React.FC = ({ } catch (error) {} }; - const handleRaiseHandEvent = (evtData: any) => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'BREAKOUT_ROOM_ATTENDEE_RAISE_HAND event recevied', - evtData, - ); - try { - const {sender, payload} = evtData; - if (!isHostRef.current) { - return; - } - if (sender === `${localUidRef.current}`) { - return; - } - const data = JSON.parse(payload); - if (data.action === 'raise' || data.action === 'lower') { - onRaiseHandRef.current?.(data.action, data.uid); - } - } catch (error) {} - }; - - const handleAnnouncementEvent = (evtData: any) => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'BREAKOUT_ROOM_ANNOUNCEMENT event recevied', - evtData, - ); - try { - const {_, payload, sender} = evtData; - const data: BreakoutRoomAnnouncementEventPayload = JSON.parse(payload); - if (sender === `${localUidRef.current}`) { - return; - } - if (data.announcement) { - Toast.show({ - leadingIconName: 'speaker', - type: 'info', - text1: `Message from host: :${data.announcement}`, - visibilityTime: 3000, - primaryBtn: null, - secondaryBtn: null, - leadingIcon: null, - }); - } - } catch (error) {} - }; - const handleBreakoutRoomSyncStateEvent = (evtData: any) => { logger.log( LogSource.Internals, @@ -211,18 +149,14 @@ const BreakoutRoomEventsConfigure: React.FC = ({ } }; - events.on( - BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT, - handleAnnouncementEvent, - ); + // events.on( + // BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT, + // handleAnnouncementEvent, + // ); events.on( BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, handlePresenterStatusEvent, ); - events.on( - BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, - handleRaiseHandEvent, - ); events.on( BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, handleBreakoutRoomSyncStateEvent, @@ -237,15 +171,11 @@ const BreakoutRoomEventsConfigure: React.FC = ({ ); return () => { - events.off(BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT); + // events.off(BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT); events.off( BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, handlePresenterStatusEvent, ); - events.off( - BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, - handleRaiseHandEvent, - ); events.off( BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, handleBreakoutRoomSyncStateEvent, diff --git a/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx b/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx deleted file mode 100644 index 524465a7c..000000000 --- a/template/src/components/breakout-room/events/BreakoutRoomMainEventsConfigure.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, {useEffect} from 'react'; -import events from '../../../rtm-events-api'; -import {BreakoutRoomEventNames} from './constants'; -import {useBreakoutRoom} from '../context/BreakoutRoomContext'; -import {BreakoutRoomSyncStateEventPayload} from '../state/types'; - -interface Props { - children: React.ReactNode; -} - -const BreakoutRoomMainEventsConfigure: React.FC = ({children}) => { - const {onRaiseHand, handleBreakoutRoomSyncState} = useBreakoutRoom(); - - useEffect(() => { - const handleRaiseHandEvent = (evtData: any) => { - console.log( - 'supriya-event BREAKOUT_ROOM_ATTENDEE_RAISE_HAND data: ', - evtData, - ); - try { - const {payload} = evtData; - const data = JSON.parse(payload); - if (data.action === 'raise' || data.action === 'lower') { - onRaiseHand(data.action, data.uid); - } - } catch (error) {} - }; - - const handleBreakoutRoomSyncStateEvent = (evtData: any) => { - const {payload} = evtData; - console.log( - 'supriya-event BREAKOUT_ROOM_SYNC_STATE data (main): ', - evtData, - ); - const data: BreakoutRoomSyncStateEventPayload = JSON.parse(payload); - if (data.data.act === 'SYNC_STATE') { - handleBreakoutRoomSyncState(data.data.data); - } - }; - - events.on( - BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, - handleRaiseHandEvent, - ); - events.on( - BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, - handleBreakoutRoomSyncStateEvent, - ); - - return () => { - events.off( - BreakoutRoomEventNames.BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, - handleRaiseHandEvent, - ); - events.off( - BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, - handleBreakoutRoomSyncStateEvent, - ); - }; - }, [onRaiseHand, handleBreakoutRoomSyncState]); - - return <>{children}; -}; - -export default BreakoutRoomMainEventsConfigure; diff --git a/template/src/components/breakout-room/state/reducer.ts b/template/src/components/breakout-room/state/reducer.ts index e473d95a1..bb9e63a32 100644 --- a/template/src/components/breakout-room/state/reducer.ts +++ b/template/src/components/breakout-room/state/reducer.ts @@ -161,14 +161,14 @@ export type BreakoutRoomAction = | { type: typeof BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN; payload: { - user: ContentInterface; + uid: UidType; fromGroupId: string; }; } | { type: typeof BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP; payload: { - user: ContentInterface; + uid: UidType; fromGroupId: string; toGroupId: string; }; @@ -309,7 +309,7 @@ export const breakoutRoomReducer = ( // AUTO ASSIGN Simple round-robin assignment (no capacity limits) // Exclude local user from auto assignment const participantsToAssign = state.unassignedParticipants.filter( - participant => participant.uid !== action.payload.localUid + participant => participant.uid !== action.payload.localUid, ); let roomIndex = 0; @@ -358,12 +358,25 @@ export const breakoutRoomReducer = ( } case BreakoutGroupActionTypes.CREATE_GROUP: { + // Find the next available room number + const existingRoomNumbers = state.breakoutGroups + .map(room => { + const match = room.name.match(/^Room (\d+)$/); + return match ? parseInt(match[1], 10) : 0; + }) + .filter(num => num > 0); + + const nextRoomNumber = + existingRoomNumbers.length === 0 + ? 1 + : Math.max(...existingRoomNumbers) + 1; + return { ...state, breakoutGroups: [ ...state.breakoutGroups, { - name: `Room ${state.breakoutGroups.length + 1}`, + name: `Room ${nextRoomNumber}`, id: `temp_${randomNameGenerator(6)}`, participants: {hosts: [], attendees: []}, }, @@ -421,7 +434,7 @@ export const breakoutRoomReducer = ( } case BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN: { - const {user, fromGroupId} = action.payload; + const {uid, fromGroupId} = action.payload; return { ...state, breakoutGroups: state.breakoutGroups.map(group => { @@ -431,9 +444,9 @@ export const breakoutRoomReducer = ( ...group, participants: { ...group.participants, - hosts: group.participants.hosts.filter(id => id !== user.uid), + hosts: group.participants.hosts.filter(id => id !== uid), attendees: group.participants.attendees.filter( - id => id !== user.uid, + id => id !== uid, ), }, }; @@ -444,7 +457,17 @@ export const breakoutRoomReducer = ( } case BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP: { - const {user, fromGroupId, toGroupId} = action.payload; + const {uid, fromGroupId, toGroupId} = action.payload; + + // Determine if user was a host or attendee in their previous group + let wasHost = false; + if (fromGroupId) { + const sourceGroup = state.breakoutGroups.find( + group => group.id === fromGroupId, + ); + wasHost = sourceGroup?.participants.hosts.includes(uid) || false; + } + return { ...state, breakoutGroups: state.breakoutGroups.map(group => { @@ -454,25 +477,24 @@ export const breakoutRoomReducer = ( ...group, participants: { ...group.participants, - hosts: group.participants.hosts.filter(id => id !== user.uid), + hosts: group.participants.hosts.filter(id => id !== uid), attendees: group.participants.attendees.filter( - id => id !== user.uid, + id => id !== uid, ), }, }; } - // Add to target group + // Add to target group with same role as previous group if (group.id === toGroupId) { - const isHost = user.isHost === 'true'; return { ...group, participants: { ...group.participants, - hosts: isHost - ? [...group.participants.hosts, user.uid] + hosts: wasHost + ? [...group.participants.hosts, uid] : group.participants.hosts, - attendees: !isHost - ? [...group.participants.attendees, user.uid] + attendees: !wasHost + ? [...group.participants.attendees, uid] : group.participants.attendees, }, }; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx b/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx index 09820f26d..c613231ad 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomActionMenu.tsx @@ -100,7 +100,7 @@ const BreakoutRoomActionMenu: React.FC = ({ return ( <> { +const BreakoutRoomGroupSettings = ({scrollOffset}) => { const { - data: {isHost}, + data: {isHost, uid, chat}, } = useRoomInfo(); const localUid = useLocalUid(); + const {sendChatSDKMessage} = useChatConfigure(); + const {isUserHandRaised} = useRaiseHand(); const { breakoutGroups, @@ -42,21 +51,18 @@ const BreakoutRoomGroupSettings: React.FC = () => { exitRoom, joinRoom, closeRoom, - sendAnnouncement, updateRoomName, canUserSwitchRoom, - raisedHands, permissions, } = useBreakoutRoom(); const disableJoinBtn = !isHost && !canUserSwitchRoom; // Render room card - const {defaultContent} = useContent(); const {mainRoomRTMUsers} = useRTMGlobalState(); // Use hook to get display names with fallback to main room users const getDisplayName = useMainRoomUserDisplayName(); - const memberMoreMenuRefs = useRef<{[key: string]: any}>({}); + const { modalOpen: isAnnoucementModalOpen, setModalOpen: setAnnouncementModal, @@ -70,17 +76,6 @@ const BreakoutRoomGroupSettings: React.FC = () => { null, ); - const [actionMenuVisible, setActionMenuVisible] = useState<{ - [key: string]: boolean; - }>({}); - - const showModal = (memberUId: UidType) => { - setActionMenuVisible(prev => ({ - ...prev, - [memberUId]: !prev[memberUId], - })); - }; - const [expandedRooms, setExpandedRooms] = useState>(new Set()); const toggleRoomExpansion = (roomId: string) => { @@ -102,17 +97,6 @@ const BreakoutRoomGroupSettings: React.FC = () => { return null; } - // Create or get ref for this specific member - if (!memberMoreMenuRefs.current[memberUId]) { - memberMoreMenuRefs.current[memberUId] = React.createRef(); - } - - const memberRef = memberMoreMenuRefs.current[memberUId]; - const isMenuVisible = actionMenuVisible[memberUId] || false; - const hasRaisedHand = - permissions?.canSeeRaisedHands && - raisedHands.some(hand => hand.uid === memberUId); - return ( @@ -127,7 +111,7 @@ const BreakoutRoomGroupSettings: React.FC = () => { - {hasRaisedHand ? ( + {isUserHandRaised(memberUId) ? ( { )} {permissions.canHostManageMainRoom && memberUId !== localUid ? ( - - showModal(memberUId)} - /> - - - setActionMenuVisible(prev => ({ - ...prev, - [memberUId]: visible, - })) - } - user={defaultContent[memberUId]} - btnRef={memberRef} - from={'breakout-room'} - /> + ) : ( <> @@ -194,7 +156,17 @@ const BreakoutRoomGroupSettings: React.FC = () => { onPress={() => toggleRoomExpansion(room.id)} /> - {room.name} + + { + return {room.name}; + }} + /> + {memberCount > 0 ? memberCount : 'No'} Member {memberCount !== 1 ? 's' : ''} @@ -271,7 +243,18 @@ const BreakoutRoomGroupSettings: React.FC = () => { const onAnnouncement = (announcement: string) => { if (announcement) { - sendAnnouncement(announcement); + const option = { + chatType: SDKChatType.GROUP_CHAT, + type: ChatMessageType.TXT, + msg: `${announcement}`, + from: uid.toString(), + to: chat.group_id, + ext: { + from_platform: isWeb() ? 'web' : 'native', + isAnnouncementText: true, + }, + }; + sendChatSDKMessage(option); setAnnouncementModal(false); } }; @@ -408,15 +391,24 @@ const styles = StyleSheet.create({ fontWeight: '600', }, roomHeaderInfo: { + flex: 1, display: 'flex', flexDirection: 'column', gap: 4, }, + roomNameToolTipContainer: { + alignSelf: 'flex-start', + maxWidth: '100%', + }, roomName: { + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, fontSize: ThemeConfig.FontSize.small, lineHeight: 14, fontWeight: '600', + maxWidth: '100%', }, roomMemberCount: { color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, diff --git a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx index 3456a31d2..75c3ff927 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx @@ -13,7 +13,7 @@ interface MainRoomUser { name: string; } -const BreakoutRoomMainRoomUsers: React.FC = () => { +const BreakoutRoomMainRoomUsers = ({scrollOffset}) => { const {mainRoomRTMUsers} = useRTMGlobalState(); const {breakoutGroups, breakoutRoomVersion} = useBreakoutRoom(); const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)(); @@ -60,6 +60,8 @@ const BreakoutRoomMainRoomUsers: React.FC = () => { {mainRoomOnlyUsers.map(user => ( { return ( diff --git a/template/src/components/breakout-room/ui/BreakoutRoomMemberActionMenu.tsx b/template/src/components/breakout-room/ui/BreakoutRoomMemberActionMenu.tsx new file mode 100644 index 000000000..f474a15af --- /dev/null +++ b/template/src/components/breakout-room/ui/BreakoutRoomMemberActionMenu.tsx @@ -0,0 +1,122 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, {useState, useEffect, useRef} from 'react'; +import {View, useWindowDimensions} from 'react-native'; +import ActionMenu, {ActionMenuItem} from '../../../atoms/ActionMenu'; +import {calculatePosition} from '../../../utils/common'; +import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import {UidType} from '../../../../agora-rn-uikit'; +import IconButton from '../../../atoms/IconButton'; + +interface BreakoutRoomMemberActionMenuProps { + memberUid: UidType; +} + +const BreakoutRoomMemberActionMenu: React.FC< + BreakoutRoomMemberActionMenuProps +> = ({memberUid}) => { + const [actionMenuVisible, setActionMenuVisible] = useState(false); + const [isPosCalculated, setIsPosCalculated] = useState(false); + const [modalPosition, setModalPosition] = useState({}); + const {width: globalWidth, height: globalHeight} = useWindowDimensions(); + const moreIconRef = useRef(null); + + const {getRoomMemberDropdownOptions} = useBreakoutRoom(); + + // Get member-specific dropdown options using breakout room context + const memberOptions = getRoomMemberDropdownOptions(memberUid); + + // Transform to ActionMenuItem format + const actionMenuItems: ActionMenuItem[] = memberOptions.map( + (option, index) => ({ + order: index + 1, + icon: option.icon, + iconColor: $config.SECONDARY_ACTION_COLOR, + textColor: $config.SECONDARY_ACTION_COLOR, + title: option.title, + onPress: () => { + option.onOptionPress(); + setActionMenuVisible(false); + }, + }), + ); + + // Calculate position when menu becomes visible + useEffect(() => { + if (actionMenuVisible) { + moreIconRef?.current?.measure( + ( + _fx: number, + _fy: number, + localWidth: number, + localHeight: number, + px: number, + py: number, + ) => { + const data = calculatePosition({ + px, + py, + localWidth, + localHeight, + globalHeight, + globalWidth, + }); + setModalPosition(data); + setIsPosCalculated(true); + }, + ); + } + }, [actionMenuVisible]); + + // Don't render if no actions available + if (actionMenuItems.length === 0) { + return null; + } + + return ( + <> + + + { + setActionMenuVisible(true); + }} + /> + + + ); +}; + +export default BreakoutRoomMemberActionMenu; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx b/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx index c69dba7ef..26bfdad5c 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomRaiseHand.tsx @@ -1,19 +1,12 @@ -import React, {useState} from 'react'; +import React from 'react'; import {View, StyleSheet, Text} from 'react-native'; import ImageIcon from '../../../atoms/ImageIcon'; -import TertiaryButton from '../../../atoms/TertiaryButton'; import ThemeConfig from '../../../theme'; import {useBreakoutRoom} from '../context/BreakoutRoomContext'; +import {RaiseHandButton} from '../../raise-hand'; export default function BreakoutRoomRaiseHand() { - const {sendRaiseHandEvent, isUserInRoom} = useBreakoutRoom(); - const [isHandRaised, setIsHandRaised] = useState(false); - - const handleRaiseHand = () => { - const action = isHandRaised ? 'lower' : 'raise'; - sendRaiseHandEvent(action); - setIsHandRaised(!isHandRaised); - }; + const {isUserInRoom} = useBreakoutRoom(); return ( {!isUserInRoom() ? ( @@ -32,15 +25,7 @@ export default function BreakoutRoomRaiseHand() { <> )} - + ); @@ -77,16 +62,4 @@ const style = StyleSheet.create({ fontWeight: '400', lineHeight: 16, }, - raiseHandBtn: { - width: '100%', - borderRadius: 4, - borderColor: $config.SECONDARY_ACTION_COLOR, - backgroundColor: 'transparent', - }, - raiseHandBtnText: { - textAlign: 'center', - color: $config.SECONDARY_ACTION_COLOR, - fontSize: ThemeConfig.FontSize.small, - lineHeight: 16, - }, }); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx b/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx index c5d1046ef..5d2091473 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx @@ -16,7 +16,9 @@ export default function BreakoutRoomRenameModal( const {currentRoomName, setModalOpen, updateRoomName} = props; const [roomName, setRoomName] = React.useState(currentRoomName); - const disabled = roomName.trim() === ''; + const MAX_ROOM_NAME_LENGTH = 30; + const disabled = + roomName.trim() === '' || roomName.trim().length > MAX_ROOM_NAME_LENGTH; return ( Room name MAX_ROOM_NAME_LENGTH && + style.inputBoxError, + ]} value={roomName} onChangeText={setRoomName} placeholder="Rename room..." @@ -40,7 +46,23 @@ export default function BreakoutRoomRenameModal( $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low } underlineColorAndroid="transparent" + maxLength={50} /> + + MAX_ROOM_NAME_LENGTH && + style.characterCountError, + ]}> + {roomName.trim().length}/{MAX_ROOM_NAME_LENGTH} + + {roomName.trim().length > MAX_ROOM_NAME_LENGTH && ( + + Room name cannot exceed {MAX_ROOM_NAME_LENGTH} characters + + )} + @@ -79,7 +101,7 @@ const style = StyleSheet.create({ flexShrink: 0, width: '100%', maxWidth: 500, - height: 235, + height: 272, }, fullBody: { width: '100%', @@ -125,6 +147,29 @@ const style = StyleSheet.create({ backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR, outline: 'none', }, + inputBoxError: { + borderColor: $config.SEMANTIC_ERROR, + }, + inputFooter: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + marginTop: 4, + }, + characterCount: { + fontSize: ThemeConfig.FontSize.tiny, + fontWeight: '400', + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium, + }, + characterCountError: { + color: $config.SEMANTIC_ERROR, + }, + errorText: { + fontSize: ThemeConfig.FontSize.tiny, + fontWeight: '400', + color: $config.SEMANTIC_ERROR, + }, actionBtnText: { color: $config.SECONDARY_ACTION_COLOR, fontSize: ThemeConfig.FontSize.small, diff --git a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx index 0178006f8..85aa7d05f 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useState, useRef} from 'react'; import {View, StyleSheet, ScrollView} from 'react-native'; import {useRoomInfo} from '../../room-info/useRoomInfo'; import {useBreakoutRoom} from './../context/BreakoutRoomContext'; @@ -21,6 +21,13 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { data: {isHost}, } = useRoomInfo(); + const scrollViewRef = useRef(null); + const [scrollOffset, setScrollOffset] = useState(0); + + const onScroll = event => { + setScrollOffset(event.nativeEvent.contentOffset.y); + }; + const { checkIfBreakoutRoomSessionExistsAPI, createBreakoutRoomGroup, @@ -28,7 +35,6 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { closeAllRooms, permissions, isBreakoutUpdateInFlight, - isAnotherHostOperating, } = useBreakoutRoom(); useEffect(() => { @@ -36,7 +42,13 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { try { setIsInitializing(true); const activeSession = await checkIfBreakoutRoomSessionExistsAPI(); + console.log('supriya-sync-queue activeSession: ', activeSession); if (!activeSession && isHost) { + console.log( + 'supriya-sync-queue callubg upsertBreakoutRoomAPI: ', + activeSession, + ); + await upsertBreakoutRoomAPI('START'); } } catch (error) { @@ -49,12 +61,14 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { }, []); // Disable all actions when API is in flight or another host is operating - const disableAllActions = isBreakoutUpdateInFlight || isAnotherHostOperating; + const disableAllActions = isBreakoutUpdateInFlight; return ( <> ) : ( - + )} - + {permissions?.canHostManageMainRoom && permissions?.canCreateRooms ? ( { fileName: body?.fileName, replyToMsgId: body?.replyToMsgId, hide: false, + isAnnouncementText: body?.isAnnouncementText, }, ]; }); diff --git a/template/src/components/chat/chatConfigure.tsx b/template/src/components/chat/chatConfigure.tsx index 2a4a2cd69..b6f8694cb 100644 --- a/template/src/components/chat/chatConfigure.tsx +++ b/template/src/components/chat/chatConfigure.tsx @@ -255,7 +255,7 @@ const ChatConfigure = ({children}) => { ); } }, - // text message is recieved + // Text message is recieved, update receiver side ui onTextMessage: message => { if (message?.ext?.channel !== data?.channel) { return; @@ -285,6 +285,7 @@ const ChatConfigure = ({children}) => { isDeleted: false, type: ChatMessageType.TXT, replyToMsgId: message.ext?.replyToMsgId, + isAnnouncementText: message.ext?.isAnnouncementText || false, }); } @@ -418,6 +419,7 @@ const ChatConfigure = ({children}) => { ext: option?.ext?.file_ext, fileName: option?.ext?.file_name, replyToMsgId: option?.ext?.replyToMsgId, + isAnnouncementText: option?.ext?.isAnnouncementText, }; // update local user message store diff --git a/template/src/components/controls/useControlPermissionMatrix.tsx b/template/src/components/controls/useControlPermissionMatrix.tsx index 3beaea422..4b4302c35 100644 --- a/template/src/components/controls/useControlPermissionMatrix.tsx +++ b/template/src/components/controls/useControlPermissionMatrix.tsx @@ -44,7 +44,10 @@ export const controlPermissionMatrix: Record< $config.ENABLE_MEETING_TRANSCRIPT && $config.ENABLE_TEXT_TRACKS && isWeb(), - breakoutRoom: () => $config.ENABLE_BREAKOUT_ROOM && ENABLE_AUTH, + breakoutRoom: () => + $config.ENABLE_BREAKOUT_ROOM && + ENABLE_AUTH && + !$config.ENABLE_CONVERSATIONAL_AI, }; export const useControlPermissionMatrix = ( diff --git a/template/src/components/participants/UserActionMenuOptions.tsx b/template/src/components/participants/UserActionMenuOptions.tsx index 7bd9e3b3d..31ec81cdf 100644 --- a/template/src/components/participants/UserActionMenuOptions.tsx +++ b/template/src/components/participants/UserActionMenuOptions.tsx @@ -155,8 +155,6 @@ export default function UserActionMenuOptionsOptions( const moreBtnSpotlightLabel = useString(moreBtnSpotlight); const {chatConnectionStatus} = useChatUIControls(); const chatErrNotConnectedText = useString(chatErrorNotConnected)(); - const {getRoomMemberDropdownOptions, presenters: breakoutRoomPresenters} = - useBreakoutRoom(); useEffect(() => { customEvents.on('DisableChat', data => { @@ -176,25 +174,6 @@ export default function UserActionMenuOptionsOptions( useEffect(() => { const items: ActionMenuItem[] = []; - if (from === 'breakout-room' && $config.ENABLE_BREAKOUT_ROOM) { - const memberOptions = getRoomMemberDropdownOptions(user.uid); - // Transform to your UI format - const breakoutRoomMenuDropdowItems = memberOptions.map( - (option: MemberDropdownOption, index) => ({ - order: index + 1, - icon: option.icon, - iconColor: $config.SECONDARY_ACTION_COLOR, - textColor: $config.SECONDARY_ACTION_COLOR, - title: option.title, - onPress: () => { - setActionMenuVisible(false); - option?.onOptionPress(); - }, - }), - ); - setActionMenuitems(breakoutRoomMenuDropdowItems); - return; - } //Context of current user role const isSelf = user.uid === localuid; @@ -760,7 +739,6 @@ export default function UserActionMenuOptionsOptions( currentLayout, spotlightUid, from, - getRoomMemberDropdownOptions, ]); const {width: globalWidth, height: globalHeight} = useWindowDimensions(); diff --git a/template/src/components/raise-hand/RaiseHandButton.tsx b/template/src/components/raise-hand/RaiseHandButton.tsx new file mode 100644 index 000000000..a87fd7af7 --- /dev/null +++ b/template/src/components/raise-hand/RaiseHandButton.tsx @@ -0,0 +1,50 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React from 'react'; +import {useRaiseHand} from './RaiseHandProvider'; +import {StyleSheet} from 'react-native'; +import ThemeConfig from '../../theme'; +import TertiaryButton from '../../atoms/TertiaryButton'; + +const RaiseHandButton: React.FC = () => { + const {isHandRaised, toggleHand} = useRaiseHand(); + + return ( + + ); +}; + +export default RaiseHandButton; + +const style = StyleSheet.create({ + raiseHandBtn: { + width: '100%', + borderRadius: 4, + borderColor: $config.SECONDARY_ACTION_COLOR, + backgroundColor: 'transparent', + }, + raiseHandBtnText: { + textAlign: 'center', + color: $config.SECONDARY_ACTION_COLOR, + fontSize: ThemeConfig.FontSize.small, + lineHeight: 16, + }, +}); diff --git a/template/src/components/raise-hand/RaiseHandProvider.tsx b/template/src/components/raise-hand/RaiseHandProvider.tsx new file mode 100644 index 000000000..fa1b060d7 --- /dev/null +++ b/template/src/components/raise-hand/RaiseHandProvider.tsx @@ -0,0 +1,288 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import React, { + createContext, + useContext, + useState, + useEffect, + useCallback, +} from 'react'; +import {useCurrentRoomInfo} from '../room-info/useCurrentRoomInfo'; +import {useLocalUid} from '../../../agora-rn-uikit'; +import events, {PersistanceLevel} from '../../rtm-events-api'; +import Toast from '../../../react-native-toast-message'; +import {useMainRoomUserDisplayName} from '../../rtm/hooks/useMainRoomUserDisplayName'; +import {EventNames} from '../../rtm-events'; +import {useRoomInfo} from '../room-info/useRoomInfo'; +import {useBreakoutRoomInfo} from '../room-info/useSetBreakoutRoomInfo'; + +interface RaiseHandData { + raised: boolean; + timestamp: number; +} + +interface RaiseHandState { + // State + raisedHands: Record; + isHandRaised: boolean; + isUserHandRaised: (uid: number) => boolean; + + // Actions + raiseHand: () => void; + lowerHand: () => void; + toggleHand: () => void; +} + +const RaiseHandContext = createContext({ + raisedHands: {}, + isHandRaised: false, + isUserHandRaised: () => false, + raiseHand: () => {}, + lowerHand: () => {}, + toggleHand: () => {}, +}); + +interface RaiseHandProviderProps { + children: React.ReactNode; +} + +export const RaiseHandProvider: React.FC = ({ + children, +}) => { + const [raisedHands, setRaisedHands] = useState>( + {}, + ); + const localUid = useLocalUid(); + const getDisplayName = useMainRoomUserDisplayName(); + const { + data: {channel: mainChannelId}, + } = useRoomInfo(); + const {isInBreakoutRoute} = useCurrentRoomInfo(); + const {breakoutRoomChannelData} = useBreakoutRoomInfo(); + // Get current user's hand state + const isHandRaised = raisedHands[localUid]?.raised || false; + + // Check if any user has hand raised + const isUserHandRaised = useCallback( + (uid: number): boolean => { + return raisedHands[uid]?.raised || false; + }, + [raisedHands], + ); + + // Raise hand action + const raiseHand = useCallback(() => { + if (isHandRaised) { + return; + } // Already raised + + const timestamp = Date.now(); + const userName = getDisplayName(localUid) || `User ${localUid}`; + + // Update local state immediately + setRaisedHands(prev => ({...prev, [localUid]: {raised: true, timestamp}})); + + // 1. Send RTM attribute event (for current room UI) + events.send( + EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, + JSON.stringify({ + uid: localUid, + raised: true, + timestamp, + }), + PersistanceLevel.Sender, + ); + console.log('supriya-here outside', isInBreakoutRoute); + // 2. Send cross-room notification to main room (if in breakout room) + if (isInBreakoutRoute) { + try { + console.log('supriya-here inside', isInBreakoutRoute); + // Get current active channel to restore later + events.send( + EventNames.CROSS_ROOM_RAISE_HAND_NOTIFICATION, + JSON.stringify({ + type: 'raise_hand', + uid: localUid, + userName: userName, + roomName: breakoutRoomChannelData?.breakoutRoomName || '', + timestamp, + }), + PersistanceLevel.None, + -1, // send in channel + mainChannelId, // send to main channel + ); + } catch (error) { + console.error( + 'Failed to send cross-room raise hand notification:', + error, + ); + } + } + + // Show toast notification + Toast.show({ + type: 'success', + text1: 'Hand raised', + visibilityTime: 2000, + }); + }, [ + isHandRaised, + localUid, + getDisplayName, + isInBreakoutRoute, + mainChannelId, + ]); + + // Lower hand action + const lowerHand = useCallback(() => { + if (!isHandRaised) { + return; + } // Already lowered + + // Update local state immediately (keep timestamp but set raised to false) + setRaisedHands(prev => ({ + ...prev, + [localUid]: {raised: false, timestamp: Date.now()}, + })); + + // Send RTM event + events.send( + EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, + JSON.stringify({ + uid: localUid, + raised: false, + timestamp: Date.now(), + }), + PersistanceLevel.Sender, + ); + + // // Show toast notification + // Toast.show({ + // type: 'info', + // text1: 'Hand lowered', + // visibilityTime: 2000, + // }); + }, [isHandRaised, localUid]); + + // Toggle hand action + const toggleHand = useCallback(() => { + if (isHandRaised) { + lowerHand(); + } else { + raiseHand(); + } + }, [isHandRaised, raiseHand, lowerHand]); + + // Listen for RTM events + useEffect(() => { + const handleRaiseHandEvent = (data: any) => { + try { + const {payload} = data; + const eventData = JSON.parse(payload); + const {uid, raised, timestamp} = eventData; + console.log('supriya-here same room'); + // Update raised hands state + setRaisedHands(prev => ({ + ...prev, + [uid]: {raised, timestamp}, + })); + + // Show toast for other users (not for self) + if (uid !== localUid) { + const userName = getDisplayName(uid) || `User ${uid}`; + Toast.show({ + type: raised ? 'success' : 'info', + text1: raised + ? `${userName} raised hand` + : `${userName} lowered hand`, + visibilityTime: 3000, + }); + } + } catch (error) { + console.error('Failed to process raise hand event:', error); + } + }; + + const handleCrossRoomNotification = (data: any) => { + try { + const {payload} = data; + const eventData = JSON.parse(payload); + const {type, uid, userName, roomName} = eventData; + console.log('supriya-here cross room'); + + // Only show notifications for other users and only in main room + if (uid !== localUid && !isInBreakoutRoute) { + if (type === 'raise_hand') { + Toast.show({ + type: 'info', + text1: `${userName} raised hand in ${roomName}`, + visibilityTime: 4000, + }); + } + } + } catch (error) { + console.error('Failed to process cross-room notification:', error); + } + }; + + // Register event listeners + events.on(EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, handleRaiseHandEvent); + events.on( + EventNames.CROSS_ROOM_RAISE_HAND_NOTIFICATION, + handleCrossRoomNotification, + ); + + return () => { + // Cleanup event listeners + events.off( + EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, + handleRaiseHandEvent, + ); + events.off( + EventNames.CROSS_ROOM_RAISE_HAND_NOTIFICATION, + handleCrossRoomNotification, + ); + }; + }, [localUid, getDisplayName, isInBreakoutRoute]); + + // Clear raised hands when room changes (optional: could be handled by RTM attribute clearing) + useEffect(() => { + setRaisedHands({}); + }, []); + + const contextValue: RaiseHandState = { + raisedHands, + isHandRaised, + isUserHandRaised, + raiseHand, + lowerHand, + toggleHand, + }; + + return ( + + {children} + + ); +}; + +// Hook to use raise hand functionality +export const useRaiseHand = (): RaiseHandState => { + const context = useContext(RaiseHandContext); + if (!context) { + throw new Error('useRaiseHand must be used within RaiseHandProvider'); + } + return context; +}; + +export default RaiseHandProvider; diff --git a/template/src/components/raise-hand/index.ts b/template/src/components/raise-hand/index.ts new file mode 100644 index 000000000..9702b7c15 --- /dev/null +++ b/template/src/components/raise-hand/index.ts @@ -0,0 +1,14 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +export {default as RaiseHandProvider, useRaiseHand} from './RaiseHandProvider'; +export {default as RaiseHandButton} from './RaiseHandButton'; diff --git a/template/src/components/room-info/useCurrentRoomInfo.tsx b/template/src/components/room-info/useCurrentRoomInfo.tsx new file mode 100644 index 000000000..7c4132541 --- /dev/null +++ b/template/src/components/room-info/useCurrentRoomInfo.tsx @@ -0,0 +1,42 @@ +/* +******************************************** + Copyright © 2021 Agora Lab, Inc., all rights reserved. + AppBuilder and all associated components, source code, APIs, services, and documentation + (the "Materials") are owned by Agora Lab, Inc. and its licensors. The Materials may not be + accessed, used, modified, or distributed for any purpose without a license from Agora Lab, Inc. + Use without a license or in violation of any license terms and conditions (including use for + any purpose competitive to Agora Lab, Inc.'s business) is strictly prohibited. For more + information visit https://appbuilder.agora.io. +********************************************* +*/ + +import {useLocation} from '../Router'; +import {useRoomInfo} from './useRoomInfo'; +import {useBreakoutRoomInfo} from './useSetBreakoutRoomInfo'; + +export interface CurrentRoomInfoContextInterface { + isInBreakoutRoute: boolean; + channelId: string; +} + +export const useCurrentRoomInfo = (): CurrentRoomInfoContextInterface => { + const mainRoomInfo = useRoomInfo(); // Always call - keeps public API intact + const {breakoutRoomChannelData} = useBreakoutRoomInfo(); // Always call - follows Rules of Hooks + const location = useLocation(); + + const isBreakoutMode = + new URLSearchParams(location.search).get('breakout') === 'true'; + + // Return overlapping data structure + if (isBreakoutMode && breakoutRoomChannelData) { + return { + isInBreakoutRoute: true, + channelId: breakoutRoomChannelData.channelId, + }; + } + + return { + isInBreakoutRoute: false, + channelId: mainRoomInfo.data.channel, + }; +}; diff --git a/template/src/components/room-info/useSetBreakoutRoomInfo.tsx b/template/src/components/room-info/useSetBreakoutRoomInfo.tsx new file mode 100644 index 000000000..318fd4ff9 --- /dev/null +++ b/template/src/components/room-info/useSetBreakoutRoomInfo.tsx @@ -0,0 +1,67 @@ +import React, {createContext, useContext, useState} from 'react'; +import {BreakoutChannelJoinEventPayload} from '../breakout-room/state/types'; + +type BreakoutRoomData = { + isBreakoutMode: boolean; + channelId: string; + breakoutRoomName: string; +}; + +interface BreakoutRoomInfoContextValue { + breakoutRoomChannelData: BreakoutRoomData | null; + setBreakoutRoomChannelData: React.Dispatch< + React.SetStateAction + >; +} + +const BreakoutRoomInfoContext = createContext({ + breakoutRoomChannelData: null, + setBreakoutRoomChannelData: () => {}, +}); + +interface BreakoutRoomInfoProviderProps { + children: React.ReactNode; + initialData?: BreakoutRoomData | null; +} + +export const BreakoutRoomInfoProvider: React.FC< + BreakoutRoomInfoProviderProps +> = ({children, initialData = null}) => { + const [breakoutRoomChannelData, setBreakoutRoomChannelData] = + useState(initialData); + + return ( + + {children} + + ); +}; + +export const useBreakoutRoomInfo = () => { + return useContext(BreakoutRoomInfoContext); +}; + +export const useSetBreakoutRoomInfo = () => { + const {setBreakoutRoomChannelData} = useBreakoutRoomInfo(); + + const setBreakoutRoomChannelInfo = ( + payload: BreakoutChannelJoinEventPayload, + ) => { + const breakoutData: BreakoutRoomData = { + breakoutRoomName: payload.data.data.room_name, + channelId: payload.data.data.channel_name, + isBreakoutMode: true, + }; + setBreakoutRoomChannelData(breakoutData); + }; + + const clearBreakoutRoomChannelInfo = () => { + setBreakoutRoomChannelData(null); + }; + + return { + setBreakoutRoomChannelInfo, + clearBreakoutRoomChannelInfo, + }; +}; diff --git a/template/src/language/default-labels/videoCallScreenLabels.ts b/template/src/language/default-labels/videoCallScreenLabels.ts index daf245daf..d68d18a1c 100644 --- a/template/src/language/default-labels/videoCallScreenLabels.ts +++ b/template/src/language/default-labels/videoCallScreenLabels.ts @@ -884,7 +884,7 @@ export const VideoCallScreenLabels: I18nVideoCallScreenLabelsInterface = { [toolbarItemSettingText]: 'Settings', [toolbarItemLayoutText]: 'Layout', [toolbarItemInviteText]: 'Invite', - [toolbarItemBreakoutRoomText]: 'Create Breakout Rooms', + [toolbarItemBreakoutRoomText]: 'Breakout Rooms', [toolbarItemMicrophoneText]: deviceStatus => { switch (deviceStatus) { diff --git a/template/src/pages/VideoCall.tsx b/template/src/pages/VideoCall.tsx index 625b69247..a53fd9845 100644 --- a/template/src/pages/VideoCall.tsx +++ b/template/src/pages/VideoCall.tsx @@ -80,6 +80,7 @@ import {LogSource, logger} from '../logger/AppBuilderLogger'; import {useCustomization} from 'customization-implementation'; import {BeautyEffectProvider} from '../components/beauty-effect/useBeautyEffects'; import {UserActionMenuProvider} from '../components/useUserActionMenu'; +import {RaiseHandProvider} from '../components/raise-hand'; import Toast from '../../react-native-toast-message'; import {AuthErrorCodes} from '../utils/common'; import {BreakoutRoomProvider} from '../components/breakout-room/context/BreakoutRoomContext'; @@ -162,7 +163,7 @@ const VideoCall = (videoCallProps: VideoCallProps) => { + currentChannel={rtcProps.channel}> @@ -209,20 +210,22 @@ const VideoCall = (videoCallProps: VideoCallProps) => { - - + - - - + + + + + diff --git a/template/src/pages/video-call/BreakoutVideoCallContent.tsx b/template/src/pages/video-call/BreakoutVideoCall.tsx similarity index 89% rename from template/src/pages/video-call/BreakoutVideoCallContent.tsx rename to template/src/pages/video-call/BreakoutVideoCall.tsx index 04ca6c957..82960751d 100644 --- a/template/src/pages/video-call/BreakoutVideoCallContent.tsx +++ b/template/src/pages/video-call/BreakoutVideoCall.tsx @@ -9,7 +9,7 @@ information visit https://appbuilder.agora.io. ********************************************* */ -import React, {useState, useEffect, SetStateAction, useMemo} from 'react'; +import React, {useState, useEffect} from 'react'; import { RtcConfigure, PropsProvider, @@ -48,7 +48,9 @@ import {ChatMessagesProvider} from '../../components/chat-messages/useChatMessag import VideoCallScreenWrapper from './../video-call/VideoCallScreenWrapper'; import {BeautyEffectProvider} from '../../components/beauty-effect/useBeautyEffects'; import {UserActionMenuProvider} from '../../components/useUserActionMenu'; +import {RaiseHandProvider} from '../../components/raise-hand'; import {BreakoutRoomProvider} from '../../components/breakout-room/context/BreakoutRoomContext'; +import {useBreakoutRoomInfo} from '../../components/room-info/useSetBreakoutRoomInfo'; import { BreakoutChannelDetails, VideoCallContentProps, @@ -56,13 +58,13 @@ import { import BreakoutRoomEventsConfigure from '../../components/breakout-room/events/BreakoutRoomEventsConfigure'; import {RTM_ROOMS} from '../../rtm/constants'; -interface BreakoutVideoCallContentProps extends VideoCallContentProps { +interface BreakoutVideoCallProps extends VideoCallContentProps { rtcProps: RtcPropsInterface; breakoutChannelDetails: BreakoutChannelDetails; onLeave: () => void; } -const BreakoutVideoCallContent: React.FC = ({ +const BreakoutVideoCall: React.FC = ({ rtcProps, breakoutChannelDetails, onLeave, @@ -70,6 +72,7 @@ const BreakoutVideoCallContent: React.FC = ({ callbacks, styleProps, }) => { + const {setBreakoutRoomChannelData} = useBreakoutRoomInfo(); const [isRecordingActive, setRecordingActive] = useState(false); const [sttAutoStarted, setSttAutoStarted] = useState(false); const [recordingAutoStarted, setRecordingAutoStarted] = useState(false); @@ -83,6 +86,14 @@ const BreakoutVideoCallContent: React.FC = ({ screenShareToken: breakoutChannelDetails?.screenShareToken || '', }); + // Set breakout room data when component mounts + useEffect(() => { + setBreakoutRoomChannelData({ + channelId: breakoutChannelDetails.channel, + isBreakoutMode: true, + }); + }, [breakoutChannelDetails.channel, setBreakoutRoomChannelData]); + return ( = ({ + currentChannel={breakoutRoomRTCProps.channel}> @@ -151,20 +162,22 @@ const BreakoutVideoCallContent: React.FC = ({ - - + - - - + + + + + @@ -199,4 +212,4 @@ const BreakoutVideoCallContent: React.FC = ({ ); }; -export default BreakoutVideoCallContent; +export default BreakoutVideoCall; diff --git a/template/src/pages/video-call/VideoCallContent.tsx b/template/src/pages/video-call/VideoCallContent.tsx index 745b8b44f..81b8594ca 100644 --- a/template/src/pages/video-call/VideoCallContent.tsx +++ b/template/src/pages/video-call/VideoCallContent.tsx @@ -16,11 +16,15 @@ import events from '../../rtm-events-api'; import {BreakoutChannelJoinEventPayload} from '../../components/breakout-room/state/types'; import {CallbacksInterface, RtcPropsInterface} from '../../../agora-rn-uikit'; import VideoCall from '../VideoCall'; -import BreakoutVideoCallContent from './BreakoutVideoCallContent'; +import BreakoutVideoCall from './BreakoutVideoCall'; import {BreakoutRoomEventNames} from '../../components/breakout-room/events/constants'; import BreakoutRoomTransition from '../../components/breakout-room/ui/BreakoutRoomTransition'; import Toast from '../../../react-native-toast-message'; import {useMainRoomUserDisplayName} from '../../rtm/hooks/useMainRoomUserDisplayName'; +import { + useSetBreakoutRoomInfo, + BreakoutRoomInfoProvider, +} from '../../components/room-info/useSetBreakoutRoomInfo'; export interface BreakoutChannelDetails { channel: string; @@ -45,6 +49,10 @@ const VideoCallContent: React.FC = props => { const location = useLocation(); const history = useHistory(); + // Room Info: + const {setBreakoutRoomChannelInfo, clearBreakoutRoomChannelInfo} = + useSetBreakoutRoomInfo(); + // Parse URL to determine current mode const searchParams = new URLSearchParams(location.search); const isBreakoutMode = searchParams.get('breakout') === 'true'; @@ -78,6 +86,11 @@ const VideoCallContent: React.FC = props => { if (data?.data?.act === 'CHAN_JOIN') { const {channel_name, mainUser, screenShare, chat, room_name} = data.data.data; + + // Set transition flag - component will unmount/remount when entering breakout + sessionStorage.setItem('breakout_room_transition', 'true'); + console.log('Set breakout transition flag for channel join'); + // Extract breakout channel details const breakoutDetails: BreakoutChannelDetails = { channel: channel_name, @@ -87,6 +100,8 @@ const VideoCallContent: React.FC = props => { screenShareUid: screenShare.uid, rtmToken: mainUser.rtm, }; + // Set breakout room info using the new system + setBreakoutRoomChannelInfo(data); // Set breakout state active history.push(`/${phrase}?breakout=true`); setBreakoutChannelDetails(null); @@ -109,6 +124,7 @@ const VideoCallContent: React.FC = props => { } setTimeout(() => { Toast.show({ + leadingIconName: 'open-room', type: 'success', text1: joinMessage, visibilityTime: 3000, @@ -133,7 +149,13 @@ const VideoCallContent: React.FC = props => { handleBreakoutJoin, ); }; - }, [phrase, getDisplayName, mainRoomLocalUid]); + }, [ + phrase, + getDisplayName, + mainRoomLocalUid, + setBreakoutRoomChannelInfo, + history, + ]); // Cleanup on unmount useEffect(() => { @@ -147,6 +169,10 @@ const VideoCallContent: React.FC = props => { // Handle leaving breakout room const handleLeaveBreakout = useCallback(() => { console.log('Leaving breakout room, returning to main room'); + + // Clear breakout room info to return to main room + clearBreakoutRoomChannelInfo(); + // Set direction for exiting setTransitionDirection('exit'); // Clear breakout channel details to show transition @@ -155,7 +181,7 @@ const VideoCallContent: React.FC = props => { setTimeout(() => { history.push(`/${phrase}`); }, 800); - }, [history, phrase]); + }, [history, phrase, clearBreakoutRoomChannelInfo]); // Route protection: Prevent direct navigation to breakout route useEffect(() => { @@ -177,12 +203,14 @@ const VideoCallContent: React.FC = props => { {isBreakoutMode ? ( breakoutChannelDetails?.channel ? ( // Breakout Room Mode - Fresh component instance - + + + ) : ( { + private _persist = async (evt: string, payload: string, roomKey?: string) => { const rtmEngine: RTMClient = RTMEngine.getInstance().engine; const userId = RTMEngine.getInstance().localUid; try { + // const roomAwareKey = roomKey ? `${roomKey}__${evt}` : evt; + // console.log( + // 'session-attributes setting roomAwareKey as: ', + // roomAwareKey, + // evt, + // ); const rtmAttribute = {key: evt, value: payload}; // Step 1: Call RTM API to update local attributes await rtmEngine.storage.setUserMetadata( @@ -110,7 +131,7 @@ class Events { private _send = async ( rtmPayload: RTMAttributePayload, toUid?: ReceiverUid, - channelId?: string, + toChannelId?: string, ) => { const to = typeof toUid === 'string' ? parseInt(toUid, 10) : toUid; @@ -133,27 +154,18 @@ class Events { 'case 1 executed - sending in channel', ); try { - const targetChannelId = - channelId || RTMEngine.getInstance().getActiveChannel(); - console.log('supriya targetChannelId', targetChannelId); - logger.debug( - LogSource.Events, - 'CUSTOM_EVENTS', - 'event is sent to targetChannelId ->', - targetChannelId, - ); logger.debug( LogSource.Events, 'CUSTOM_EVENTS', 'event is sent to targetChannelId ->', - targetChannelId, + toChannelId, ); - if (!targetChannelId || targetChannelId.trim() === '') { + if (!toChannelId || toChannelId.trim() === '') { throw new Error( 'Channel ID is not set. Cannot send channel messages.', ); } - await rtmEngine.publish(targetChannelId, text, { + await rtmEngine.publish(toChannelId, text, { channelType: nativeChannelTypeMapping.MESSAGE, // 1 is message }); } catch (error) { @@ -235,7 +247,7 @@ class Events { private _sendAsChannelAttribute = async ( rtmPayload: RTMAttributePayload, - channelId?: string, + toChannelId?: string, ) => { // Case 1: send to channel logger.debug( @@ -250,14 +262,17 @@ class Events { } const rtmEngine: RTMClient = RTMEngine.getInstance().engine; - const targetChannelId = RTMEngine.getInstance().getActiveChannel(); - if (!targetChannelId || targetChannelId.trim() === '') { + if (!toChannelId) { throw new Error('Channel ID is not set. Cannot send channel messages.'); } const rtmAttribute = [{key: rtmPayload.evt, value: rtmPayload.value}]; + console.log( + 'supriya-channel-attrbiutes setting channel attrbiytes: ', + rtmAttribute, + ); await rtmEngine.storage.setChannelMetadata( - targetChannelId, + toChannelId, nativeChannelTypeMapping.MESSAGE, { items: rtmAttribute, @@ -370,7 +385,7 @@ class Events { payload: string = '', persistLevel: PersistanceLevel = PersistanceLevel.None, receiver: ReceiverUid = -1, - channelId?: string, + toChannelId?: string, ) => { try { if (!this._validateEvt(eventName)) { @@ -386,12 +401,18 @@ class Events { return; // Don't throw - just log and return } + // Add meta data + let currentEventScope = getRTMEventScope(eventName); + let currentChannelId = RTMEngine.getInstance().getActiveChannelId(); + let currentRoomKey = RTMEngine.getInstance().getActiveChannelName(); + const persistValue = JSON.stringify({ payload, persistLevel, source: this.source, + _scope: currentEventScope, + _channelId: currentChannelId, }); - const rtmPayload: RTMAttributePayload = { evt: eventName, value: persistValue, @@ -402,7 +423,13 @@ class Events { persistLevel === PersistanceLevel.Session ) { try { - await this._persist(eventName, persistValue); + await this._persist( + eventName, + persistValue, + persistLevel === PersistanceLevel.Session + ? currentRoomKey + : undefined, + ); } catch (error) { logger.error(LogSource.Events, 'CUSTOM_EVENTS', 'persist error', error); // don't throw - just log the error, application should continue running @@ -415,10 +442,11 @@ class Events { `sending event -> ${eventName}`, persistValue, ); + const targetChannelId = toChannelId || currentChannelId; if (persistLevel === PersistanceLevel.Channel) { - await this._sendAsChannelAttribute(rtmPayload, channelId); + await this._sendAsChannelAttribute(rtmPayload, targetChannelId); } else { - await this._send(rtmPayload, receiver, channelId); + await this._send(rtmPayload, receiver, targetChannelId); } } catch (error) { logger.error( diff --git a/template/src/rtm-events/constants.ts b/template/src/rtm-events/constants.ts index 04fbfadb2..70ffe9db6 100644 --- a/template/src/rtm-events/constants.ts +++ b/template/src/rtm-events/constants.ts @@ -1,4 +1,7 @@ /** ***** EVENTS ACTIONS BEGINS***** */ + +import {BreakoutRoomEventNames} from '../components/breakout-room/events/constants'; + // 1. SCREENSHARE const SCREENSHARE_STARTED = 'SCREENSHARE_STARTED'; const SCREENSHARE_STOPPED = 'SCREENSHARE_STOPPED'; @@ -16,6 +19,7 @@ const RECORDING_STATE_ATTRIBUTE = 'recording_state'; const RECORDING_STARTED_BY_ATTRIBUTE = 'recording_started_by'; // 2. SCREENSHARE const SCREENSHARE_ATTRIBUTE = 'screenshare'; + // 2. LIVE STREAMING const RAISED_ATTRIBUTE = 'raised'; const ROLE_ATTRIBUTE = 'role'; @@ -40,6 +44,11 @@ const BOARD_COLOR_CHANGED = 'BOARD_COLOR_CHANGED'; const WHITEBOARD_LAST_IMAGE_UPLOAD_POSITION = 'WHITEBOARD_L_I_U_P'; const RECORDING_DELETED = 'RECORDING_DELETED'; const SPOTLIGHT_USER_CHANGED = 'SPOTLIGHT_USER_CHANGED'; +// 9. General raise hand +// Later on we will only have one raise hand i.e which will tied with livestream ad breakout +const BREAKOUT_RAISE_HAND_ATTRIBUTE = 'breakout_raise_hand'; +// 10. Cross-room raise hand notifications (messages, not attributes) +const CROSS_ROOM_RAISE_HAND_NOTIFICATION = 'cross_room_raise_hand_notification'; const EventNames = { RECORDING_STATE_ATTRIBUTE, @@ -62,7 +71,33 @@ const EventNames = { WHITEBOARD_LAST_IMAGE_UPLOAD_POSITION, RECORDING_DELETED, SPOTLIGHT_USER_CHANGED, + BREAKOUT_RAISE_HAND_ATTRIBUTE, + CROSS_ROOM_RAISE_HAND_NOTIFICATION, }; /** ***** EVENT NAMES ENDS ***** */ -export {EventActions, EventNames}; +/** SCOPE OF EVENTS */ +const RTM_GLOBAL_SCOPE_EVENTS = [ + EventNames.NAME_ATTRIBUTE, + EventNames.CROSS_ROOM_RAISE_HAND_NOTIFICATION, + BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, +]; +const RTM_SESSION_SCOPE_EVENTS = []; +// const RTM_SESSION_SCOPE_EVENTS = [ +// EventNames.RECORDING_STATE_ATTRIBUTE, +// EventNames.RECORDING_STARTED_BY_ATTRIBUTE, +// ]; + +enum RTM_EVENT_SCOPE { + GLOBAL = 'GLOBAL', // These event-attributes dont change ex: name, screenuid even when room chanes (main -> breakout) + SESSION = 'SESSION', // These event-attributes are stored as per channel but there needs to be persistance..when user returns to main room..he should have the state of that channel ex: recording, whiteboard active + LOCAL = 'LOCAL', // These event-attributes are specific to channel and can be reseted or removed, ex: raise_hand, screenshare +} + +export { + EventActions, + EventNames, + RTM_GLOBAL_SCOPE_EVENTS, + RTM_EVENT_SCOPE, + RTM_SESSION_SCOPE_EVENTS, +}; diff --git a/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx b/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx index f92877706..b84b81581 100644 --- a/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx +++ b/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx @@ -19,22 +19,18 @@ import React, { useCallback, } from 'react'; import { - type GetChannelMetadataResponse, - type GetOnlineUsersResponse, type MessageEvent, type PresenceEvent, type SetOrUpdateUserMetadataOptions, type StorageEvent, - type GetUserMetadataResponse, + type RTMClient, } from 'agora-react-native-rtm'; import { ContentInterface, DispatchContext, useLocalUid, } from '../../agora-rn-uikit'; -import ChatContext from '../components/ChatContext'; import {Platform} from 'react-native'; -import {backOff} from 'exponential-backoff'; import {isAndroid, isIOS} from '../utils/common'; import {useContent} from 'customization-api'; import { @@ -43,12 +39,17 @@ import { hasJsonStructure, getMessageTime, get32BitUid, + isEventForActiveChannel, } from '../rtm/utils'; +import { + fetchChannelAttributesWithRetries, + clearRoomScopedUserAttributes, + processUserAttributeForQueue, +} from './rtm-presence-utils'; import {EventUtils, EventsQueue} from '../rtm-events'; import {PersistanceLevel} from '../rtm-events-api'; import RTMEngine from '../rtm/RTMEngine'; import {filterObject} from '../utils'; -import SDKEvents from '../utils/SdkEvents'; import {useAsyncEffect} from '../utils/useAsyncEffect'; import { WaitingRoomStatus, @@ -66,11 +67,19 @@ import { nativeStorageEventTypeMapping, } from '../../bridge/rtm/web/Types'; import {useRTMCore} from '../rtm/RTMCoreProvider'; -import {RTM_ROOMS} from './constants'; +import { + RTM_ROOMS, + RTM_EVENTS_ATTRIBUTES_TO_RESET_WHEN_ROOM_CHANGES, +} from './constants'; import {useUserGlobalPreferences} from '../components/UserGlobalPreferenceProvider'; import {ToggleState} from '../../agora-rn-uikit'; import useMuteToggleLocal from '../utils/useMuteToggleLocal'; import {useRtc} from 'customization-api'; +import { + fetchOnlineMembersWithRetries, + fetchUserAttributesWithRetries, + mapUserAttributesToState, +} from './rtm-presence-utils'; export enum UserType { ScreenShare = 'screenshare', @@ -108,7 +117,7 @@ export const useRTMConfigureBreakout = () => { interface RTMConfigureBreakoutRoomProviderProps { callActive: boolean; children: React.ReactNode; - channelName: string; + currentChannel: string; } const RTMConfigureBreakoutRoomProvider = ( @@ -116,7 +125,7 @@ const RTMConfigureBreakoutRoomProvider = ( ) => { const rtmInitTimstamp = new Date().getTime(); const localUid = useLocalUid(); - const {callActive, channelName} = props; + const {callActive, currentChannel} = props; const {dispatch} = useContext(DispatchContext); const {defaultContent, activeUids} = useContent(); const { @@ -129,16 +138,32 @@ const RTMConfigureBreakoutRoomProvider = ( const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); const [onlineUsersCount, setTotalOnlineUsers] = useState(0); - const timerValueRef: any = useRef(5); + // Track RTM connection state (equivalent to v1.5x connectionState check) const {client, isLoggedIn, registerCallbacks, unregisterCallbacks} = useRTMCore(); const {rtcTracksReady} = useRtc(); /** - * inside event callback state won't have latest value. - * so creating ref to access the state + * Refs */ + + const isRTMMounted = useRef(true); + + const hasInitRef = useRef(false); + const subscribeTimerRef: any = useRef(5); + const subscribeTimeoutRef = useRef | null>( + null, + ); + + const channelAttributesTimerRef: any = useRef(5); + const channelAttributesTimeoutRef = useRef | null>(null); + + const membersTimerRef: any = useRef(5); + const membersTimeoutRef = useRef | null>(null); + const isHostRef = useRef({isHost: isHost}); useEffect(() => { isHostRef.current.isHost = isHost; @@ -159,19 +184,6 @@ const RTMConfigureBreakoutRoomProvider = ( defaultContentRef.current = defaultContent; }, [defaultContent]); - // Eventdispatcher timeout refs clean - const isRTMMounted = useRef(true); - useEffect(() => { - return () => { - isRTMMounted.current = false; - // Clear all pending timeouts on unmount - for (const timeout of eventTimeouts.values()) { - clearTimeout(timeout); - } - eventTimeouts.clear(); - }; - }, []); - // Apply user preferences when breakout room mounts useEffect(() => { if (rtcTracksReady) { @@ -214,10 +226,12 @@ const RTMConfigureBreakoutRoomProvider = ( ), ).length, ); - }, [defaultContent]); + }, [defaultContent, activeUids]); const init = async () => { await subscribeChannel(); + await getMembersWithAttributes(); + await getChannelAttributes(); setHasUserJoinedRTM(true); await runQueuedEvents(); setIsInitialQueueCompleted(true); @@ -226,50 +240,34 @@ const RTMConfigureBreakoutRoomProvider = ( const subscribeChannel = async () => { try { - if (RTMEngine.getInstance().allChannels.includes(channelName)) { + if (RTMEngine.getInstance().allChannelIds.includes(currentChannel)) { logger.debug( LogSource.AgoraSDK, 'Log', '🚫 RTM already subscribed channel skipping', - channelName, + currentChannel, ); } else { - await client.subscribe(channelName, { + await client.subscribe(currentChannel, { withMessage: true, withPresence: true, withMetadata: true, withLock: false, }); logger.log(LogSource.AgoraSDK, 'API', 'RTM subscribeChannel', { - data: channelName, + data: currentChannel, }); // Set channel ID AFTER successful subscribe (like v1.5x) - console.log('setting primary channel', channelName); - RTMEngine.getInstance().addChannel(RTM_ROOMS.BREAKOUT, channelName); - RTMEngine.getInstance().setActiveChannel(RTM_ROOMS.BREAKOUT); - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM setChannelId as subscribe is successful', - channelName, - ); - logger.debug( - LogSource.SDK, - 'Event', - 'Emitting rtm joined', - channelName, - ); - // @ts-ignore - SDKEvents.emit('_rtm-joined', channelName); - timerValueRef.current = 5; - await getMembers(); - await readAllChannelAttributes(); - logger.log( - LogSource.AgoraSDK, - 'Log', - 'RTM readAllChannelAttributes and getMembers done', - ); + console.log('setting primary channel', currentChannel); + RTMEngine.getInstance().addChannel(RTM_ROOMS.BREAKOUT, currentChannel); + RTMEngine.getInstance().setActiveChannelName(RTM_ROOMS.BREAKOUT); + + // Clear any pending retry timeout since we succeeded + if (subscribeTimeoutRef.current) { + clearTimeout(subscribeTimeoutRef.current); + subscribeTimeoutRef.current = null; + } } } catch (error) { logger.error( @@ -278,253 +276,131 @@ const RTMConfigureBreakoutRoomProvider = ( 'RTM subscribeChannel failed..Trying again', {error}, ); - setTimeout(async () => { + subscribeTimeoutRef.current = setTimeout(async () => { // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); + subscribeTimerRef.current = Math.min(subscribeTimerRef.current * 2, 30); subscribeChannel(); - }, timerValueRef.current * 1000); + }, subscribeTimerRef.current * 1000); } }; - const getMembers = async () => { + const getMembersWithAttributes = async () => { try { logger.log( LogSource.AgoraSDK, 'API', 'RTM presence.getOnlineUsers(getMembers) start', ); - await client.presence - .getOnlineUsers(channelName, 1) - .then(async (data: GetOnlineUsersResponse) => { - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM presence.getOnlineUsers data received', - data, - ); - await Promise.all( - data.occupants?.map(async member => { - try { - const backoffAttributes = - await fetchUserAttributesWithBackoffRetry(member.userId); - - await processUserUidAttributes( - backoffAttributes, - member.userId, - ); - // setting screenshare data - // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) - // isActive to identify all active screenshare users in the call - backoffAttributes?.items?.forEach(item => { - try { - if (hasJsonStructure(item.value as string)) { - const data = { - evt: item.key, // Use item.key instead of key - value: item.value, // Use item.value instead of value - }; - // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events - EventsQueue.enqueue({ - data: data, - uid: member.userId, - ts: timeNow(), - }); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'Log', - `RTM Failed to process user attribute item for ${ - member.userId - }: ${JSON.stringify(item)}`, - {error}, + const {allMembers, totalOccupancy} = await fetchOnlineMembersWithRetries( + client, + currentChannel, + { + onPage: async ({occupants, pageToken}) => { + console.log( + 'rudra-core-client: fetching user attributes for page: ', + pageToken, + occupants, + ); + await Promise.all( + occupants.map(async member => { + try { + const userAttributes = await fetchUserAttributesWithRetries( + client, + member.userId, + { + isMounted: () => isRTMMounted.current, + // 👈 called later if name arrives + onNameFound: retriedAttributesWithName => + mapUserAttributesToState( + retriedAttributesWithName, + member.userId, + syncUserState, + ), + }, + ); + console.log( + 'supriya rtm [breakout] attr backoffAttributes', + userAttributes, + ); + mapUserAttributesToState( + userAttributes, + member.userId, + syncUserState, + ); + // setting screenshare data + // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) + // isActive to identify all active screenshare users in the call + userAttributes?.items?.forEach(item => { + processUserAttributeForQueue( + item, + member.userId, + RTM_ROOMS.BREAKOUT, + (eventKey, value, userId) => { + const data = {evt: eventKey, value}; + EventsQueue.enqueue({ + data, + uid: userId, + ts: timeNow(), + }); + }, ); - // Continue processing other items - } - }); - } catch (e) { - logger.error( - LogSource.AgoraSDK, - 'Log', - `RTM Could not retrieve name of ${member.userId}`, - {error: e}, - ); - } - }), - ); - logger.debug( - LogSource.AgoraSDK, - 'Log', - 'RTM fetched all data and user attr...RTM init done', - ); - }); - timerValueRef.current = 5; + }); + } catch (e) { + logger.error( + LogSource.AgoraSDK, + 'Log', + `RTM Could not retrieve name of ${member.userId}`, + {error: e}, + ); + } + }), + ); + }, + }, + ); + + logger.debug( + LogSource.AgoraSDK, + 'Log', + 'RTM fetched all data and user attr...RTM init done', + ); + membersTimerRef.current = 5; + // Clear any pending retry timeout since we succeeded + if (membersTimeoutRef.current) { + clearTimeout(membersTimeoutRef.current); + membersTimeoutRef.current = null; + } } catch (error) { - setTimeout(async () => { + membersTimeoutRef.current = setTimeout(async () => { // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); - await getMembers(); - }, timerValueRef.current * 1000); + membersTimerRef.current = Math.min(membersTimerRef.current * 2, 30); + await getMembersWithAttributes(); + }, membersTimerRef.current * 1000); } }; - const readAllChannelAttributes = async () => { + const getChannelAttributes = async () => { try { - await client.storage - .getChannelMetadata(channelName, 1) - .then(async (data: GetChannelMetadataResponse) => { - for (const item of data.items) { - try { - const {key, value, authorUserId, updateTs} = item; - if (hasJsonStructure(value as string)) { - const evtData = { - evt: key, - value, - }; - // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events - EventsQueue.enqueue({ - data: evtData, - uid: authorUserId, - ts: updateTs, - }); - } - } catch (error) { - logger.error( - LogSource.AgoraSDK, - 'Log', - `RTM Failed to process channel attribute item: ${JSON.stringify( - item, - )}`, - {error}, - ); - // Continue processing other items - } - } - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM storage.getChannelMetadata data received', - data, - ); - }); - timerValueRef.current = 5; + await fetchChannelAttributesWithRetries( + client, + currentChannel, + eventData => EventsQueue.enqueue(eventData), + ); + channelAttributesTimerRef.current = 5; + // Clear any pending retry timeout since we succeeded + if (channelAttributesTimeoutRef.current) { + clearTimeout(channelAttributesTimeoutRef.current); + channelAttributesTimeoutRef.current = null; + } } catch (error) { - setTimeout(async () => { + channelAttributesTimeoutRef.current = setTimeout(async () => { // Cap the timer to prevent excessive delays (max 30 seconds) - timerValueRef.current = Math.min(timerValueRef.current * 2, 30); - await readAllChannelAttributes(); - }, timerValueRef.current * 1000); - } - }; - - const fetchUserAttributesWithBackoffRetry = async ( - userId: string, - ): Promise => { - return backOff( - async () => { - logger.log( - LogSource.AgoraSDK, - 'API', - `RTM fetching getUserMetadata for member ${userId}`, - ); - - const attr: GetUserMetadataResponse = - await client.storage.getUserMetadata({ - userId: userId, - }); - - if (!attr || !attr.items) { - logger.log( - LogSource.AgoraSDK, - 'API', - 'RTM attributes for member not found', - ); - throw attr; - } - - logger.log( - LogSource.AgoraSDK, - 'API', - `RTM getUserMetadata for member ${userId} received`, - {attr}, + channelAttributesTimerRef.current = Math.min( + channelAttributesTimerRef.current * 2, + 30, ); - - if (attr.items && attr.items.length > 0) { - return attr; - } else { - throw attr; - } - }, - { - retry: (e, idx) => { - logger.debug( - LogSource.AgoraSDK, - 'Log', - `RTM [retrying] Attempt ${idx}. Fetching ${userId}'s attributes`, - e, - ); - return true; - }, - }, - ); - }; - - const processUserUidAttributes = async ( - attr: GetUserMetadataResponse, - userId: string, - ) => { - try { - console.log('[user attributes]:', {attr}); - const uid = parseInt(userId, 10); - const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); - const isHostItem = attr?.items?.find(item => item.key === 'isHost'); - const nameItem = attr?.items?.find(item => item.key === 'name'); - const screenUid = screenUidItem?.value - ? parseInt(screenUidItem.value, 10) - : undefined; - - let userName = ''; - if (nameItem?.value) { - try { - const parsedValue = JSON.parse(nameItem.value); - const payloadString = parsedValue.payload; - if (payloadString) { - const payload = JSON.parse(payloadString); - userName = payload.name; - } - } catch (parseError) {} - } - - //start - updating user data in rtc - const userData = { - screenUid: screenUid, - //below thing for livestreaming - type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', - uid, - name: userName, - offline: false, - isHost: isHostItem?.value || false, - lastMessageTimeStamp: 0, - }; - console.log('new user joined', uid, userData); - syncUserState(uid, userData); - //end- updating user data in rtc - - //start - updating screenshare data in rtc - if (screenUid) { - const screenShareUser = { - type: UserType.ScreenShare, - parentUid: uid, - }; - syncUserState(screenUid, screenShareUser); - } - //end - updating screenshare data in rtc - } catch (e) { - logger.error( - LogSource.AgoraSDK, - 'Event', - `RTM Failed to process user data for ${userId}`, - {error: e}, - ); + getChannelAttributes(); + }, channelAttributesTimerRef.current * 1000); } }; @@ -560,6 +436,7 @@ const RTMConfigureBreakoutRoomProvider = ( 'inside eventDispatcher ', data, ); + console.log('supriya rtm [BREAKOUT] dispatcher: ', data); let evt = '', value = ''; @@ -645,9 +522,24 @@ const RTMConfigureBreakoutRoomProvider = ( ); return; } - const {payload, persistLevel, source} = parsedValue; + const {payload, persistLevel, source, _scope, _channelId} = parsedValue; + + console.log('supriya rtm [BREAKOUT] event data', data); + console.log( + 'supriya rtm [BREAKOUT] _scope and _channelId: ', + _scope, + _channelId, + currentChannel, + ); + // Filter if its for this channel + if (!isEventForActiveChannel(_scope, _channelId, currentChannel)) { + return; + } + // Step 1: Set local attributes if (persistLevel === PersistanceLevel.Session) { + // const roomKey = RTM_ROOMS.BREAKOUT; + // const roomAwareKey = `${roomKey}_${evt}`; const rtmAttribute = {key: evt, value: value}; const options: SetOrUpdateUserMetadataOptions = { userId: `${localUid}`, @@ -696,193 +588,152 @@ const RTMConfigureBreakoutRoomProvider = ( } }; - // Register listeners when client is created - useEffect(() => { - if (!client) { - return; - } - - const handleStorageEvent = (storage: StorageEvent) => { - // when remote user sets/updates metadata - 3 - if ( - storage.eventType === nativeStorageEventTypeMapping.SET || - storage.eventType === nativeStorageEventTypeMapping.UPDATE - ) { - const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; - const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; - logger.log( - LogSource.AgoraSDK, - 'Event', - `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, - storage, - ); - try { - if (storage.data?.items && Array.isArray(storage.data.items)) { - storage.data.items.forEach(item => { - try { - if (!item || !item.key) { - logger.warn( - LogSource.Events, - 'CUSTOM_EVENTS', - 'Invalid storage item:', - item, - ); - return; - } - - const {key, value, authorUserId, updateTs} = item; - const timestamp = getMessageTime(updateTs); - const sender = Platform.OS - ? get32BitUid(authorUserId) - : parseInt(authorUserId, 10); - eventDispatcher( - { - evt: key, - value, - }, - `${sender}`, - timestamp, - ); - } catch (error) { - logger.error( + // Listeners + const handleStorageEvent = (storage: StorageEvent) => { + // when remote user sets/updates metadata - 3 + if ( + storage.eventType === nativeStorageEventTypeMapping.SET || + storage.eventType === nativeStorageEventTypeMapping.UPDATE + ) { + const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; + const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; + logger.log( + LogSource.AgoraSDK, + 'Event', + `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, + storage, + ); + try { + if (storage.data?.items && Array.isArray(storage.data.items)) { + storage.data.items.forEach(item => { + try { + if (!item || !item.key) { + logger.warn( LogSource.Events, 'CUSTOM_EVENTS', - `Failed to process storage item: ${JSON.stringify(item)}`, - {error}, + 'Invalid storage item:', + item, ); + return; } - }); - } - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } - }; - - const handlePresenceEvent = async (presence: PresenceEvent) => { - if (`${localUid}` === presence.publisher) { - return; - } - if (presence.channelName !== channelName) { - console.log( - 'supriya event recevied in channel', - presence.channelName, - channelName, - ); - return; - } - // remoteJoinChannel - if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { - logger.log( - LogSource.AgoraSDK, - 'Event', - 'RTM presenceEvent of type [3 - remoteJoin] (channelMemberJoined)', - ); - const backoffAttributes = await fetchUserAttributesWithBackoffRetry( - presence.publisher, - ); - await processUserUidAttributes(backoffAttributes, presence.publisher); - } - // remoteLeaveChannel - if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { - logger.log( - LogSource.AgoraSDK, - 'Event', - 'RTM presenceEvent of type [4 - remoteLeave] (channelMemberLeft)', - presence, - ); - // Chat of left user becomes undefined. So don't cleanup - const uid = presence?.publisher - ? parseInt(presence.publisher, 10) - : undefined; - if (!uid) { - return; + const {key, value, authorUserId, updateTs} = item; + console.log('supriya-eventDispatcher item: ', item); + const timestamp = getMessageTime(updateTs); + const sender = Platform.OS + ? get32BitUid(authorUserId) + : parseInt(authorUserId, 10); + eventDispatcher( + { + evt: key, + value, + }, + `${sender}`, + timestamp, + ); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + `Failed to process storage item: ${JSON.stringify(item)}`, + {error}, + ); + } + }); } - SDKEvents.emit('_rtm-left', uid); - // updating the rtc data - syncUserState(uid, { - offline: true, - }); - } - }; - - const handleMessageEvent = (message: MessageEvent) => { - console.log('supriya current message channel: ', channelName); - console.log('supriya message event is', message); - // message - 1 (channel) - if (message.channelType === nativeChannelTypeMapping.MESSAGE) { - // here the channel name will be the channel name - logger.debug( + } catch (error) { + logger.error( LogSource.Events, 'CUSTOM_EVENTS', - 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', - message, + 'error while dispatching through eventDispatcher', + {error}, ); - const { - publisher: uid, - channelName, - message: text, - timestamp: ts, - } = message; - //whiteboard upload - if (parseInt(uid, 10) === 1010101) { - const [err, res] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - if (res?.data?.data?.images) { - LocalEventEmitter.emit( - LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, - res?.data?.data?.images, - ); - } - } else { - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } + } + } + }; - const timestamp = getMessageTime(ts); - const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); - try { - eventDispatcher(msg, `${sender}`, timestamp); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } + const handlePresenceEvent = async (presence: PresenceEvent) => { + if (presence.type === nativePresenceEventTypeMapping.REMOTE_JOIN) { + logger.log( + LogSource.AgoraSDK, + 'Event', + 'RTM presenceEvent of type [3 - remoteJoin] (channelMemberJoined)', + ); + const useAttributes = await fetchUserAttributesWithRetries( + client, + presence.publisher, + { + isMounted: () => isRTMMounted.current, + // This is called later if name arrives and hence we process that attribute + onNameFound: retriedAttributesWithName => + mapUserAttributesToState( + retriedAttributesWithName, + presence.publisher, + syncUserState, + ), + }, + ); + // This is called as soon as we receive any attributes + mapUserAttributesToState( + useAttributes, + presence.publisher, + syncUserState, + ); + } + // remoteLeaveChannel + if (presence.type === nativePresenceEventTypeMapping.REMOTE_LEAVE) { + logger.log( + LogSource.AgoraSDK, + 'Event', + 'RTM presenceEvent of type [4 - remoteLeave] (channelMemberLeft)', + presence, + ); + // Chat of left user becomes undefined. So don't cleanup + const uid = presence?.publisher + ? parseInt(presence.publisher, 10) + : undefined; + + if (!uid) { + return; } + // updating the rtc data + syncUserState(uid, { + offline: true, + }); + } + }; - // message - 3 (user) - if (message.channelType === nativeChannelTypeMapping.USER) { - logger.debug( - LogSource.Events, - 'CUSTOM_EVENTS', - 'messageEvent of type [3- USER] (messageReceived)', - message, - ); - // here the (message.channelname) channel name will be the to UID - const {publisher: peerId, timestamp: ts, message: text} = message; + const handleMessageEvent = (message: MessageEvent) => { + console.log('supriya current message channel: ', currentChannel); + console.log('supriya message event is', message); + // message - 1 (channel) + if (message.channelType === nativeChannelTypeMapping.MESSAGE) { + // here the channel name will be the channel name + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', + message, + ); + const {publisher: uid, message: text, timestamp: ts} = message; + //whiteboard upload + if (parseInt(uid, 10) === 1010101) { + const [err, res] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + if (res?.data?.data?.images) { + LocalEventEmitter.emit( + LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, + res?.data?.data?.images, + ); + } + } else { const [err, msg] = safeJsonParse(text); if (err) { logger.error( @@ -894,9 +745,7 @@ const RTMConfigureBreakoutRoomProvider = ( } const timestamp = getMessageTime(ts); - - const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); - + const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); try { eventDispatcher(msg, `${sender}`, timestamp); } catch (error) { @@ -908,32 +757,58 @@ const RTMConfigureBreakoutRoomProvider = ( ); } } - }; + } - registerCallbacks(channelName, { - storage: handleStorageEvent, - presence: handlePresenceEvent, - message: handleMessageEvent, - }); + // message - 3 (user) + if (message.channelType === nativeChannelTypeMapping.USER) { + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [3- USER] (messageReceived)', + message, + ); + // here the (message.channelname) channel name will be the to UID + const {publisher: peerId, timestamp: ts, message: text} = message; + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } - return () => { - unregisterCallbacks(channelName); - }; - }, [client, channelName]); + const timestamp = getMessageTime(ts); + + const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); - const unsubscribeAndCleanup = async (channel: string) => { - if (!callActive || !isLoggedIn) { - return; + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } } + }; + + const unsubscribeAndCleanup = async ( + currentClient: RTMClient, + channel: string, + ) => { try { - client.unsubscribe(channel); + setHasUserJoinedRTM(false); + setIsInitialQueueCompleted(false); + currentClient.unsubscribe(channel); RTMEngine.getInstance().removeChannel(channel); logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); if (isIOS() || isAndroid()) { EventUtils.clear(); } - setHasUserJoinedRTM(false); - setIsInitialQueueCompleted(false); logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); } catch (unsubscribeError) { console.log('supriya error while unsubscribing: ', unsubscribeError); @@ -942,18 +817,35 @@ const RTMConfigureBreakoutRoomProvider = ( useAsyncEffect(async () => { try { - if (client && isLoggedIn && callActive) { + if (client && isLoggedIn && callActive && currentChannel) { + hasInitRef.current = true; + registerCallbacks(currentChannel, { + storage: handleStorageEvent, + presence: handlePresenceEvent, + message: handleMessageEvent, + }); await init(); } } catch (error) { logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); } return async () => { - if (client) { - await unsubscribeAndCleanup(channelName); + const currentClient = RTMEngine.getInstance().engine; + hasInitRef.current = false; + isRTMMounted.current = false; + // Clear all pending timeouts on unmount + for (const timeout of eventTimeouts.values()) { + clearTimeout(timeout); + } + eventTimeouts.clear(); + if (currentChannel) { + unregisterCallbacks(currentChannel); + } + if (currentClient && callActive && isLoggedIn) { + await unsubscribeAndCleanup(currentClient, currentChannel); } }; - }, [isLoggedIn, callActive, channelName, client]); + }, [isLoggedIn, callActive, currentChannel, client]); const contextValue: RTMBreakoutRoomData = { hasUserJoinedRTM, diff --git a/template/src/rtm/RTMConfigureMainRoomProvider.tsx b/template/src/rtm/RTMConfigureMainRoomProvider.tsx index f675d810e..202f14721 100644 --- a/template/src/rtm/RTMConfigureMainRoomProvider.tsx +++ b/template/src/rtm/RTMConfigureMainRoomProvider.tsx @@ -18,34 +18,25 @@ import React, { createContext, useCallback, } from 'react'; -import { - type GetChannelMetadataResponse, - type GetOnlineUsersResponse, - type LinkStateEvent, - type MessageEvent, - type Metadata, - type PresenceEvent, - type SetOrUpdateUserMetadataOptions, - type StorageEvent, - type RTMClient, - type GetUserMetadataResponse, -} from 'agora-react-native-rtm'; +import {type MessageEvent, type StorageEvent} from 'agora-react-native-rtm'; import { ContentInterface, DispatchContext, - PropsContext, - UidType, useLocalUid, } from '../../agora-rn-uikit'; import {Platform} from 'react-native'; -import {isAndroid, isIOS, isWebInternal} from '../utils/common'; +import {isAndroid, isIOS} from '../utils/common'; import {useContent} from 'customization-api'; -import {safeJsonParse, getMessageTime, get32BitUid} from './utils'; +import { + safeJsonParse, + getMessageTime, + get32BitUid, + isEventForActiveChannel, +} from './utils'; import {EventUtils, EventsQueue} from '../rtm-events'; +import {PersistanceLevel} from '../rtm-events-api'; import RTMEngine from './RTMEngine'; import {filterObject} from '../utils'; -import SDKEvents from '../utils/SdkEvents'; -import isSDK from '../utils/isSDK'; import {useAsyncEffect} from '../utils/useAsyncEffect'; import { WaitingRoomStatus, @@ -65,8 +56,15 @@ import {RTMUserData, useRTMGlobalState} from './RTMGlobalStateProvider'; import {useUserGlobalPreferences} from '../components/UserGlobalPreferenceProvider'; import {ToggleState} from '../../agora-rn-uikit'; import useMuteToggleLocal from '../utils/useMuteToggleLocal'; -import {RTM_ROOMS} from './constants'; +import { + RTM_ROOMS, + RTM_EVENTS_ATTRIBUTES_TO_RESET_WHEN_ROOM_CHANGES, +} from './constants'; import {useRtc} from 'customization-api'; +import { + fetchChannelAttributesWithRetries, + clearRoomScopedUserAttributes, +} from './rtm-presence-utils'; const eventTimeouts = new Map>(); @@ -99,13 +97,13 @@ export const useRTMConfigureMain = () => { interface RTMConfigureMainRoomProviderProps { callActive: boolean; - channelName: string; + currentChannel: string; children: React.ReactNode; } const RTMConfigureMainRoomProvider: React.FC< RTMConfigureMainRoomProviderProps -> = ({callActive, channelName, children}) => { +> = ({callActive, currentChannel, children}) => { const rtmInitTimstamp = new Date().getTime(); const {dispatch} = useContext(DispatchContext); const {defaultContent, activeUids} = useContent(); @@ -117,6 +115,7 @@ const RTMConfigureMainRoomProvider: React.FC< const {applyUserPreferences, syncUserPreferences} = useUserGlobalPreferences(); const toggleMute = useMuteToggleLocal(); + const {rtcTracksReady} = useRtc(); const [hasUserJoinedRTM, setHasUserJoinedRTM] = useState(false); const [isInitialQueueCompleted, setIsInitialQueueCompleted] = useState(false); const [onlineUsersCount, setTotalOnlineUsers] = useState(0); @@ -133,10 +132,13 @@ const RTMConfigureMainRoomProvider: React.FC< unregisterMainChannelStorageHandler, } = useRTMGlobalState(); - /** - * inside event callback state won't have latest value. - * so creating ref to access the state - */ + // refs + const isRTMMounted = useRef(true); + const channelAttributesTimerRef: any = useRef(5); + const channelAttributesTimeoutRef = useRef | null>(null); + const isHostRef = useRef({isHost: isHost}); useEffect(() => { isHostRef.current.isHost = isHost; @@ -158,26 +160,31 @@ const RTMConfigureMainRoomProvider: React.FC< defaultContentRef.current = defaultContent; }, [defaultContent]); - const {rtcTracksReady} = useRtc(); - - // Set main room as active channel when this provider mounts again active - useEffect(() => { - const rtmEngine = RTMEngine.getInstance(); - if (rtmEngine.hasChannel(RTM_ROOMS.MAIN)) { - rtmEngine.setActiveChannel(RTM_ROOMS.MAIN); - } - }, []); + // Set online users + React.useEffect(() => { + setTotalOnlineUsers( + Object.keys( + filterObject( + defaultContent, + ([k, v]) => + v?.type === 'rtc' && + !v.offline && + activeUidsRef.current.activeUids.indexOf(v?.uid) !== -1, + ), + ).length, + ); + }, [defaultContent, activeUids]); - // Apply user preferences when main room mounts + // Set user preferences when main room mounts useEffect(() => { - if (rtcTracksReady) { + if (rtcTracksReady && localUid) { console.log( 'UP: trackesready', JSON.stringify(defaultContentRef.current[localUid]), ); applyUserPreferences(defaultContentRef.current[localUid], toggleMute); } - }, [rtcTracksReady]); + }, [rtcTracksReady, localUid]); // Sync current audio/video state audio video changes useEffect(() => { @@ -193,20 +200,7 @@ const RTMConfigureMainRoomProvider: React.FC< } }, [defaultContent, localUid, syncUserPreferences, rtcTracksReady]); - // Eventdispatcher timeout refs clean - const isRTMMounted = useRef(true); - useEffect(() => { - return () => { - isRTMMounted.current = false; - // Clear all pending timeouts on unmount - for (const timeout of eventTimeouts.values()) { - clearTimeout(timeout); - } - eventTimeouts.clear(); - }; - }, []); - - // Main room specific syncUserState function + // Set Main room specific syncUserState function const syncUserState = useCallback( (uid: number, data: any) => { // Extract only RTM-related fields that are actually passed @@ -256,21 +250,6 @@ const RTMConfigureMainRoomProvider: React.FC< [setMainRoomRTMUsers], ); - // Set online users - React.useEffect(() => { - setTotalOnlineUsers( - Object.keys( - filterObject( - defaultContent, - ([k, v]) => - v?.type === 'rtc' && - !v.offline && - activeUidsRef.current.activeUids.indexOf(v?.uid) !== -1, - ), - ).length, - ); - }, [defaultContent]); - useEffect(() => { Object.entries(mainRoomRTMUsers).forEach(([uidStr, rtmUser]) => { const uid = parseInt(uidStr, 10); @@ -292,16 +271,99 @@ const RTMConfigureMainRoomProvider: React.FC< }); }, [mainRoomRTMUsers, dispatch]); + const rehydrateSessionAttributes = async () => { + try { + const uid = localUid.toString(); + const attr = await client.storage.getUserMetadata({userId: uid}); + console.log('supriya-wasInBreakoutRoom: attr: ', attr); + + if (!attr?.items) { + return; + } + + attr.items.forEach(item => { + try { + // Check if this is a room-aware session attribute for current room + if (item.key && item.key.startsWith(`${RTM_ROOMS.MAIN}__`)) { + const parsed = JSON.parse(item.value); + if (parsed.persistLevel === PersistanceLevel.Session) { + // Replay into eventDispatcher so state gets rebuilt + eventDispatcher( + {evt: item.key, value: item.value}, + uid, + Date.now(), + ); + } + } + } catch (e) { + console.log('Failed to rehydrate session attribute', item.key, e); + } + }); + } catch (error) { + console.log('Failed to rehydrate session attributes', error); + } + }; + const init = async () => { + // Set main room as active channel when this provider mounts again active + const currentActiveChannel = RTMEngine.getInstance().getActiveChannelName(); + const wasInBreakoutRoom = currentActiveChannel === RTM_ROOMS.BREAKOUT; + + if (currentActiveChannel !== RTM_ROOMS.MAIN) { + RTMEngine.getInstance().setActiveChannelName(RTM_ROOMS.MAIN); + } + // Clear room-scoped RTM attributes to ensure fresh state + await clearRoomScopedUserAttributes( + client, + RTM_EVENTS_ATTRIBUTES_TO_RESET_WHEN_ROOM_CHANGES, + ); + + // Rehydrate session attributes ONLY when returning from breakout room + if (wasInBreakoutRoom) { + await rehydrateSessionAttributes(); + } + + await getChannelAttributes(); + setHasUserJoinedRTM(true); await runQueuedEvents(); setIsInitialQueueCompleted(true); }; + const getChannelAttributes = async () => { + try { + await fetchChannelAttributesWithRetries( + client, + currentChannel, + eventData => EventsQueue.enqueue(eventData), + ); + channelAttributesTimerRef.current = 5; + // Clear any pending retry timeout since we succeeded + if (channelAttributesTimeoutRef.current) { + clearTimeout(channelAttributesTimeoutRef.current); + channelAttributesTimeoutRef.current = null; + } + } catch (error) { + console.log( + 'rudra-core-client: RTM getchannelattributes failed..Trying again', + error, + ); + channelAttributesTimeoutRef.current = setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + channelAttributesTimerRef.current = Math.min( + channelAttributesTimerRef.current * 2, + 30, + ); + getChannelAttributes(); + }, channelAttributesTimerRef.current * 1000); + } + }; + const runQueuedEvents = async () => { try { while (!EventsQueue.isEmpty()) { const currEvt = EventsQueue.dequeue(); + console.log('supriya-session inside queue currEvt: ', currEvt); await eventDispatcher(currEvt.data, `${currEvt.uid}`, currEvt.ts); } } catch (error) { @@ -415,7 +477,20 @@ const RTMConfigureMainRoomProvider: React.FC< ); return; } - const {payload, persistLevel, source} = parsedValue; + const {payload, persistLevel, source, _scope, _channelId} = parsedValue; + console.log( + 'supriya-session-attributes [MAIN] _scope and _channelId: ', + source, + _scope, + _channelId, + currentChannel, + payload, + ); + // Filter if its for this channel + if (!isEventForActiveChannel(_scope, _channelId, currentChannel)) { + console.log('supriya-session-attributes SKIPPING', payload); + return; + } // Step 2: Emit the event (no metadata persistence - handled by RTMGlobalStateProvider) console.log(LogSource.Events, 'CUSTOM_EVENTS', 'emiting event..: ', evt); @@ -454,145 +529,100 @@ const RTMConfigureMainRoomProvider: React.FC< } }; - // Register listeners when client is created - useEffect(() => { - if (!client) { - return; - } - - const handleMainChannelStorageEvent = (storage: StorageEvent) => { - // when remote user sets/updates metadata - 3 - if ( - storage.eventType === nativeStorageEventTypeMapping.SET || - storage.eventType === nativeStorageEventTypeMapping.UPDATE - ) { - const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; - const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; - logger.log( - LogSource.AgoraSDK, - 'Event', - `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, - storage, - ); - try { - if (storage.data?.items && Array.isArray(storage.data.items)) { - storage.data.items.forEach(item => { - try { - if (!item || !item.key) { - logger.warn( - LogSource.Events, - 'CUSTOM_EVENTS', - 'Invalid storage item:', - item, - ); - return; - } - - const {key, value, authorUserId, updateTs} = item; - const timestamp = getMessageTime(updateTs); - const sender = Platform.OS - ? get32BitUid(authorUserId) - : parseInt(authorUserId, 10); - eventDispatcher( - { - evt: key, - value, - }, - `${sender}`, - timestamp, - ); - } catch (error) { - logger.error( + // Listeners + const handleMainChannelStorageEvent = (storage: StorageEvent) => { + // when remote user sets/updates metadata - 3 + if ( + storage.eventType === nativeStorageEventTypeMapping.SET || + storage.eventType === nativeStorageEventTypeMapping.UPDATE + ) { + const storageTypeStr = storage.storageType === 1 ? 'user' : 'channel'; + const eventTypeStr = storage.eventType === 2 ? 'SET' : 'UPDATE'; + logger.log( + LogSource.AgoraSDK, + 'Event', + `RTM storage event of type: [${eventTypeStr} ${storageTypeStr} metadata]`, + storage, + ); + try { + if (storage.data?.items && Array.isArray(storage.data.items)) { + storage.data.items.forEach(item => { + try { + if (!item || !item.key) { + logger.warn( LogSource.Events, 'CUSTOM_EVENTS', - `Failed to process storage item: ${JSON.stringify(item)}`, - {error}, + 'Invalid storage item:', + item, ); + return; } - }); - } - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } - }; - const handleMainChannelMessageEvent = (message: MessageEvent) => { - console.log('supriya current message channel: ', channelName); - console.log('supriya message event is', message); - // message - 1 (channel) - if (message.channelType === nativeChannelTypeMapping.MESSAGE) { - // here the channel name will be the channel name - logger.debug( + const {key, value, authorUserId, updateTs} = item; + const timestamp = getMessageTime(updateTs); + const sender = Platform.OS + ? get32BitUid(authorUserId) + : parseInt(authorUserId, 10); + eventDispatcher( + { + evt: key, + value, + }, + `${sender}`, + timestamp, + ); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + `Failed to process storage item: ${JSON.stringify(item)}`, + {error}, + ); + } + }); + } + } catch (error) { + logger.error( LogSource.Events, 'CUSTOM_EVENTS', - 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', - message, + 'error while dispatching through eventDispatcher', + {error}, ); - const { - publisher: uid, - channelName, - message: text, - timestamp: ts, - } = message; - //whiteboard upload - if (parseInt(uid, 10) === 1010101) { - const [err, res] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - if (res?.data?.data?.images) { - LocalEventEmitter.emit( - LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, - res?.data?.data?.images, - ); - } - } else { - const [err, msg] = safeJsonParse(text); - if (err) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'JSON payload incorrect, Error while parsing the payload', - {error: err}, - ); - } - - const timestamp = getMessageTime(ts); - const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); - try { - eventDispatcher(msg, `${sender}`, timestamp); - } catch (error) { - logger.error( - LogSource.Events, - 'CUSTOM_EVENTS', - 'error while dispatching through eventDispatcher', - {error}, - ); - } - } } + } + }; - // message - 3 (user) - if (message.channelType === nativeChannelTypeMapping.USER) { - logger.debug( - LogSource.Events, - 'CUSTOM_EVENTS', - 'messageEvent of type [3- USER] (messageReceived)', - message, - ); - // here the (message.channelname) channel name will be the to UID - const {publisher: peerId, timestamp: ts, message: text} = message; + const handleMainChannelMessageEvent = (message: MessageEvent) => { + console.log('supriya current message channel: ', currentChannel); + console.log('supriya message event is', message); + // message - 1 (channel) + if (message.channelType === nativeChannelTypeMapping.MESSAGE) { + // here the channel name will be the channel name + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [1 - CHANNEL] (channelMessageReceived)', + message, + ); + const {publisher: uid, message: text, timestamp: ts} = message; + //whiteboard upload + if (parseInt(uid, 10) === 1010101) { + const [err, res] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + if (res?.data?.data?.images) { + LocalEventEmitter.emit( + LocalEventsEnum.WHITEBOARD_FILE_UPLOAD, + res?.data?.data?.images, + ); + } + } else { const [err, msg] = safeJsonParse(text); if (err) { logger.error( @@ -604,9 +634,7 @@ const RTMConfigureMainRoomProvider: React.FC< } const timestamp = getMessageTime(ts); - - const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); - + const sender = Platform.OS ? get32BitUid(uid) : parseInt(uid, 10); try { eventDispatcher(msg, `${sender}`, timestamp); } catch (error) { @@ -618,42 +646,78 @@ const RTMConfigureMainRoomProvider: React.FC< ); } } - }; - - // Register with RTMGlobalStateProvider for main channel message handling - registerMainChannelMessageHandler(handleMainChannelMessageEvent); - registerMainChannelStorageHandler(handleMainChannelStorageEvent); - console.log( - 'RTMConfigureMainRoom: Registered main channel message handler', - ); + } - return () => { - unregisterMainChannelMessageHandler(); - unregisterMainChannelStorageHandler(); - console.log( - 'RTMConfigureMainRoom: Unregistered main channel message handler', + // message - 3 (user) + if (message.channelType === nativeChannelTypeMapping.USER) { + logger.debug( + LogSource.Events, + 'CUSTOM_EVENTS', + 'messageEvent of type [3- USER] (messageReceived)', + message, ); - }; - }, [client, channelName]); + // here the (message.channelname) channel name will be the to UID + const {publisher: peerId, timestamp: ts, message: text} = message; + const [err, msg] = safeJsonParse(text); + if (err) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'JSON payload incorrect, Error while parsing the payload', + {error: err}, + ); + } + + const timestamp = getMessageTime(ts); + + const sender = isAndroid() ? get32BitUid(peerId) : parseInt(peerId, 10); + + try { + eventDispatcher(msg, `${sender}`, timestamp); + } catch (error) { + logger.error( + LogSource.Events, + 'CUSTOM_EVENTS', + 'error while dispatching through eventDispatcher', + {error}, + ); + } + } + }; useAsyncEffect(async () => { try { - if (isLoggedIn && callActive) { + if (client && isLoggedIn && callActive && currentChannel) { + registerMainChannelMessageHandler(handleMainChannelMessageEvent); + registerMainChannelStorageHandler(handleMainChannelStorageEvent); await init(); } } catch (error) { logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); } return async () => { + isRTMMounted.current = false; logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); + unregisterMainChannelMessageHandler(); + unregisterMainChannelStorageHandler(); if (isIOS() || isAndroid()) { EventUtils.clear(); } + // Clear all pending timeouts on unmount + for (const timeout of eventTimeouts.values()) { + clearTimeout(timeout); + } + eventTimeouts.clear(); + // Clear timer-based retry timeouts + if (channelAttributesTimeoutRef.current) { + clearTimeout(channelAttributesTimeoutRef.current); + channelAttributesTimeoutRef.current = null; + } setHasUserJoinedRTM(false); setIsInitialQueueCompleted(false); logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); }; - }, [isLoggedIn, callActive, channelName]); + }, [client, isLoggedIn, callActive, currentChannel]); // Provide context data to children const contextValue: RTMMainRoomData = { diff --git a/template/src/rtm/RTMCoreProvider.tsx b/template/src/rtm/RTMCoreProvider.tsx index 3fa3ccbe9..c43790515 100644 --- a/template/src/rtm/RTMCoreProvider.tsx +++ b/template/src/rtm/RTMCoreProvider.tsx @@ -20,6 +20,7 @@ import {UidType} from '../../agora-rn-uikit'; import RTMEngine from '../rtm/RTMEngine'; import {isWeb, isWebInternal} from '../utils/common'; import isSDK from '../utils/isSDK'; +import {useAsyncEffect} from '../utils/useAsyncEffect'; // Event callback types type MessageCallback = (message: MessageEvent) => void; @@ -135,6 +136,7 @@ export const RTMCoreProvider: React.FC = ({ const options: SetOrUpdateUserMetadataOptions = { userId: `${userInfo.localUid}`, }; + await rtmClient.storage.removeUserMetadata(); await rtmClient.storage.setUserMetadata(data, options); } catch (setAttributeError) { console.log('setAttributeError: ', setAttributeError); @@ -155,13 +157,14 @@ export const RTMCoreProvider: React.FC = ({ // Global event listeners - centralized in RTMCoreProvider useEffect(() => { - if (!client) { + if (!client || !userInfo?.localUid) { return; } const handleGlobalStorageEvent = (storage: StorageEvent) => { console.log( 'rudra-core-client ********************** ---StorageEvent event: ', storage, + callbackRegistry, ); // Distribute to all registered callbacks callbackRegistry.current.forEach((callbacks, channelName) => { @@ -179,6 +182,7 @@ export const RTMCoreProvider: React.FC = ({ console.log( 'rudra-core-client @@@@@@@@@@@@@@@@@@@@@@@ ---PresenceEvent: ', presence, + callbackRegistry, ); // Distribute to all registered callbacks callbackRegistry.current.forEach((callbacks, channelName) => { @@ -197,6 +201,14 @@ export const RTMCoreProvider: React.FC = ({ 'rudra-core-client ######################## ---MessageEvent event: ', message, ); + if (String(userInfo.localUid) === message.publisher) { + console.log( + 'rudra-core-client ######################## SKIPPING this message event as it is local', + message, + callbackRegistry, + ); + return; + } // Distribute to all registered callbacks callbackRegistry.current.forEach((callbacks, channelName) => { if (callbacks.message) { @@ -219,13 +231,14 @@ export const RTMCoreProvider: React.FC = ({ client.removeEventListener('presence', handleGlobalPresenceEvent); client.removeEventListener('message', handleGlobalMessageEvent); }; - }, [client]); + }, [client, userInfo?.localUid]); useEffect(() => { if (client) { return; } const initializeRTM = async () => { + console.log('supriya-rtm-lifecycle init'); // 1, Check if engine is already connected // 2. Initialize RTM Engine if (!RTMEngine.getInstance()?.isEngineReady) { @@ -269,50 +282,70 @@ export const RTMCoreProvider: React.FC = ({ }; initializeRTM(); + }, [client, stableUserInfo, setAttribute]); - return () => { - // Cleanup - console.log('supriya-rtm RTM cleanup is happening'); - if (client) { - console.log('supriya RTM cleanup is happening'); - RTMEngine.getInstance().destroy(); - setClient(null); + const cleanupRTM = async () => { + try { + const engine = RTMEngine.getInstance(); + if (engine?.engine) { + console.log('RTM cleanup: destroying engine...'); + await engine.destroy(); + console.log('RTM cleanup: engine destroyed.'); } - }; - }, [client, stableUserInfo, setAttribute]); + setClient(null); + } catch (err) { + console.error('RTM cleanup failed:', err); + } + }; + useAsyncEffect(() => { + return async () => { + // Cleanup + console.log('supriya-rtm-lifecycle cleanup'); + await cleanupRTM(); + // if (currentClient) { + // console.log('supriya-rtm-lifecycle cleanup calling destroy'); + // await RTMEngine.getInstance().destroy(); + // console.log( + // 'supriya-rtm-lifecycle setting client null', + // RTMEngine.getInstance().engine, + // ); + // setClient(null); + // } + }; + }, []); // Handle browser close/reload events for RTM cleanup useEffect(() => { - if (!$config.ENABLE_CONVERSATIONAL_AI) { + if (!$config.ENABLE_CONVERSATIONAL_AI && isWebInternal()) { const handleBrowserClose = (ev: BeforeUnloadEvent) => { ev.preventDefault(); return (ev.returnValue = 'Are you sure you want to exit?'); }; - const handleRTMLogout = () => { - if (client && isLoggedIn) { - console.log('Browser closing, logging out from RTM'); - client.logout().catch(() => {}); - } + const handleRTMCleanup = () => { + console.log('Browser closing: calling cleanupRTM()'); + // Fire-and-forget, no await because page is unloading + cleanupRTM(); + // Optional: add beacon for debugging + // navigator.sendBeacon?.( + // '/cleanup-log', + // JSON.stringify({msg: 'RTM cleanup triggered on pagehide'}), + // ); }; - if (!isWebInternal()) { - return; - } - window.addEventListener( 'beforeunload', isWeb() && !isSDK() ? handleBrowserClose : () => {}, ); - window.addEventListener('pagehide', handleRTMLogout); + window.addEventListener('pagehide', handleRTMCleanup); return () => { window.removeEventListener( 'beforeunload', isWeb() && !isSDK() ? handleBrowserClose : () => {}, ); - window.removeEventListener('pagehide', handleRTMLogout); + window.removeEventListener('pagehide', handleRTMCleanup); }; } }, [client, isLoggedIn]); diff --git a/template/src/rtm/RTMEngine.ts b/template/src/rtm/RTMEngine.ts index f38395c1b..5f0a936c4 100644 --- a/template/src/rtm/RTMEngine.ts +++ b/template/src/rtm/RTMEngine.ts @@ -76,7 +76,7 @@ class RTMEngine { throw new Error('addChannel: channelID must be a non-empty string'); } this.channelMap.set(name, channelID); - this.setActiveChannel(name); + this.setActiveChannelName(name); } removeChannel(name: string) { @@ -87,14 +87,14 @@ class RTMEngine { return this.localUID; } - getChannel(name?: string): string { + getChannelId(name?: string): string { // Default to active channel if no name provided const channelName = name || this.activeChannelName; console.log('supriya channelName: ', this.channelMap.get(channelName)); return this.channelMap.get(channelName) || ''; } - get allChannels(): string[] { + get allChannelIds(): string[] { return Array.from(this.channelMap.values()).filter( channel => channel.trim() !== '', ); @@ -109,7 +109,7 @@ class RTMEngine { } /** Set the active channel for default operations */ - setActiveChannel(name: string): void { + setActiveChannelName(name: string): void { if (!name || typeof name !== 'string' || name.trim() === '') { throw new Error('setActiveChannel: name must be a non-empty string'); } @@ -120,13 +120,13 @@ class RTMEngine { } this.activeChannelName = name; console.log( - `RTMEngine: Active channel set to '${name}' (${this.getChannel(name)})`, + `RTMEngine: Active channel set to '${name}' (${this.getChannelId(name)})`, ); } /** Get the current active channel ID */ - getActiveChannel(): string { - return this.getChannel(this.activeChannelName); + getActiveChannelId(): string { + return this.getChannelId(this.activeChannelName); } /** Get the current active channel name */ @@ -185,9 +185,10 @@ class RTMEngine { if (!this._engine) { return; } + console.log('supriya-rtm-lifecycle unsubscribing from all channel'); // Unsubscribe from all tracked channels - for (const channel of this.allChannels) { + for (const channel of this.allChannelIds) { try { await this._engine.unsubscribe(channel); } catch (err) { @@ -197,7 +198,9 @@ class RTMEngine { // 2. Remove all listeners if supported try { - this._engine.removeAllListeners?.(); + console.log('supriya-rtm-lifecycle remove all listeners '); + + await this._engine.removeAllListeners?.(); } catch { console.warn('Failed to remove listeners:'); } @@ -205,11 +208,17 @@ class RTMEngine { // 3. Logout and release resources try { await this._engine.logout(); + console.log('supriya-rtm-lifecycle logged out '); if (isAndroid() || isIOS()) { this._engine.release(); } - } catch (err) { - console.warn('RTM logout/release failed:', err); + } catch (logoutErrorState) { + // Logout of Signaling + const {operation, reason, errorCode} = logoutErrorState; + console.log( + `${operation} supriya-rtm-lifecycle logged out failed, the error code is ${errorCode}, because of: ${reason}.`, + ); + console.warn('RTM logout/release failed:', logoutErrorState); } } @@ -220,12 +229,15 @@ class RTMEngine { return; } await this.destroyClientInstance(); + console.log('supriya-rtm-lifecycle destruction completed '); + this.channelMap.clear(); // Reset state this.localUID = ''; this.activeChannelName = RTM_ROOMS.MAIN; this._engine = undefined; RTMEngine._instance = null; + console.log('supriya-rtm-lifecycle setting engine instance as null'); } catch (error) { console.error('Error destroying RTM instance:', error); // Don't re-throw - destruction should be a best-effort cleanup diff --git a/template/src/rtm/RTMGlobalStateProvider.tsx b/template/src/rtm/RTMGlobalStateProvider.tsx index f03dc4073..330da3fe6 100644 --- a/template/src/rtm/RTMGlobalStateProvider.tsx +++ b/template/src/rtm/RTMGlobalStateProvider.tsx @@ -12,33 +12,29 @@ import React, {useState, useEffect, useRef} from 'react'; import { - type GetChannelMetadataResponse, - type GetOnlineUsersResponse, - type GetUserMetadataResponse, type PresenceEvent, type StorageEvent, type SetOrUpdateUserMetadataOptions, type MessageEvent, } from 'agora-react-native-rtm'; -import {backOff} from 'exponential-backoff'; import {timeNow, hasJsonStructure} from '../rtm/utils'; import {EventsQueue} from '../rtm-events'; import {PersistanceLevel} from '../rtm-events-api'; import RTMEngine from '../rtm/RTMEngine'; -import {LogSource, logger} from '../logger/AppBuilderLogger'; -import {RECORDING_BOT_UID} from '../utils/constants'; import {useRTMCore} from './RTMCoreProvider'; -import { - ContentInterface, - RtcPropsInterface, - UidType, - useLocalUid, -} from '../../agora-rn-uikit'; +import {RtcPropsInterface, UidType} from '../../agora-rn-uikit'; import { nativePresenceEventTypeMapping, nativeStorageEventTypeMapping, } from '../../bridge/rtm/web/Types'; import {RTM_ROOMS} from './constants'; +import { + fetchOnlineMembersWithRetries, + fetchUserAttributesWithRetries, + mapUserAttributesToState, + fetchChannelAttributesWithRetries, + processUserAttributeForQueue, +} from './rtm-presence-utils'; export enum UserType { ScreenShare = 'screenshare', @@ -57,8 +53,6 @@ export interface RTMUserData { isHost: string; // Host privileges (stored in RTM user metadata as 'isHost') } -const eventTimeouts = new Map>(); - interface RTMGlobalStateProviderProps { children: React.ReactNode; mainChannelRtcProps: Partial; @@ -92,7 +86,7 @@ const RTMGlobalStateProvider: React.FC = ({ mainChannelRtcProps, }) => { const mainChannelName = mainChannelRtcProps.channel; - const localUid = useLocalUid(); + const localUid = mainChannelRtcProps.uid; const {client, isLoggedIn, registerCallbacks, unregisterCallbacks} = useRTMCore(); // Main room RTM users (RTM-specific data only) @@ -100,20 +94,22 @@ const RTMGlobalStateProvider: React.FC = ({ [uid: number]: RTMUserData; }>({}); - const hasInitRef = useRef(false); // Timeout Refs + const isRTMMounted = useRef(true); + const hasInitRef = useRef(false); + const subscribeTimerRef: any = useRef(5); - const channelAttributesTimerRef: any = useRef(5); - const membersTimerRef: any = useRef(5); const subscribeTimeoutRef = useRef | null>( null, ); + + const channelAttributesTimerRef: any = useRef(5); const channelAttributesTimeoutRef = useRef | null>(null); - const membersTimeoutRef = useRef | null>(null); - const isRTMMounted = useRef(true); + const membersTimerRef: any = useRef(5); + const membersTimeoutRef = useRef | null>(null); // Message handler registration for main channel const messageHandlerRef = useRef<((message: MessageEvent) => void) | null>( @@ -132,7 +128,6 @@ const RTMGlobalStateProvider: React.FC = ({ } messageHandlerRef.current = handler; }; - const unregisterMainChannelMessageHandler = () => { console.log( 'rudra-core-client: RTM unregistering main channel message handler', @@ -144,7 +139,6 @@ const RTMGlobalStateProvider: React.FC = ({ const storageHandlerRef = useRef<((storage: StorageEvent) => void) | null>( null, ); - const registerMainChannelStorageHandler = ( handler: (storage: StorageEvent) => void, ) => { @@ -158,7 +152,6 @@ const RTMGlobalStateProvider: React.FC = ({ } storageHandlerRef.current = handler; }; - const unregisterMainChannelStorageHandler = () => { console.log( 'rudra-core-client: RTM unregistering main channel storage handler', @@ -166,31 +159,15 @@ const RTMGlobalStateProvider: React.FC = ({ storageHandlerRef.current = null; }; - useEffect(() => { - return () => { - isRTMMounted.current = false; - // Clear all pending timeouts on unmount - for (const timeout of eventTimeouts.values()) { - clearTimeout(timeout); - } - eventTimeouts.clear(); - - // Clear timer-based retry timeouts - if (subscribeTimeoutRef.current) { - clearTimeout(subscribeTimeoutRef.current); - subscribeTimeoutRef.current = null; - } - if (channelAttributesTimeoutRef.current) { - clearTimeout(channelAttributesTimeoutRef.current); - channelAttributesTimeoutRef.current = null; - } - if (membersTimeoutRef.current) { - clearTimeout(membersTimeoutRef.current); - membersTimeoutRef.current = null; - } - }; - }, []); + // Update main rtm users state + const updateMainRoomUser = (uid: number, data: RTMUserData) => { + setMainRoomRTMUsers(prev => ({ + ...prev, + [uid]: {...(prev[uid] || {}), ...data}, + })); + }; + // Init cycle starts const init = async () => { try { console.log('rudra-core-client: Starting RTM init for main channel'); @@ -205,7 +182,7 @@ const RTMGlobalStateProvider: React.FC = ({ const subscribeChannel = async () => { try { - if (RTMEngine.getInstance().allChannels.includes(mainChannelName)) { + if (RTMEngine.getInstance().allChannelIds.includes(mainChannelName)) { console.log('rudra- main channel already subsribed'); } else { console.log('rudra- subscribing...'); @@ -218,6 +195,7 @@ const RTMGlobalStateProvider: React.FC = ({ console.log('rudra- subscribed main channel', mainChannelName); RTMEngine.getInstance().addChannel(RTM_ROOMS.MAIN, mainChannelName); + RTMEngine.getInstance().setActiveChannelName(RTM_ROOMS.MAIN); subscribeTimerRef.current = 5; // Clear any pending retry timeout since we succeeded if (subscribeTimeoutRef.current) { @@ -239,126 +217,101 @@ const RTMGlobalStateProvider: React.FC = ({ } }; - const getChannelAttributes = async () => { - try { - await client.storage - .getChannelMetadata(mainChannelName, 1) - .then(async (data: GetChannelMetadataResponse) => { - for (const item of data.items) { - try { - const {key, value, authorUserId, updateTs} = item; - if (hasJsonStructure(value as string)) { - const evtData = { - evt: key, - value, - }; - // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events - EventsQueue.enqueue({ - data: evtData, - uid: authorUserId, - ts: updateTs, - }); - } - } catch (error) { - console.log( - 'rudra-core-client: RTM Failed to process channel attribute item', - item, - error, - ); - // Continue processing other items - } - } - console.log( - 'rudra-core-client: RTM storage.getChannelMetadata data received', - data, - ); - }); - channelAttributesTimerRef.current = 5; - // Clear any pending retry timeout since we succeeded - if (channelAttributesTimeoutRef.current) { - clearTimeout(channelAttributesTimeoutRef.current); - channelAttributesTimeoutRef.current = null; - } - } catch (error) { - channelAttributesTimeoutRef.current = setTimeout(async () => { - // Cap the timer to prevent excessive delays (max 30 seconds) - channelAttributesTimerRef.current = Math.min( - channelAttributesTimerRef.current * 2, - 30, - ); - await getChannelAttributes(); - }, channelAttributesTimerRef.current * 1000); - } - }; - const getMembersWithAttributes = async () => { try { console.log( 'rudra-core-client: RTM presence.getOnlineUsers(getMembers) start', ); - await client.presence - .getOnlineUsers(mainChannelName, 1) - .then(async (data: GetOnlineUsersResponse) => { - console.log( - 'rudra-core-client: RTM presence.getOnlineUsers data received', - data, - ); - await Promise.all( - data.occupants?.map(async member => { - try { - const backoffAttributes = - await fetchUserAttributesWithBackoffRetry(member.userId); - - await processUserUidAttributes( - backoffAttributes, - member.userId, - ); - // setting screenshare data - // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) - // isActive to identify all active screenshare users in the call - backoffAttributes?.items?.forEach(item => { - try { - if (hasJsonStructure(item.value as string)) { - const data = { - evt: item.key, // Use item.key instead of key - value: item.value, // Use item.value instead of value - }; - // TODOSUP: Add the data to queue, dont add same mulitple events, use set so as to not repeat events - EventsQueue.enqueue({ - data: data, - uid: member.userId, - ts: timeNow(), - }); - } - } catch (error) { - console.log( - 'rudra-core-client: RTM Failed to process user attribute item for', - member.userId, + const {allMembers, totalOccupancy} = await fetchOnlineMembersWithRetries( + client, + mainChannelName, + { + onPage: async ({occupants, total, pageToken}) => { + console.log( + 'rudra-core-client: fetching user attributes for page: ', + pageToken, + occupants, + ); + await Promise.all( + occupants.map(async member => { + try { + const userAttributes = await fetchUserAttributesWithRetries( + client, + member.userId, + { + isMounted: () => isRTMMounted.current, + // called later if name arrives + onNameFound: async retryAttr => + mapUserAttributesToState( + retryAttr, + member.userId, + updateMainRoomUser, + ), + }, + ); + console.log( + `supriya rtm backoffAttributes for ${member.userId}`, + userAttributes, + ); + mapUserAttributesToState( + userAttributes, + member.userId, + updateMainRoomUser, + ); + console.log( + `supriya rtm backoffAttributes for ${member.userId}`, + userAttributes, + ); + + // setting screenshare data + // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) + // isActive to identify all active screenshare users in the call + console.log( + 'supriya-session-test userAttributes', + userAttributes, + ); + userAttributes?.items?.forEach(item => { + processUserAttributeForQueue( item, - error, + member.userId, + RTM_ROOMS.MAIN, + (eventKey, value, userId) => { + const data = {evt: eventKey, value}; + console.log( + 'supriya-session-test adding to queue', + data, + ); + EventsQueue.enqueue({ + data, + uid: userId, + ts: timeNow(), + }); + }, ); - // Continue processing other items - } - }); - } catch (e) { - console.log( - 'rudra-core-client: RTM Could not retrieve name of', - member.userId, - e, - ); - } - }), - ); - membersTimerRef.current = 5; - // Clear any pending retry timeout since we succeeded - if (membersTimeoutRef.current) { - clearTimeout(membersTimeoutRef.current); - membersTimeoutRef.current = null; - } - console.log( - 'rudra-core-client: RTM fetched all data and user attr...RTM init done', - ); - }); + }); + } catch (e) { + console.log( + 'rudra-core-client: RTM Could not retrieve name of', + member.userId, + e, + ); + } + }), + ); + }, + }, + ); + + membersTimerRef.current = 5; + // Clear any pending retry timeout since we succeeded + if (membersTimeoutRef.current) { + clearTimeout(membersTimeoutRef.current); + membersTimeoutRef.current = null; + } + console.log( + 'rudra-core-client: RTM fetched all data and user attr...RTM init done', + ); + // }); } catch (error) { membersTimeoutRef.current = setTimeout(async () => { // Cap the timer to prevent excessive delays (max 30 seconds) @@ -368,120 +321,36 @@ const RTMGlobalStateProvider: React.FC = ({ } }; - const fetchUserAttributesWithBackoffRetry = async ( - userId: string, - ): Promise => { - return backOff( - async () => { - console.log( - 'rudra-core-client: RTM fetching getUserMetadata for member', - userId, - ); - - const attr: GetUserMetadataResponse = - await client.storage.getUserMetadata({ - userId: userId, - }); - - if (!attr || !attr.items) { - console.log('rudra-core-client: RTM attributes for member not found'); - throw attr; - } - - console.log( - 'rudra-core-client: RTM getUserMetadata for member received', - userId, - attr, - ); - - if (attr.items && attr.items.length > 0) { - return attr; - } else { - throw attr; - } - }, - { - retry: (e, idx) => { - console.log( - 'rudra-core-client: RTM [retrying] Attempt', - idx, - 'Fetching', - userId, - 'attributes', - e, - ); - return true; - }, - }, - ); - }; - - const processUserUidAttributes = async ( - attr: GetUserMetadataResponse, - userId: string, - ) => { + const getChannelAttributes = async () => { try { - console.log('rudra-core-client: [user attributes]:', attr); - const uid = parseInt(userId, 10); - const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); - const isHostItem = attr?.items?.find(item => item.key === 'isHost'); - const nameItem = attr?.items?.find(item => item.key === 'name'); - const screenUid = screenUidItem?.value - ? parseInt(screenUidItem.value, 10) - : undefined; - - let userName = ''; - if (nameItem?.value) { - try { - const parsedValue = JSON.parse(nameItem.value); - const payloadString = parsedValue.payload; - if (payloadString) { - const payload = JSON.parse(payloadString); - userName = payload.name; - } - } catch (parseError) {} - } - - //start - updating RTM user data - const rtmUserData: RTMUserData = { - uid, - type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', - screenUid: screenUid, - name: userName, - offline: false, - isHost: isHostItem?.value || 'false', - lastMessageTimeStamp: 0, - }; - console.log('rudra-core-client: new RTM user joined', uid, rtmUserData); - setMainRoomRTMUsers(prev => ({ - ...prev, - [uid]: {...(prev[uid] || {}), ...rtmUserData}, - })); - //end- updating RTM user data - - //start - updating screenshare RTM data - if (screenUid) { - // @ts-ignore - const screenShareRTMData: RTMUserData = { - type: 'screenshare', - parentUid: uid, - // Note: screenUid itself doesn't need screenUid field, parentUid will be handled in RTC layer - }; - setMainRoomRTMUsers(prev => ({ - ...prev, - [screenUid]: {...(prev[screenUid] || {}), ...screenShareRTMData}, - })); + await fetchChannelAttributesWithRetries( + client, + mainChannelName, + eventData => EventsQueue.enqueue(eventData), + ); + channelAttributesTimerRef.current = 5; + // Clear any pending retry timeout since we succeeded + if (channelAttributesTimeoutRef.current) { + clearTimeout(channelAttributesTimeoutRef.current); + channelAttributesTimeoutRef.current = null; } - //end - updating screenshare RTM data - } catch (e) { + } catch (error) { console.log( - 'rudra-core-client: RTM Failed to process user data for', - userId, - e, + 'rudra-core-client: RTM getchannelattributes failed..Trying again', + error, ); + channelAttributesTimeoutRef.current = setTimeout(async () => { + // Cap the timer to prevent excessive delays (max 30 seconds) + channelAttributesTimerRef.current = Math.min( + channelAttributesTimerRef.current * 2, + 30, + ); + getChannelAttributes(); + }, channelAttributesTimerRef.current * 1000); } }; + // Listeners const handleMainChannelPresenceEvent = async (presence: PresenceEvent) => { console.log( 'rudra-core-client: RTM presence event received for different channel', @@ -506,10 +375,26 @@ const RTMGlobalStateProvider: React.FC = ({ presence.publisher, ); try { - const backoffAttributes = await fetchUserAttributesWithBackoffRetry( + const userAttributes = await fetchUserAttributesWithRetries( + client, presence.publisher, + { + isMounted: () => isRTMMounted.current, + // This is called later if name arrives and hence we process that attribute + onNameFound: retriedAttributes => + mapUserAttributesToState( + retriedAttributes, + presence.publisher, + updateMainRoomUser, + ), + }, + ); + // This is called as soon as we receive any attributes + mapUserAttributesToState( + userAttributes, + presence.publisher, + updateMainRoomUser, ); - await processUserUidAttributes(backoffAttributes, presence.publisher); } catch (error) { console.log( 'rudra-core-client: RTM Failed to process user who joined main room', @@ -545,7 +430,7 @@ const RTMGlobalStateProvider: React.FC = ({ } // Also mark screenshare as offline if exists - const screenUid = prev[uid]?.screenUid; + // const screenUid = prev[uid]?.screenUid; // if (screenUid && updated[screenUid]) { // updated[screenUid] = { // ...updated[screenUid], @@ -556,7 +441,6 @@ const RTMGlobalStateProvider: React.FC = ({ console.log( 'rudra-core-client: RTM marked user as offline in main room', uid, - screenUid ? `and screenshare ${screenUid}` : '', ); return updated; }); @@ -580,79 +464,79 @@ const RTMGlobalStateProvider: React.FC = ({ `rudra-core-client: RTM processing ${eventTypeStr} ${storageTypeStr} metadata`, ); - // STEP 1: Handle metadata persistence FIRST (core RTM functionality) - try { - if (storage.data?.items && Array.isArray(storage.data.items)) { - for (const item of storage.data.items) { - try { - if (!item || !item.key) { - console.log( - 'rudra-core-client: RTM invalid storage item:', - item, - ); - continue; - } - - const {key, value, authorUserId, updateTs} = item; - - // Parse the value to check persistLevel - let parsedValue; - try { - parsedValue = - typeof value === 'string' ? JSON.parse(value) : value; - } catch (parseError) { - console.log( - 'rudra-core-client: RTM failed to parse storage event value:', - parseError, - ); - continue; - } - - const {persistLevel} = parsedValue; - - // Handle metadata persistence for Session level events - if (persistLevel === PersistanceLevel.Session) { - console.log( - 'rudra-core-client: RTM setting user metadata for key:', - key, - ); - - const rtmAttribute = {key: key, value: value}; - const options: SetOrUpdateUserMetadataOptions = { - userId: `${localUid}`, - }; - - try { - await client.storage.setUserMetadata( - {items: [rtmAttribute]}, - options, - ); - console.log( - 'rudra-core-client: RTM successfully set user metadata for key:', - key, - ); - } catch (setMetadataError) { - console.log( - 'rudra-core-client: RTM failed to set user metadata:', - setMetadataError, - ); - } - } - } catch (itemError) { - console.log( - 'rudra-core-client: RTM failed to process storage item:', - item, - itemError, - ); - } - } - } - } catch (error) { - console.log( - 'rudra-core-client: RTM error processing storage event:', - error, - ); - } + // // STEP 1: Handle metadata persistence FIRST (core RTM functionality) + // try { + // if (storage.data?.items && Array.isArray(storage.data.items)) { + // for (const item of storage.data.items) { + // try { + // if (!item || !item.key) { + // console.log( + // 'rudra-core-client: RTM invalid storage item:', + // item, + // ); + // continue; + // } + + // const {key, value, authorUserId, updateTs} = item; + + // // Parse the value to check persistLevel + // let parsedValue; + // try { + // parsedValue = + // typeof value === 'string' ? JSON.parse(value) : value; + // } catch (parseError) { + // console.log( + // 'rudra-core-client: RTM failed to parse storage event value:', + // parseError, + // ); + // continue; + // } + + // const {persistLevel} = parsedValue; + + // // Handle metadata persistence for Session level events + // if (persistLevel === PersistanceLevel.Session) { + // console.log( + // 'rudra-core-client: RTM setting user metadata for key:', + // key, + // ); + + // const rtmAttribute = {key: key, value: value}; + // const options: SetOrUpdateUserMetadataOptions = { + // userId: `${localUid}`, + // }; + + // try { + // await client.storage.setUserMetadata( + // {items: [rtmAttribute]}, + // options, + // ); + // console.log( + // 'rudra-core-client: RTM successfully set user metadata for key:', + // key, + // ); + // } catch (setMetadataError) { + // console.log( + // 'rudra-core-client: RTM failed to set user metadata:', + // setMetadataError, + // ); + // } + // } + // } catch (itemError) { + // console.log( + // 'rudra-core-client: RTM failed to process storage item:', + // item, + // itemError, + // ); + // } + // } + // } + // } catch (error) { + // console.log( + // 'rudra-core-client: RTM error processing storage event:', + // error, + // ); + // } // STEP 2: Forward to application logic AFTER metadata persistence if (storageHandlerRef.current) { @@ -671,12 +555,51 @@ const RTMGlobalStateProvider: React.FC = ({ } }; - const handleMainChannelMessageEvent = (message: MessageEvent) => { + const handleMainChannelMessageEvent = async (message: MessageEvent) => { console.log( 'rudra-core-client: RTM main channel message event received', message, ); + // Check if this is a SESSION-level event and persist it + try { + if (hasJsonStructure(message.message)) { + const parsed = JSON.parse(message.message); + const {evt, value} = parsed; + + if (value && hasJsonStructure(value)) { + const parsedValue = JSON.parse(value); + const {persistLevel, _channelId} = parsedValue; + + // If this is a SESSION-level event from main channel, store it on local user's attributes + if ( + persistLevel === PersistanceLevel.Session && + _channelId === mainChannelName + ) { + // const roomAwareKey = `${RTM_ROOMS.MAIN}__${evt}`; + const rtmAttribute = {key: evt, value: value}; + + const options: SetOrUpdateUserMetadataOptions = { + userId: `${localUid}`, + }; + await client.storage.setUserMetadata( + {items: [rtmAttribute]}, + options, + ); + // console.log( + // 'rudra-core-client: Stored SESSION attribute cross-room', + // roomAwareKey, + // ); + } + } + } + } catch (error) { + console.log( + 'rudra-core-client: RTM error storing session attribute:', + error, + ); + } + // Forward to registered message handler (RTMConfigure) if (messageHandlerRef.current) { try { @@ -715,17 +638,29 @@ const RTMGlobalStateProvider: React.FC = ({ ); init(); return () => { - console.log('rudra-clean up for global state - call unsubscribe'); + console.log('rudra-core-client: main state cleanup'); hasInitRef.current = false; + isRTMMounted.current = false; if (mainChannelName) { unregisterCallbacks(mainChannelName); if (RTMEngine.getInstance().hasChannel(mainChannelName)) { client?.unsubscribe(mainChannelName).catch(() => {}); RTMEngine.getInstance().removeChannel(mainChannelName); } - console.log( - 'rudra-core-client: RTM unregistered callbacks for main channel', - ); + } + + // Clear timer-based retry timeouts + if (subscribeTimeoutRef.current) { + clearTimeout(subscribeTimeoutRef.current); + subscribeTimeoutRef.current = null; + } + if (membersTimeoutRef.current) { + clearTimeout(membersTimeoutRef.current); + membersTimeoutRef.current = null; + } + if (channelAttributesTimeoutRef.current) { + clearTimeout(channelAttributesTimeoutRef.current); + channelAttributesTimeoutRef.current = null; } }; }, [client, isLoggedIn, mainChannelName]); diff --git a/template/src/rtm/constants.ts b/template/src/rtm/constants.ts index bfa274e18..c8026e13d 100644 --- a/template/src/rtm/constants.ts +++ b/template/src/rtm/constants.ts @@ -1,4 +1,15 @@ +import {EventNames} from '../rtm-events'; + export enum RTM_ROOMS { BREAKOUT = 'BREAKOUT', MAIN = 'MAIN', } + +// RTM attributes to reset when room changes +export const RTM_EVENTS_ATTRIBUTES_TO_RESET_WHEN_ROOM_CHANGES = [ + EventNames.RAISED_ATTRIBUTE, // (livestream) + EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, // Breakout room raise hand ( will be made into independent) + EventNames.STT_ACTIVE, + EventNames.STT_LANGUAGE, + EventNames.ROLE_ATTRIBUTE, +] as const; diff --git a/template/src/rtm/rtm-presence-utils.ts b/template/src/rtm/rtm-presence-utils.ts new file mode 100644 index 000000000..ac95b231c --- /dev/null +++ b/template/src/rtm/rtm-presence-utils.ts @@ -0,0 +1,339 @@ +import React from 'react'; +import {backOff} from 'exponential-backoff'; +import {LogSource, logger} from '../logger/AppBuilderLogger'; +import { + type GetUserMetadataResponse as NativeGetUserMetadataResponse, + type GetOnlineUsersResponse as NativeGetOnlineUsersResponse, + type GetChannelMetadataResponse, + type RTMClient, + type UserState, + type MetadataItem, +} from 'agora-react-native-rtm'; +import {RTMUserData} from './RTMGlobalStateProvider'; +import {RECORDING_BOT_UID} from '../utils/constants'; +import {hasJsonStructure, stripRoomPrefixFromEventKey} from '../rtm/utils'; +import {nativeChannelTypeMapping} from '../../bridge/rtm/web/Types'; +import {PersistanceLevel} from '../rtm-events-api'; + +export const fetchOnlineMembersWithRetries = async ( + client: RTMClient, + channelName: string, + { + onPage, // 👈 callback so caller can process each page as soon as it's ready + }: { + onPage?: (page: { + occupants: UserState[]; + total: number; + pageToken?: string; + }) => void | Promise; + } = {}, +) => { + let allMembers: any[] = []; + let nextPage: string | undefined; + let totalOccupancy = 0; + + const fetchPage = async (pageNumber?: string) => { + return backOff( + async () => { + const result: NativeGetOnlineUsersResponse = + await client.presence.getOnlineUsers( + channelName, + nativeChannelTypeMapping.MESSAGE, + {page: pageNumber}, // cursor for pagination + ); + return result; + }, + { + numOfAttempts: 3, + retry: (e, attempt) => { + console.warn( + `[RTM] Page fetch failed (attempt ${attempt}/3). Retrying…`, + e, + ); + return attempt < 3; // 👈 stop retrying after 3rd attempt + }, + }, + ); + }; + + do { + try { + const result = await fetchPage(nextPage); + const {totalOccupancy: total, occupants, nextPage: next} = result; + if (occupants) { + allMembers = allMembers.concat(occupants); + // process this page immediately + await onPage?.({occupants, total, pageToken: nextPage}); + } + totalOccupancy = total; + nextPage = next; + console.log( + `[RTM] Fetched ${allMembers.length}/${totalOccupancy} users, nextPage=${nextPage}`, + ); + } catch (fetchPageError) { + console.error(`[RTM] Page ${nextPage || 'first'} failed`, fetchPageError); + // 👉 Skip to next page if this one keeps failing + nextPage = undefined; + } + } while (nextPage && nextPage.trim() !== ''); + + return {allMembers, totalOccupancy}; +}; + +export const fetchUserAttributesWithRetries = async ( + client: RTMClient, + userId: string, + opts?: { + isMounted?: () => boolean; // <-- injected check + onNameFound?: (attr: NativeGetUserMetadataResponse) => void; + }, +): Promise => { + return backOff( + async () => { + console.log( + 'rudra-core-client: RTM fetching getUserMetadata for member', + userId, + ); + + // Fetch attributes + const attr: NativeGetUserMetadataResponse = + await client.storage.getUserMetadata({userId}); + console.log('[user attributes', attr); + + // 1. Check if attributes exist + if (!attr || !attr.items || attr.items.length === 0) { + console.log('rudra-core-client: RTM attributes for member not found'); + throw new Error('No attribute items found'); + } + console.log('sup-attribute-check attributes', attr); + + // 2. Partial update allowed (screenUid, isHost, etc.) + const hasAny = attr.items.some(i => i.value); + if (!hasAny) { + throw new Error('No usable attributes yet'); + } + console.log('sup-attribute-check hasAny', hasAny); + + // 3. If name exists, return immediately + const hasNameAttribute = attr.items.find( + i => i.key === 'name' && i.value, + ); + console.log('sup-attribute-check name', hasNameAttribute); + if (hasNameAttribute) { + return attr; + } + // 4. Background retry for name only + (async () => { + await backOff( + async () => { + // 🔒 Stop if unmounted + if (opts?.isMounted && !opts?.isMounted) { + throw new Error(`Component unmounted while retrying ${userId}`); + } + console.log('sup-attribute-check inside name backoff'); + + const retriedAttributes: NativeGetUserMetadataResponse = + await client.storage.getUserMetadata({userId}); + console.log( + 'sup-attribute-check retriedAttributes', + retriedAttributes, + ); + + const hasNameAttributeRetry = retriedAttributes.items.find( + i => i.key === 'name' && i.value, + ); + console.log( + 'sup-attribute-check hasNameAttributeRetry', + hasNameAttributeRetry, + ); + + if (!hasNameAttributeRetry) { + throw new Error('Name still not found'); + } + + if (opts?.isMounted) { + console.log('sup-attribute-check onNameFound'); + opts?.onNameFound?.(retriedAttributes); + } + return retriedAttributes; + }, + { + retry: () => true, + }, + ).catch(() => { + console.log( + `Name not found for ${userId} within 30s, giving up further retries`, + ); + }); + })(); + + return attr; + }, + { + retry: (e, idx) => { + logger.debug( + LogSource.AgoraSDK, + 'Log', + `[retrying] Attempt ${idx}. Fetching ${userId}'s name`, + e, + ); + return true; + }, + }, + ); +}; + +export const mapUserAttributesToState = ( + attr: NativeGetUserMetadataResponse, + userId: string, + updateFn: (uid: number, userData: Partial) => void, +) => { + try { + const uid = parseInt(userId, 10); + const screenUidItem = attr?.items?.find(item => item.key === 'screenUid'); + const isHostItem = attr?.items?.find(item => item.key === 'isHost'); + const nameItem = attr?.items?.find(item => item.key === 'name'); + const screenUid = screenUidItem?.value + ? parseInt(screenUidItem.value, 10) + : undefined; + + let userName = ''; + if (nameItem?.value) { + try { + const parsedValue = JSON.parse(nameItem.value); + const payloadString = parsedValue.payload; + if (payloadString) { + const payload = JSON.parse(payloadString); + userName = payload.name; + } + } catch { + // ignore parse errors + } + } + + // --- Update main user RTM data + const userData: RTMUserData = { + uid, + type: uid === parseInt(RECORDING_BOT_UID, 10) ? 'bot' : 'rtc', + screenUid, + name: userName, + offline: false, + isHost: isHostItem?.value || 'false', + lastMessageTimeStamp: 0, + }; + + updateFn(uid, userData); + + // --- Update screenshare RTM data if present + if (screenUid) { + const screenShareData: RTMUserData = { + type: 'screenshare', + parentUid: uid, + }; + updateFn(screenUid, screenShareData); + } + } catch (e) { + console.log('RTM Failed to process user data for', userId, e); + } +}; + +export const fetchChannelAttributesWithRetries = async ( + client: RTMClient, + channelName: string, + updateFn?: (eventData: {data: any; uid: string; ts: number}) => void, +) => { + try { + await client.storage + .getChannelMetadata(channelName, nativeChannelTypeMapping.MESSAGE) + .then(async (data: GetChannelMetadataResponse) => { + console.log('supriya-channel-attributes: ', data); + for (const item of data.items) { + try { + const {key, value, authorUserId, updateTs} = item; + if (hasJsonStructure(value as string)) { + const evtData = { + evt: key, + value, + }; + updateFn?.({ + data: evtData, + uid: authorUserId, + ts: updateTs, + }); + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + `RTM Failed to process channel attribute item: ${JSON.stringify( + item, + )}`, + {error}, + ); + } + } + }); + } catch (error) {} +}; + +export const clearRoomScopedUserAttributes = async ( + client: RTMClient, + attributeKeys: readonly string[], +) => { + try { + await client?.storage.removeUserMetadata({ + data: { + items: attributeKeys.map(key => ({ + key, + value: '', + })), + }, + }); + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'RTMConfigure', + 'Failed to clear room-scoped attributes', + {error}, + ); + } +}; + +export const processUserAttributeForQueue = ( + item: MetadataItem, + userId: string, + currentRoomKey: string, + onProcessedEvent: (eventKey: string, value: string, userId: string) => void, +) => { + try { + if (hasJsonStructure(item.value as string)) { + let eventKey = item.key; + try { + // const parsedValue = JSON.parse(item.value); + // if (parsedValue.persistLevel === PersistanceLevel.Session) { + // const strippedKey = stripRoomPrefixFromEventKey( + // item.key, + // currentRoomKey, + // ); + // if (strippedKey === null) { + // console.log( + // 'Skipping SESSION attribute for different room:', + // item.key, + // ); + // return; + // } + // eventKey = strippedKey; + // } + + onProcessedEvent(eventKey, item.value, userId); + } catch (e) {} + } + } catch (error) { + logger.error( + LogSource.AgoraSDK, + 'Log', + 'RTM Failed to process user attribute item', + {error}, + ); + } +}; diff --git a/template/src/rtm/utils.ts b/template/src/rtm/utils.ts index 87e226da0..b8492c020 100644 --- a/template/src/rtm/utils.ts +++ b/template/src/rtm/utils.ts @@ -1,5 +1,10 @@ +import {RTM_EVENT_SCOPE} from '../rtm-events'; +import {RTM_ROOMS} from './constants'; + export const hasJsonStructure = (str: string) => { - if (typeof str !== 'string') return false; + if (typeof str !== 'string') { + return false; + } try { const result = JSON.parse(str); const type = Object.prototype.toString.call(result); @@ -42,3 +47,82 @@ export const get32BitUid = (peerId: string) => { arr[0] = parseInt(peerId); return arr[0]; }; + +export function isEventForActiveChannel( + scope: RTM_EVENT_SCOPE, + eventChannelId: string | undefined, + currentChannel: string, +): boolean { + // Case 1: Events without scope/channel → assume SERVER event → always pass + if (!scope && !eventChannelId) { + console.log( + 'isEventForActiveChannel: passing server event (no scope/channel)', + ); + return true; + } + + // Case 2: Global events always pass + if (scope === RTM_EVENT_SCOPE.GLOBAL) { + return true; + } + + // Case 3: Local and Session scope must match current channel + if ( + (scope === RTM_EVENT_SCOPE.LOCAL || scope === RTM_EVENT_SCOPE.SESSION) && + eventChannelId !== currentChannel + ) { + console.log( + `isEventForActiveChannel: skipped ${scope.toLowerCase()} event (expected=${currentChannel}, got=${eventChannelId})`, + ); + return false; + } + // Default: allow + return true; +} + +export function stripRoomPrefixFromEventKey( + eventKey: string, + currentRoomKey: string, +): string | null { + // Event key + if (!eventKey) { + return eventKey; + } + + // Only handle room-aware keys + if (!eventKey.startsWith(`${currentRoomKey}__`)) { + return eventKey; + } + + // Format: room____ + const parts = eventKey.split('__'); + console.log('supriya-session-attribute parts: ', parts); + const [roomKey, ...evtParts] = parts; + + console.log('supriya-session-attribute parts:', roomKey, evtParts); + + // If the roomKey matches current room, strip and return event name + if (roomKey === currentRoomKey) { + console.log( + 'supriya-session-attribute Matched current room, stripping prefix:', + roomKey, + ); + return evtParts.join('__'); + } + + // If the roomKey is "MAIN" or "BREAKOUT" but doesn't match current room → skip + if (roomKey === RTM_ROOMS.MAIN || roomKey === RTM_ROOMS.BREAKOUT) { + console.log( + 'supriya-session-attribute Prefix is MAIN/BREAKOUT but does not match, skipping', + ); + return null; + } + + // Different room → skip + console.log( + 'supriya-session-attribute Different room, skipping event:', + roomKey, + currentRoomKey, + ); + return null; +} diff --git a/template/src/subComponents/ChatContainer.tsx b/template/src/subComponents/ChatContainer.tsx index d60274b7b..ed3679c3c 100644 --- a/template/src/subComponents/ChatContainer.tsx +++ b/template/src/subComponents/ChatContainer.tsx @@ -59,6 +59,7 @@ import { } from '../language/default-labels/videoCallScreenLabels'; import CommonStyles from '../components/CommonStyles'; import PinnedMessage from './chat/PinnedMessage'; +import ChatAnnouncementView from './chat/ChatAnnouncementView'; /** * Chat container is the component which renders all the chat messages @@ -252,7 +253,13 @@ const ChatContainer = (props?: { ) : null} - {!message?.hide ? ( + + {message?.isAnnouncementText ? ( + + ) : !message?.hide ? ( + + + + Message from host: {senderName} + + {message} + + + ); +} + +const style = StyleSheet.create({ + announcementView: { + display: 'flex', + flexDirection: 'row', + alignItems: 'flex-start', + justifyContent: 'flex-start', + marginRight: 12, + marginLeft: 12, + marginBottom: 4, + marginTop: 16, + gap: 6, + }, + announcementMessage: { + display: 'flex', + flexDirection: 'column', + gap: 2, + }, + announcementMessageHeader: { + fontSize: ThemeConfig.FontSize.tiny, + fontStyle: 'normal', + fontFamily: ThemeConfig.FontFamily.sansPro, + color: '#099DFD', + fontWeight: '700', + lineHeight: 14, + }, + announcementMessageBody: { + fontSize: ThemeConfig.FontSize.tiny, + fontStyle: 'italic', + fontFamily: ThemeConfig.FontFamily.sansPro, + color: '#099DFD', + fontWeight: '400', + lineHeight: 14, + }, +}); diff --git a/template/src/subComponents/chat/ChatSendButton.tsx b/template/src/subComponents/chat/ChatSendButton.tsx index 9c1ef8594..89c3722c8 100644 --- a/template/src/subComponents/chat/ChatSendButton.tsx +++ b/template/src/subComponents/chat/ChatSendButton.tsx @@ -71,6 +71,7 @@ export const handleChatSend = ({ return; } + // Text message sent update sender side ui const sendTextMessage = () => { const option = { chatType: selectedUserId diff --git a/template/src/subComponents/screenshare/ScreenshareButton.tsx b/template/src/subComponents/screenshare/ScreenshareButton.tsx index f47475c5c..db5171d4e 100644 --- a/template/src/subComponents/screenshare/ScreenshareButton.tsx +++ b/template/src/subComponents/screenshare/ScreenshareButton.tsx @@ -55,6 +55,9 @@ const ScreenshareButton = (props: ScreenshareButtonProps) => { const screenShareButtonLabel = useString(toolbarItemShareText); const lstooltip = useString(livestreamingShareTooltipText); const {permissions} = useBreakoutRoom(); + // In the main room (default case), permissions come from the main room state. + // If the user is in a breakout room, retrieve permissions from the breakout room instead. + const canScreenshareInBreakoutRoom = permissions.canScreenshare; const onPress = () => { if (isScreenshareActive) { @@ -107,7 +110,14 @@ const ScreenshareButton = (props: ScreenshareButtonProps) => { iconButtonProps.toolTipMessage = lstooltip(isHandRaised(local.uid)); iconButtonProps.disabled = true; } - if (!permissions.canScreenshare) { + + if (!canScreenshareInBreakoutRoom) { + iconButtonProps.iconProps = { + ...iconButtonProps.iconProps, + tintColor: $config.SEMANTIC_NEUTRAL, + showWarningIcon: true, + }; + iconButtonProps.toolTipMessage = 'cannot screenshare'; iconButtonProps.disabled = true; } diff --git a/template/src/utils/useEndCall.ts b/template/src/utils/useEndCall.ts index 13d8d08d2..d3d72cd7c 100644 --- a/template/src/utils/useEndCall.ts +++ b/template/src/utils/useEndCall.ts @@ -69,7 +69,6 @@ const useEndCall = () => { if ($config.CHAT) { deleteChatUser(); } - RTMEngine.getInstance().destroy(); if (!ENABLE_AUTH) { // await authLogout(); await authLogin(); From 1649b54efc1be4435cd0605de468cc41ed9368d0 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 00:52:48 +0530 Subject: [PATCH 10/56] add breakout user --- .../context/BreakoutRoomContext.tsx | 113 +++++++++++------- .../components/breakout-room/state/reducer.ts | 11 +- 2 files changed, 80 insertions(+), 44 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 5aa46d578..8c98721a5 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -21,6 +21,7 @@ import { initialBreakoutRoomState, RoomAssignmentStrategy, ManualParticipantAssignment, + BreakoutRoomUser, } from '../state/reducer'; import {useLocalUid} from '../../../../agora-rn-uikit'; import {useContent} from '../../../../customization-api'; @@ -40,8 +41,6 @@ import { } from '../../../rtm/RTMGlobalStateProvider'; import {useScreenshare} from '../../../subComponents/screenshare/useScreenshare'; -const BREAKOUT_LOCK_TIMEOUT_MS = 5000; - const HOST_BROADCASTED_OPERATIONS = [ BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, BreakoutGroupActionTypes.CREATE_GROUP, @@ -57,6 +56,7 @@ const HOST_BROADCASTED_OPERATIONS = [ const getSanitizedPayload = ( payload: BreakoutGroup[], + defaultContentRef: any, mainRoomRTMUsers: {[uid: number]: RTMUserData}, ) => { return payload.map(({id, ...rest}) => { @@ -67,15 +67,19 @@ const getSanitizedPayload = ( ...group, participants: { hosts: group.participants.hosts.filter(uid => { - // Check defaultContent first let user = mainRoomRTMUsers[uid]; + if (defaultContentRef[uid]) { + user = defaultContentRef[uid]; + } if (user) { return !user.offline && user.type === 'rtc'; } }), attendees: group.participants.attendees.filter(uid => { - // Check defaultContent first let user = mainRoomRTMUsers[uid]; + if (defaultContentRef[uid]) { + user = defaultContentRef[uid]; + } if (user) { return !user.offline && user.type === 'rtc'; } @@ -92,20 +96,20 @@ const getSanitizedPayload = ( }); }; -const validateRollbackState = (state: BreakoutRoomState): boolean => { - return ( - Array.isArray(state.breakoutGroups) && - typeof state.breakoutSessionId === 'string' && - typeof state.canUserSwitchRoom === 'boolean' && - state.breakoutGroups.every( - group => - typeof group.id === 'string' && - typeof group.name === 'string' && - Array.isArray(group.participants?.hosts) && - Array.isArray(group.participants?.attendees), - ) - ); -}; +// const validateRollbackState = (state: BreakoutRoomState): boolean => { +// return ( +// Array.isArray(state.breakoutGroups) && +// typeof state.breakoutSessionId === 'string' && +// typeof state.canUserSwitchRoom === 'boolean' && +// state.breakoutGroups.every( +// group => +// typeof group.id === 'string' && +// typeof group.name === 'string' && +// Array.isArray(group.participants?.hosts) && +// Array.isArray(group.participants?.attendees), +// ) +// ); +// }; export const deepCloneBreakoutGroups = ( groups: BreakoutGroup[] = [], @@ -179,7 +183,7 @@ interface BreakoutRoomContextValue { assignmentStrategy: RoomAssignmentStrategy; canUserSwitchRoom: boolean; toggleRoomSwitchingAllowed: (value: boolean) => void; - unassignedParticipants: {uid: UidType; user: ContentInterface}[]; + unassignedParticipants: {uid: UidType; user: BreakoutRoomUser}[]; manualAssignments: ManualParticipantAssignment[]; setManualAssignments: (assignments: ManualParticipantAssignment[]) => void; clearManualAssignments: () => void; @@ -428,7 +432,7 @@ const BreakoutRoomProvider = ({ return; } - const hostName = defaultContentRef.current[localUid]?.name || 'Host'; + const hostName = getDisplayName(localUid); logger.log( LogSource.Internals, @@ -456,7 +460,7 @@ const BreakoutRoomProvider = ({ return; } - const hostName = defaultContentRef.current[localUid]?.name || 'Host'; + const hostName = getDisplayName(localUid); logger.log( LogSource.Internals, @@ -527,34 +531,45 @@ const BreakoutRoomProvider = ({ return; } - // Get currently assigned participants from all rooms - // Filter active UIDs to exclude: - // 1. Custom content (not type 'rtc') - // 2. Screenshare UIDs - // 3. Offline users - const filteredParticipants = activeUids - .map(uid => ({ - uid, - user: defaultContent[uid], - })) - .filter(({uid, user}) => { - console.log('supriya-breakoutSessionId user: ', user); - if (!user) { - return false; - } + // Filter users from defaultContent first, then check if they're in activeUids + // This follows the legacy RTM pattern: start with defaultContent, then filter by activeUids + const filteredParticipants = Object.entries(defaultContent) + .filter(([k, v]) => { // Only include RTC users - if (user.type !== 'rtc') { + if (v?.type !== 'rtc') { return false; } // Exclude offline users - if (user.offline) { + if (v?.offline) { return false; } // Exclude screenshare UIDs (they typically have a parentUid) - if (user.parentUid) { + if (v?.parentUid) { + return false; + } + // KEY CHECK: Only include users who are in activeUids (actually in the call) + const uid = parseInt(k); + if (activeUids.indexOf(uid) === -1) { return false; } return true; + }) + .map(([k, v]) => { + const uid = parseInt(k); + + // Get additional RTM data if available for cross-room scenarios + const rtmUser = mainRoomRTMUsers[uid]; + const user = v || rtmUser; + + console.log('supriya-breakoutSessionId user: ', user); + + // Create BreakoutRoomUser object with proper fallback + const breakoutRoomUser: BreakoutRoomUser = { + name: user?.name || rtmUser?.name || '', + isHost: user?.isHost === 'true', + }; + + return {uid, user: breakoutRoomUser}; }); // Sort participants to show local user first @@ -598,7 +613,14 @@ const BreakoutRoomProvider = ({ unassignedParticipants: filteredParticipants, }, }); - }, [defaultContent, activeUids, localUid, dispatch, state.breakoutSessionId]); + }, [ + defaultContent, + activeUids, + localUid, + dispatch, + state.breakoutSessionId, + mainRoomRTMUsers, + ]); // Increment version when breakout group assignments change useEffect(() => { @@ -791,9 +813,14 @@ const BreakoutRoomProvider = ({ assignment_type: stateRef.current.assignmentStrategy, breakout_room: type === 'START' - ? getSanitizedPayload(initialBreakoutGroups, mainRoomRTMUsers) + ? getSanitizedPayload( + initialBreakoutGroups, + defaultContentRef, + mainRoomRTMUsers, + ) : getSanitizedPayload( stateRef.current.breakoutGroups, + defaultContentRef, mainRoomRTMUsers, ), }; @@ -1362,6 +1389,9 @@ const BreakoutRoomProvider = ({ return; } + // If u are reciving or calling this tha means u will have + // valid data in defaultcontent as u cannot exit from the room + // you are not in const localUser = defaultContentRef.current[localUid]; try { @@ -2069,6 +2099,7 @@ const BreakoutRoomProvider = ({ ); const senderName = getDisplayName(srcuid); + console.log('supriya-senderName: ', senderName, srcuid); // ---- SCREEN SHARE CLEANUP ---- // Stop screen share if user is moving between rooms or leaving breakout diff --git a/template/src/components/breakout-room/state/reducer.ts b/template/src/components/breakout-room/state/reducer.ts index bb9e63a32..62b1fbdfa 100644 --- a/template/src/components/breakout-room/state/reducer.ts +++ b/template/src/components/breakout-room/state/reducer.ts @@ -13,6 +13,11 @@ export interface ManualParticipantAssignment { isSelected: boolean; } +export interface BreakoutRoomUser { + name: string; + isHost: boolean; +} + export interface BreakoutGroup { id: string; name: string; @@ -24,7 +29,7 @@ export interface BreakoutGroup { export interface BreakoutRoomState { breakoutSessionId: string; breakoutGroups: BreakoutGroup[]; - unassignedParticipants: {uid: UidType; user: ContentInterface}[]; + unassignedParticipants: {uid: UidType; user: BreakoutRoomUser}[]; manualAssignments: ManualParticipantAssignment[]; assignmentStrategy: RoomAssignmentStrategy; canUserSwitchRoom: boolean; @@ -143,7 +148,7 @@ export type BreakoutRoomAction = | { type: typeof BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS; payload: { - unassignedParticipants: {uid: UidType; user: ContentInterface}[]; + unassignedParticipants: {uid: UidType; user: BreakoutRoomUser}[]; }; } | { @@ -318,7 +323,7 @@ export const breakoutRoomReducer = ( const currentRoomId = roomIds[roomIndex]; const roomAssignment = roomAssignments.get(currentRoomId)!; // Assign participant based on their isHost status (string "true"/"false") - if (participant.user?.isHost === 'true') { + if (participant.user?.isHost) { roomAssignment.hosts.push(participant.uid); } else { roomAssignment.attendees.push(participant.uid); From f2a836286d81c5d4d93d54bd9663ce5c1eecba5d Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 09:46:36 +0530 Subject: [PATCH 11/56] add correct join info --- .../room-info/useSetBreakoutRoomInfo.tsx | 9 +- .../pages/video-call/BreakoutVideoCall.tsx | 32 +- .../src/pages/video-call/VideoCallContent.tsx | 45 +-- template/src/rtm/RTMCoreProvider.tsx | 286 +++++++++--------- 4 files changed, 178 insertions(+), 194 deletions(-) diff --git a/template/src/components/room-info/useSetBreakoutRoomInfo.tsx b/template/src/components/room-info/useSetBreakoutRoomInfo.tsx index 318fd4ff9..170cf55f7 100644 --- a/template/src/components/room-info/useSetBreakoutRoomInfo.tsx +++ b/template/src/components/room-info/useSetBreakoutRoomInfo.tsx @@ -1,10 +1,8 @@ import React, {createContext, useContext, useState} from 'react'; import {BreakoutChannelJoinEventPayload} from '../breakout-room/state/types'; -type BreakoutRoomData = { +type BreakoutRoomData = BreakoutChannelJoinEventPayload['data']['data'] & { isBreakoutMode: boolean; - channelId: string; - breakoutRoomName: string; }; interface BreakoutRoomInfoContextValue { @@ -46,12 +44,11 @@ export const useSetBreakoutRoomInfo = () => { const {setBreakoutRoomChannelData} = useBreakoutRoomInfo(); const setBreakoutRoomChannelInfo = ( - payload: BreakoutChannelJoinEventPayload, + breakoutJoinChannelDetails: BreakoutRoomData, ) => { const breakoutData: BreakoutRoomData = { - breakoutRoomName: payload.data.data.room_name, - channelId: payload.data.data.channel_name, isBreakoutMode: true, + ...breakoutJoinChannelDetails, }; setBreakoutRoomChannelData(breakoutData); }; diff --git a/template/src/pages/video-call/BreakoutVideoCall.tsx b/template/src/pages/video-call/BreakoutVideoCall.tsx index 82960751d..279250349 100644 --- a/template/src/pages/video-call/BreakoutVideoCall.tsx +++ b/template/src/pages/video-call/BreakoutVideoCall.tsx @@ -50,49 +50,47 @@ import {BeautyEffectProvider} from '../../components/beauty-effect/useBeautyEffe import {UserActionMenuProvider} from '../../components/useUserActionMenu'; import {RaiseHandProvider} from '../../components/raise-hand'; import {BreakoutRoomProvider} from '../../components/breakout-room/context/BreakoutRoomContext'; -import {useBreakoutRoomInfo} from '../../components/room-info/useSetBreakoutRoomInfo'; -import { - BreakoutChannelDetails, - VideoCallContentProps, -} from './VideoCallContent'; +import {useSetBreakoutRoomInfo} from '../../components/room-info/useSetBreakoutRoomInfo'; +import {VideoCallContentProps} from './VideoCallContent'; import BreakoutRoomEventsConfigure from '../../components/breakout-room/events/BreakoutRoomEventsConfigure'; import {RTM_ROOMS} from '../../rtm/constants'; +import {BreakoutChannelJoinEventPayload} from '../../components/breakout-room/state/types'; interface BreakoutVideoCallProps extends VideoCallContentProps { rtcProps: RtcPropsInterface; - breakoutChannelDetails: BreakoutChannelDetails; + breakoutJoinChannelDetails: BreakoutChannelJoinEventPayload['data']['data']; onLeave: () => void; } const BreakoutVideoCall: React.FC = ({ rtcProps, - breakoutChannelDetails, + breakoutJoinChannelDetails, onLeave, callActive, callbacks, styleProps, }) => { - const {setBreakoutRoomChannelData} = useBreakoutRoomInfo(); + const {setBreakoutRoomChannelInfo} = useSetBreakoutRoomInfo(); const [isRecordingActive, setRecordingActive] = useState(false); const [sttAutoStarted, setSttAutoStarted] = useState(false); const [recordingAutoStarted, setRecordingAutoStarted] = useState(false); const [breakoutRoomRTCProps, setBreakoutRoomRtcProps] = useState({ ...rtcProps, - channel: breakoutChannelDetails.channel, - uid: breakoutChannelDetails.uid as number, - token: breakoutChannelDetails.token, - rtm: breakoutChannelDetails.rtmToken, - screenShareUid: breakoutChannelDetails?.screenShareUid as number, - screenShareToken: breakoutChannelDetails?.screenShareToken || '', + channel: breakoutJoinChannelDetails.channel_name, + uid: breakoutJoinChannelDetails.mainUser.uid as number, + token: breakoutJoinChannelDetails.mainUser.rtc, + rtm: breakoutJoinChannelDetails.mainUser.rtm, + screenShareUid: breakoutJoinChannelDetails?.screenShare.uid as number, + screenShareToken: breakoutJoinChannelDetails?.screenShare.rtc, }); // Set breakout room data when component mounts useEffect(() => { - setBreakoutRoomChannelData({ - channelId: breakoutChannelDetails.channel, + setBreakoutRoomChannelInfo({ isBreakoutMode: true, + ...breakoutJoinChannelDetails, }); - }, [breakoutChannelDetails.channel, setBreakoutRoomChannelData]); + }, [breakoutJoinChannelDetails]); return ( >; @@ -63,8 +54,9 @@ const VideoCallContent: React.FC = props => { const getDisplayName = useMainRoomUserDisplayName(); // Breakout channel details (populated by RTM events) - const [breakoutChannelDetails, setBreakoutChannelDetails] = - useState(null); + const [breakoutJoinChannelDetails, setBreakoutJoinChannelDetails] = useState< + BreakoutChannelJoinEventPayload['data']['data'] | null + >(null); // Track transition direction for better UX const [transitionDirection, setTransitionDirection] = useState< @@ -91,26 +83,15 @@ const VideoCallContent: React.FC = props => { sessionStorage.setItem('breakout_room_transition', 'true'); console.log('Set breakout transition flag for channel join'); - // Extract breakout channel details - const breakoutDetails: BreakoutChannelDetails = { - channel: channel_name, - token: mainUser.rtc, - uid: mainUser?.uid || 0, - screenShareToken: screenShare.rtc, - screenShareUid: screenShare.uid, - rtmToken: mainUser.rtm, - }; - // Set breakout room info using the new system - setBreakoutRoomChannelInfo(data); // Set breakout state active history.push(`/${phrase}?breakout=true`); - setBreakoutChannelDetails(null); + setBreakoutJoinChannelDetails(null); setTransitionDirection('enter'); // Set direction for entering // Add state after a delay to show transitioning screen breakoutTimeoutRef.current = setTimeout(() => { - setBreakoutChannelDetails(prev => ({ + setBreakoutJoinChannelDetails(prev => ({ ...prev, - ...breakoutDetails, + ...data.data.data, })); breakoutTimeoutRef.current = null; }, 800); @@ -176,7 +157,7 @@ const VideoCallContent: React.FC = props => { // Set direction for exiting setTransitionDirection('exit'); // Clear breakout channel details to show transition - setBreakoutChannelDetails(null); + setBreakoutJoinChannelDetails(null); // Navigate back to main room after a delay setTimeout(() => { history.push(`/${phrase}`); @@ -185,7 +166,7 @@ const VideoCallContent: React.FC = props => { // Route protection: Prevent direct navigation to breakout route useEffect(() => { - if (isBreakoutMode && !breakoutChannelDetails) { + if (isBreakoutMode && !breakoutJoinChannelDetails) { // If user navigated to breakout route without valid channel details, // redirect to main room after a short delay to prevent infinite loops const redirectTimer = setTimeout(() => { @@ -195,18 +176,18 @@ const VideoCallContent: React.FC = props => { return () => clearTimeout(redirectTimer); } - }, [isBreakoutMode, breakoutChannelDetails, history, phrase]); + }, [isBreakoutMode, breakoutJoinChannelDetails, history, phrase]); // Conditional rendering based on URL params return ( <> {isBreakoutMode ? ( - breakoutChannelDetails?.channel ? ( + breakoutJoinChannelDetails?.channel_name ? ( // Breakout Room Mode - Fresh component instance @@ -215,7 +196,7 @@ const VideoCallContent: React.FC = props => { { - setBreakoutChannelDetails(null); + setBreakoutJoinChannelDetails(null); }} /> ) diff --git a/template/src/rtm/RTMCoreProvider.tsx b/template/src/rtm/RTMCoreProvider.tsx index c43790515..ceb335b7a 100644 --- a/template/src/rtm/RTMCoreProvider.tsx +++ b/template/src/rtm/RTMCoreProvider.tsx @@ -15,14 +15,48 @@ import type { StorageEvent, Metadata, SetOrUpdateUserMetadataOptions, + RtmLinkState, } from 'agora-react-native-rtm'; import {UidType} from '../../agora-rn-uikit'; import RTMEngine from '../rtm/RTMEngine'; import {isWeb, isWebInternal} from '../utils/common'; import isSDK from '../utils/isSDK'; import {useAsyncEffect} from '../utils/useAsyncEffect'; +import {nativeLinkStateMapping} from '../../bridge/rtm/web/Types'; -// Event callback types +// ---- Helpers ---- // +const delay = (ms: number) => new Promise(r => setTimeout(r, ms)); + +async function loginWithBackoff( + rtmClient: RTMClient, + token: string, + onAttempt?: (n: number) => void, + maxAttempts = 5, +) { + let attempt = 0; + while (attempt <= maxAttempts) { + try { + try { + await rtmClient.logout(); + } catch {} + await delay(300); + await rtmClient.login({token}); + return; // success + } catch (e: any) { + attempt += 1; + onAttempt?.(attempt); + if (attempt > maxAttempts) { + throw new Error(`RTM login failed: ${e?.message ?? e}`); + } + const backoff = + Math.min(1000 * 2 ** (attempt - 1), 30_000) + + Math.floor(Math.random() * 300); + await delay(backoff); + } + } +} + +// ---- Context ---- // type MessageCallback = (message: MessageEvent) => void; type PresenceCallback = (presence: PresenceEvent) => void; type StorageCallback = (storage: StorageEvent) => void; @@ -35,17 +69,16 @@ interface EventCallbacks { interface RTMContextType { client: RTMClient | null; - connectionState: number; + connectionState: RtmLinkState; error: Error | null; isLoggedIn: boolean; - // Callback registration methods registerCallbacks: (channelName: string, callbacks: EventCallbacks) => void; unregisterCallbacks: (channelName: string) => void; } const RTMContext = createContext({ client: null, - connectionState: 0, + connectionState: nativeLinkStateMapping.IDLE, error: null, isLoggedIn: false, registerCallbacks: () => {}, @@ -71,10 +104,19 @@ export const RTMCoreProvider: React.FC = ({ const [connectionState, setConnectionState] = useState(0); console.log('supriya-rtm connectionState: ', connectionState); const [error, setError] = useState(null); - // Callback registration storage + + const mountedRef = useRef(true); + const cleaningRef = useRef(false); const callbackRegistry = useRef>(new Map()); - // Memoize userInfo to prevent unnecessary re-renders + useEffect(() => { + mountedRef.current = true; + return () => { + mountedRef.current = false; + }; + }, []); + + // Memoize userInfo const stableUserInfo = useMemo( () => ({ localUid: userInfo.localUid, @@ -90,72 +132,35 @@ export const RTMCoreProvider: React.FC = ({ ], ); - // Login function - const loginToRTM = async ( - rtmClient: RTMClient, - loginToken: string, - retryCount = 0, - ) => { - try { - try { - // 1. Handle ghost sessions, so do logout to leave any ghost sessions - await rtmClient.logout(); - // 2. Wait for sometime - await new Promise(resolve => setTimeout(resolve, 500)); - // 3. Login again - await rtmClient.login({token: loginToken}); - // 4. Wait for sometime - await new Promise(resolve => setTimeout(resolve, 500)); - } catch (logoutError) { - console.log('logoutError: ', logoutError); - } - } catch (loginError) { - if (retryCount < 5) { - // Retry with exponential backoff (capped at 30s) - const delay = Math.min(1000 * Math.pow(2, retryCount), 30000); - await new Promise(resolve => setTimeout(resolve, delay)); - return loginToRTM(rtmClient, loginToken, retryCount + 1); - } else { - const contextError = new Error( - `RTM login failed after retries: ${error.message}`, - ); - setError(contextError); - } - } - }; - const setAttribute = useCallback(async (rtmClient: RTMClient, userInfo) => { const rtmAttributes = [ {key: 'screenUid', value: String(userInfo.screenShareUid)}, {key: 'isHost', value: String(userInfo.isHost)}, ]; try { - const data: Metadata = { - items: rtmAttributes, - }; + const data: Metadata = {items: rtmAttributes}; const options: SetOrUpdateUserMetadataOptions = { userId: `${userInfo.localUid}`, }; - await rtmClient.storage.removeUserMetadata(); + // await rtmClient.storage.removeUserMetadata(); await rtmClient.storage.setUserMetadata(data, options); } catch (setAttributeError) { console.log('setAttributeError: ', setAttributeError); } }, []); - // Callback registration methods - const registerCallbacks = ( - channelName: string, - callbacks: EventCallbacks, - ) => { - callbackRegistry.current.set(channelName, callbacks); - }; + const registerCallbacks = useCallback( + (channelName: string, callbacks: EventCallbacks) => { + callbackRegistry.current.set(channelName, callbacks); + }, + [], + ); - const unregisterCallbacks = (channelName: string) => { + const unregisterCallbacks = useCallback((channelName: string) => { callbackRegistry.current.delete(channelName); - }; + }, []); - // Global event listeners - centralized in RTMCoreProvider + // Global event listeners useEffect(() => { if (!client || !userInfo?.localUid) { return; @@ -168,12 +173,10 @@ export const RTMCoreProvider: React.FC = ({ ); // Distribute to all registered callbacks callbackRegistry.current.forEach((callbacks, channelName) => { - if (callbacks.storage) { - try { - callbacks.storage(storage); - } catch (globalStorageCbError) { - console.log('globalStorageCbError: ', globalStorageCbError); - } + try { + callbacks.storage?.(storage); + } catch (globalStorageCbError) { + console.log('globalStorageCbError: ', globalStorageCbError); } }); }; @@ -186,12 +189,10 @@ export const RTMCoreProvider: React.FC = ({ ); // Distribute to all registered callbacks callbackRegistry.current.forEach((callbacks, channelName) => { - if (callbacks.presence) { - try { - callbacks.presence(presence); - } catch (globalPresenceCbError) { - console.log('globalPresenceCbError: ', globalPresenceCbError); - } + try { + callbacks.presence?.(presence); + } catch (globalPresenceCbError) { + console.log('globalPresenceCbError: ', globalPresenceCbError); } }); }; @@ -211,12 +212,10 @@ export const RTMCoreProvider: React.FC = ({ } // Distribute to all registered callbacks callbackRegistry.current.forEach((callbacks, channelName) => { - if (callbacks.message) { - try { - callbacks.message(message); - } catch (globalMessageCbError) { - console.log('globalMessageCbError: ', globalMessageCbError); - } + try { + callbacks.message?.(message); + } catch (globalMessageCbError) { + console.log('globalMessageCbError: ', globalMessageCbError); } }); }; @@ -233,11 +232,48 @@ export const RTMCoreProvider: React.FC = ({ }; }, [client, userInfo?.localUid]); + // Link state listener for reconnects + useEffect(() => { + if (!client) { + return; + } + + const onLink = async (evt: LinkStateEvent) => { + setConnectionState(evt.currentState); + if (evt.currentState === nativeLinkStateMapping.DISCONNECTED) { + setIsLoggedIn(false); + if (stableUserInfo.rtmToken) { + try { + await loginWithBackoff(client, stableUserInfo.rtmToken); + if (!mountedRef.current) { + return; + } + setIsLoggedIn(true); + } catch (err: any) { + if (!mountedRef.current) { + return; + } + setError(err); + } + } + } else if (evt.currentState === nativeLinkStateMapping.CONNECTED) { + setIsLoggedIn(true); + } + }; + + client.addEventListener('linkState', onLink); + return () => { + client.removeEventListener('linkState', onLink); + }; + }, [client, stableUserInfo, setAttribute]); + + // Initialize RTM useEffect(() => { if (client) { return; } - const initializeRTM = async () => { + + (async () => { console.log('supriya-rtm-lifecycle init'); // 1, Check if engine is already connected // 2. Initialize RTM Engine @@ -250,41 +286,37 @@ export const RTMCoreProvider: React.FC = ({ } // 3. Set client after successful setup setClient(rtmClient); - // 4 .Global linkState listener - const onLink = async (evt: LinkStateEvent) => { - setConnectionState(evt.currentState); - if (evt.currentState === 0 /* DISCONNECTED */) { - setIsLoggedIn(false); - console.warn('RTM disconnected. Attempting re-login...'); - if (stableUserInfo.rtmToken) { - try { - await loginToRTM(rtmClient, stableUserInfo.rtmToken); - await setAttribute(rtmClient, stableUserInfo); - console.log('RTM re-login successful.'); - } catch (err) { - console.error('RTM re-login failed:', err); - } - } - } - }; - rtmClient.addEventListener('linkState', onLink); try { - // 5. Client Login if (stableUserInfo.rtmToken) { - await loginToRTM(rtmClient, stableUserInfo.rtmToken); + await loginWithBackoff(rtmClient, stableUserInfo.rtmToken); await setAttribute(rtmClient, stableUserInfo); + if (!mountedRef.current) { + return; + } setIsLoggedIn(true); } - } catch (err) { - console.error('RTM login failed', err); + } catch (err: any) { + if (!mountedRef.current) { + return; + } + setError(err); } - }; - - initializeRTM(); + })(); }, [client, stableUserInfo, setAttribute]); - const cleanupRTM = async () => { + // Refresh attributes if userInfo changes while logged in + useEffect(() => { + if (client && isLoggedIn && stableUserInfo.rtmToken) { + setAttribute(client, stableUserInfo).catch(console.warn); + } + }, [client, isLoggedIn, stableUserInfo, setAttribute]); + + const cleanupRTM = useCallback(async () => { + if (cleaningRef.current) { + return; + } + cleaningRef.current = true; try { const engine = RTMEngine.getInstance(); if (engine?.engine) { @@ -295,60 +327,42 @@ export const RTMCoreProvider: React.FC = ({ setClient(null); } catch (err) { console.error('RTM cleanup failed:', err); + } finally { + cleaningRef.current = false; } - }; + }, []); useAsyncEffect(() => { return async () => { // Cleanup console.log('supriya-rtm-lifecycle cleanup'); await cleanupRTM(); - // if (currentClient) { - // console.log('supriya-rtm-lifecycle cleanup calling destroy'); - // await RTMEngine.getInstance().destroy(); - // console.log( - // 'supriya-rtm-lifecycle setting client null', - // RTMEngine.getInstance().engine, - // ); - // setClient(null); - // } }; }, []); - // Handle browser close/reload events for RTM cleanup + + // Browser unload cleanup useEffect(() => { - if (!$config.ENABLE_CONVERSATIONAL_AI && isWebInternal()) { + if ( + !$config.ENABLE_CONVERSATIONAL_AI && + isWebInternal() && + isWeb() && + !isSDK() + ) { const handleBrowserClose = (ev: BeforeUnloadEvent) => { ev.preventDefault(); - return (ev.returnValue = 'Are you sure you want to exit?'); + ev.returnValue = 'Are you sure you want to exit?'; }; - const handleRTMCleanup = () => { - console.log('Browser closing: calling cleanupRTM()'); - // Fire-and-forget, no await because page is unloading cleanupRTM(); - // Optional: add beacon for debugging - // navigator.sendBeacon?.( - // '/cleanup-log', - // JSON.stringify({msg: 'RTM cleanup triggered on pagehide'}), - // ); }; - - window.addEventListener( - 'beforeunload', - isWeb() && !isSDK() ? handleBrowserClose : () => {}, - ); - + window.addEventListener('beforeunload', handleBrowserClose); window.addEventListener('pagehide', handleRTMCleanup); - return () => { - window.removeEventListener( - 'beforeunload', - isWeb() && !isSDK() ? handleBrowserClose : () => {}, - ); + window.removeEventListener('beforeunload', handleBrowserClose); window.removeEventListener('pagehide', handleRTMCleanup); }; } - }, [client, isLoggedIn]); + }, [cleanupRTM]); return ( = ({ ); }; -export const useRTMCore = () => { - const context = useContext(RTMContext); - if (!context) { - throw new Error('useRTMCore must be used within RTMCoreProvider'); - } - return context; -}; +export const useRTMCore = () => useContext(RTMContext); From e1340dbdc2ebab6b7b12e2ec73d95a07183358a4 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 11:36:57 +0530 Subject: [PATCH 12/56] add config.light --- config.light.json | 118 ++++++++++++++++++++++++---------------------- 1 file changed, 61 insertions(+), 57 deletions(-) diff --git a/config.light.json b/config.light.json index 01c1388e2..8f295ea69 100644 --- a/config.light.json +++ b/config.light.json @@ -1,23 +1,6 @@ { - "PROJECT_ID": "49c705c1c9efb71000d7", - "APP_ID": "a569f8fb0309417780b793786b534a86", - "APP_CERTIFICATE": "6545ecd19d554737be863eb1eaaf9cee", - "CUSTOMER_ID": "40b25d211955491580720cb54099c3c4", - "CUSTOMER_CERTIFICATE": "555d0c42035c450a9b562ec20773d6b4", - "PRODUCT_ID": "helloworld", - "APP_NAME": "HelloWorld", - "LOGO": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKIAAAA5CAYAAAC1U/CbAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABFwSURBVHgB7Z0NnFTVdcDPue/NzJuFBSV+EBEFAvxsCjZVUxXUfGgNajWkiRoTg4GdXVBQSLStpmUFTWt+KthE4rK7s7uuJmqoaaOm1aZNFaI0mlhav1JTMVsMMcSA4sLMezPv3ZNzZ0H36933MW83+Nv5/37+lpl75r77cd6955577hUhBuNb6CgvC7PA9Y4hgJQAKEgydgsLXil8AV8Pm8+4LppMrndqodF8CKpk0jdogpMuT5cpMZMI6oAAoQos03j4rcX4FlTBuPZ9R0svMwNSOJXLlOEC2QS0B8rmdvtK7IFqaN0zMYP1p6Ew/9fO4f+H/t0assZPLs3wTOPEYs58AGJS10rvl+jORsTJXDdTGNBb4rq5wnwBIrSb1UrHUcr9g9CdZbWXPwooPsv/XMj/He0vib8ApM0A8h67IfX4cBLpDvdTQoqVLPcR/mjbOSMLMchspFloykWscxci0Inq4ZAQHooPlRvwfyAiVr58FkpYSEJ8hj9O1Yju4pd4iyB3U7Ex8yBEfQ53IBjygALi04TY7DTgD3zlO7mtPe8qErgACSao77jdw7fXespmJsJ8lN4V/LwF3NJH+MoS/JzTt3jotZYb0j8dtjx5N8f1X4aAJ7NsT2BBrHZ3MXf09YgwG6LTA5LW2k3m3ZVPm8jI7PU28luU6ycTWRHNtvI8fgX/lmusFDkx5XsXfM3OieOi/CKTLy8Awq9y3U6G6PSw4t7h7IANsIbVOAQDFbEP7tgNzgSxCi5B7+B36TzNEUDf5NSzBucRShEfJzPzilyJSDdwu7wPIsIzwLNCwLriEvN+9XnSt2jC/gLdh4IueEeIFdH0y2BCniaVyNsAiJdV0dPTQGCX1eGdbpbEavdtuRwGKmFkMp30JZByLVexHkYM+cOwkqqdHHS/giS+zA0at6mmoZRft6bQn0FLscm+MtsDMeCHr7B6YbfNE7D6nMkTzxReVxwFUtR10DHeduriQejcuO+7GvFIwn2ZvHeSUxB/s7/grkEhLhgqNwzZPM0nkN8F7RQcmV0++YUaEVWjSKD7gCqj4EjyWyiXPhxGGbijZyLIfwP1wiUFwmus1NcWc/gPOrHhRsR3kdxGJo+A8mZdHroRMdvhXiIJ21hgIiQEj9hvcX6HDUngEVEM/q4u714gyVMjQpJKCNXkpxpdEv1oFJSQeMq4JpQSttFsVsInIUklrJQApvIgsKky8sfOQjwUpIQ6ePS6iggfSFIJFcMq4QEGKKLVQtM8wk1s52TgkIEQDI9HB5oBIwiv/N5EFBcftGV0ZFtoCorKSJj0y/oOPFWvt/LFs2L8VNvhQaS7nBP49xtgRGxvf95VxFZKQYp+yPZAHRxCWHn1ZuOfwAiDAtNc918EChIJSknlDYi0mIlXqPS36lvfPgJGCTUQCc98FEZZCRXvKKJleq0hR53dRPI2cuV5WBbH1lmCh28xjb87l4u/kTsqvE8rAF6xX85//jpIjkezAo/iD/L0/TkwStPrCmJipiQmeeSdznbJ1SzxUwjOZJz05COHt5J2OrI6YTX/mQVB2fXZQ3fwqvEi1U7jUUww9hUn81B3JiffAsqjEJzJ1LIx7uuQHHuAJHsb5DkiI44ZkpqmjRDC1OC6qT6+CYQ8W6CYonSAbZoPsElwHrdjN6fthohUND/dRXOFJ58LFCa57rCjzebXL8KCr1DrTi7X5Os559UQDt/FCq+2t3OtZwQUaiuWjEuKV+JOnRi7MS5mm6490O4haLYbjeHtq06aannyZa5bVl8kuDNjiuYgh3g2763gTr0TAuAOvsjJ4SP9v9MvVoYpE8j14503b3xj+VH7hkvvW2HLhwMzknCT/StxM7uZXD8R5ch30WrmFfNVEK5wfYsVIeWXg2TZz7Wy2Ji6TquEiqVTCtyRzfyvr0AVpDvdzwQqIUC3LYyzg5RQUVKrUBQnsftI33kIq6CLrOGSLA8+H6SEnH5DsdG4JsyuTDFnbBCSLuR6FvRZUmD/+MGzRRklXVrMpa71U8LKM4iWafNRIzznYzcZN+qUULG/cfwuJ2eyq67PjRQGAbxdx0/5vE6IK/MlZwl+AyLAo9wtXLvFEBPeefmiVoCgxW4Qi3k7yYaQ8M7Dq+CiWgDotiEnpV3v/GFTUC7V/I5997LZbjC+BhEoNJnfZ0VboornL0UfTW90PgjxWFlsMjfpBKx2ms59db5GhKQU5wblMxhui7XcT2vDyIp0yjuH/6Z8JRC6nEbj7yEGdoN5NxFF3r6Cu+hw7lZdw/SkHLGaRzeCqGVaijt4FNK+/ULgpYO/y3bQ6aCznxCfLjWmYrlMio3md7irb9XJiLT5KYhON/ddS5AQGXChNp3k7eUm/AnEgGfHNdw4TwTJcZvDOToB8kSkN3zIA8BYBxHJmO7Jqmf90jmhpXcFRjaID8Kj0MPsJ9vuKyBhiNtESnceaCBP/h1UgS3NdfxW7ffNX6L2+cPBo9htYeTQ8z6hSSYBZqTZcJgnfDVIgrch4QT/ZHrGacKfQxUUG/HH7KANXAgNKJQQ2v1aiSL6KDsIFN69/okwWRnc/b/i1eGp4M8up8l4BKphKe4V5B+0wK/lSRABVuptpSZ8MZSswBP886F/4T78JVSB/UtQ7i7twMGLFZztX0DaAgkgAJ+MIs+rxOP90+DViq1XLQg/0iWXIH3swOdWpmafMtG2ylq5SiS4/nvcRJPhHhoHIeEVcLgXYw2ZXPLpfskCcTNUyxqUbAf/u05ErZr9vfAevQYJIDGa4vAI6uvE5Qol46d0TG2ZBIiBuyboHzggkKqaNQ7Cnolf6dKz+8PvmJA0Q9l09e/vVXn6mkGeFNshAaQkbd1ExQTwAw0HkoANqCji/Ib6jy4IHiQA1oMb6QdS004SItXPD/RA7wEwI+x4SDdU/aSoN2AUYNNCq0uCAHs1P54MSRSC8KhIPwDSGe3HQhLsc3VBq1x3Y6DPDcXbfrK8qxArzGrIMw393rUloRcSZn/Jf4GkQCwfCUmAOEWXzItmucsvkVeWyezxCjwlkjyQr3HML8eMoG24MEgD5+rSyYSBTnKSvoY/D9/zIQkknqFJLbzJCxpImuXILxy+4ZeMYPwxJAAPeCfq0tkWNfzD4RHmqbMgUA0tveww1zbwUAxTZ5ekC+idCVXCq+DzfBMJHKdnYAAE+0Of8RPn+XJGppviRLAPzujj/on0PIwY0r+9CRbygkZAFVhdNI3b6I90Muzbpaf9Ennb57BCVl4N1RTCrGtUf6L8Bh3QrrBQwF9CFWTuKs5gM3Sh/wNo65CQfYF6D4ILseMHFVaHu4j/+K5eed/2WRghSMV6+j/46OwU71KoAt6WuSFIRqRlWbvRzT6m6ypbQDFQYUU8bV0PEenbO9ZFzOCZmby3EuLA9oZIpf9JK4MwJCbRwTe3cF187UQkuUwdnIIYqHbiWUO7Fca7G9+FEQIN85916YS4Xp3agxjUddJJvPhsCpITvU0Wux7oP/0E1KjIQ/fj2c7CVIiA1U3HgUmb2aYbDzGQhNqG56H+tnS7++cQkUyH10YIWnsF3NK/Dvmu4chenp8e1f4OxP1Wno6HCBzWxe2bpqDjBj12Y+o/YISwl+BmtuF0TuvJZNAj9d0UaVFmtRene1KGKnff3C/hm1ophONJZrao8HgIQaqdToOyt4W1OHbwaKksWrlxdMZ5ig3cTaxYfwEhqL+Pjsjk3Yd5igs4vEX32kvrdgybQkLfTgDHsNTWrIocCoE6DmtL+RRnPFMnxy9d0HOrhv2zrbp0NmVOLpfl01ZLcRqEINNWPgcw/VTY4wZ9fik2RjNT5E9CbSMh3M3eyW47Z24eEHTQSilLuPP5O7YJ8TKA0D4v/3jEvKfCycJEb7zAfrOvpTLpx3qvGLgHPb6F5rhp+Wm2g1axEuodwsSWnluapTuzksnL73GnfRIC4OnsQbYr25xe2ALX4Ls+NG5r61j3DBZYxA7sy3jGqQso0yt2lrc8L8cBZkFQPKIKXHaWpR6DsHRQvUXeS9xtQe4x5cft9ki0l3n7dkCKOvtcD7ww9VYh4vkQlv7nmit2Skr+DEIuLNQGPXesWsnZ/OF9hJWVUZwjnv6n+LhiVj2xwtOHISyIL3F5dvNLlSVSB9wp/LkSFcYVEEFTOWIp5csRTI4yd8rzXBZuL5gggY6PcqbEtN05+1ZkhriOAgNjDfExe3Fw1Et/xrWVP+EJEVp5WQf2ct1U2cr835EkWQfiHDUZfMA+2+6tYvvpDhhdtMdJMx00A8l7Ju7Z3LCoEcxpEBeHkbXy7he4PCokfmTPdpC8mV+M5uGSgkdEMdtZhv8HEeFZSM1AzTCaDD5OWlRxh1WGMyWNCnBwhXER/7MAI8eLWaFMinCwWXIvv/nXwcjS7qeEIXg1jhIqeFC4kUfvNhhlhjgq7aUpdVjpJjiEcJfgVhelipv8NSSNpCdTKfGRqBcuFRuM9expvLYSv54w6jin3SCWQkxIQlXxg06jWEYgbodRZFiPuXorUNDnlA0ACaDOOwgQy7mJX4aYmAVXKWHoYwFhIQNf7l0EeyAGxSZjPYHxyaROLnI7OazW1xVzxtVxos8P0OOURQdUBZKTQ+WNuBEgmSATrtxvePNEbSIM65Hw3bpRB82xLD7EjRx44Fz/fHxIkJhbyOFdbLYvgDDHKAdh3VmcDtnME5D0rQpQifRpsDrl3crRDTFQp+t4P/YM/nV10xnREzwKneI0Ro9o78cOdtef3bd/XD08IN3Ei56Z/FL8AOLC7gpWgg6zKOaUGs2HyBZ/CsMoY6jGT+edOQLNVZyhCikPjn7BSjRuOyvgJlbAbf2Tsp00lXcJ1nIBv3jgOEDg3TcV53hJ+SUxkrM4Cjz83O7kjFA+ST8qO0kpdzlvRy3kms0M8cy3BMG3IS3uKV6Bz0AE+i9WqG+m2GA6Yt3+5Zi8+QKVfptHnsc7JLxHjxQmmup11oM8mqKV6zYggKTioUl7t/AK8bOVL8JcSzeYVHvpNBCpEwzyVMSJcnBnePPXYd/a81LScwbIZws7U9uCrldTOxDkwQIwvHOdnPnpoOdmumgWuPK/gtwm3Ckuy+zl9zAd3Z1Ei9RCBBJAvbzsQzmRXVynsIvjD9kYHc8bB8rN8RIPvs+ZKe/H++emXoRTsAwxOKCIt3I9N2dNcX+1l4qGhgtvtbnzKWXORSlP5c8zWJHUFTUFJPxvHv5ekBlja2kR/iwoK+XQBwMuQPQ+OLLuh4QxO8pnGKSmwiGOabbxZCsK+H5xn/nsQQeyutm2ZHhnCoHKNrk8KH91/006LWYNdorXGHneU4qosDrKH+OpTzld0+qzulXBkmJ1UKxeppU+gEbl4iRtAAePqLfyFP1XUGNUec8pooIdyldw0bukgNWlJerm2JDk906ycNxjbJvodmr2WYaYOmpTXY0KVQU8/r5gO66bR8WPR1JCRW7iHts0VECs7uDUeNv1Am3WGsnynlREhd0YbR/1HZT9R7JBJ8IukDi3KtSogvesIlYDb509ob8Gg+bF9SvWiMeYVEQF+1l9o7RZAw/P5mEK1Bg1xqwiCkNoL+90oZzMsdUaoRi7I2IZtPe5iITOKtcIx5hVRPZC6uvuUjKb/TVCMWYVET1XawOiYWpvQKiRLGNWEaUU+vM5aO+AGqPG2J2aBehO871h57LJ3DpWIxQmjEEOHIT3PdtMI3irQo3hGXMj4sRvq/u5RbdOBqX7HagxqowpRVRHQZ0iPQX6SO89dsmu7hriGpEZM1OzivKWZakuUgqK8v5HWDGhFo84yoyZEdHOwk5CDLjaDd+AcilaRE+NRBg7U/Ml6DmA6mTiNj8RBLwm7v+0u0Z1jK3FSgP2OoZQkTVbh6QRrC3m8AGo8Xth7PkR1f8yLW1c1v8sMrtrvmfvFIfUpQJjjTEbc1e5U0d6TwLio7YnlsHSeKfpatSomsolmTUOCX4Hn8i63ZAyQc4AAAAASUVORK5CYII=", - "ICON": "logoSquare.png", - "FRONTEND_ENDPOINT": "https://app-builder-core-light-git-preprod-agoraio.vercel.app", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "WEB", - "SCREEN_SHARING": true, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform", - "ENCRYPTION_ENABLED": false, + "APP_ID": "aae40f7b5ab348f2a27e992c9f3e13a7", + "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, "ENABLE_APPLE_OAUTH": false, "ENABLE_SLACK_OAUTH": false, @@ -26,6 +9,11 @@ "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", + "PROJECT_ID": "8e5a647465fb0a5e9006", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", "BUCKET_NAME": "", "BUCKET_ACCESS_KEY": "", "BUCKET_ACCESS_SECRET": "", @@ -38,64 +26,80 @@ "PSTN_EMAIL": "", "PSTN_ACCOUNT": "", "PSTN_PASSWORD": "", - "RECORDING_REGION": 0, + "RECORDING_REGION": 3, "GEO_FENCING": true, - "GEO_FENCING_INCLUDE_AREA": "GLOBAL", - "GEO_FENCING_EXCLUDE_AREA": "CHINA", "LOG_ENABLED": true, "EVENT_MODE": false, "RAISE_HAND": false, "AUDIO_ROOM": false, - "BG": "https://dbudicf5k4as1.cloudfront.net/10/Artboard.png", - "PROFILE": "720p_3", + "PRODUCT_ID": "breakoutroomfeature", + "APP_NAME": "BreakoutRoomFeature", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1573238", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "BG": "", + "PRIMARY_FONT_COLOR": "#363636", + "SECONDARY_FONT_COLOR": "#FFFFFF", + "SENTRY_DSN": "", + "PROFILE": "480p_8", "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, + "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", - "SECONDARY_ACTION_COLOR": "#19394D", - "FONT_COLOR": "#333333", - "BACKGROUND_COLOR": "#FFFFFF", + "SECONDARY_ACTION_COLOR": "#FFFFFF", + "FONT_COLOR": "#FFFFFF", + "BACKGROUND_COLOR": "#111111", "VIDEO_AUDIO_TILE_COLOR": "#222222", - "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#80808080", + "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#000004", "VIDEO_AUDIO_TILE_TEXT_COLOR": "#FFFFFF", - "VIDEO_AUDIO_TILE_AVATAR_COLOR": "#446881", + "VIDEO_AUDIO_TILE_AVATAR_COLOR": "#BDD0DB", "SEMANTIC_ERROR": "#FF414D", "SEMANTIC_SUCCESS": "#36B37E", "SEMANTIC_WARNING": "#FFAB00", "SEMANTIC_NEUTRAL": "#808080", - "INPUT_FIELD_BACKGROUND_COLOR": "#FFFFFF", - "INPUT_FIELD_BORDER_COLOR": "#333333", - "CARD_LAYER_1_COLOR": "#FFFFFF", - "CARD_LAYER_2_COLOR": "#F0F0F0", - "CARD_LAYER_3_COLOR": "#E3E3E3", - "CARD_LAYER_4_COLOR": "#FFFFFF", + "INPUT_FIELD_BACKGROUND_COLOR": "#181818", + "INPUT_FIELD_BORDER_COLOR": "#262626", + "CARD_LAYER_1_COLOR": "#1D1D1D", + "CARD_LAYER_2_COLOR": "#262626", + "CARD_LAYER_3_COLOR": "#2D2D2D", + "CARD_LAYER_4_COLOR": "#333333", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_TEXT": true, - "ICON_BG_COLOR": "#EBF1F5", - "TOOLBAR_COLOR": "#FFFFFF00", + "ICON_BG_COLOR": "#242529", + "TOOLBAR_COLOR": "#111111", "ACTIVE_SPEAKER": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_IDP_AUTH": false, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, - "ENABLE_NOISE_CANCELLATION": true, + "WHITEBOARD_APPIDENTIFIER": "", + "WHITEBOARD_REGION": "us-sv", + "ENABLE_NOISE_CANCELLATION": false, "ENABLE_VIRTUAL_BACKGROUND": true, - "ENABLE_WHITEBOARD": true, + "ENABLE_WHITEBOARD": false, "ENABLE_WHITEBOARD_FILE_UPLOAD": false, + "ENABLE_CHAT_NOTIFICATION": true, + "ENABLE_CHAT_OPTION": true, "ENABLE_WAITING_ROOM": false, - "WHITEBOARD_APPIDENTIFIER": "EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "41754367", - "CHAT_APP_NAME": "1042822", - "CHAT_URL": "https://a41.chat.agora.io", - "ENABLE_NOISE_CANCELLATION_BY_DEFAULT": true, - "DISABLE_LANDSCAPE_MODE": false, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false, - "ENABLE_WAITING_ROOM_AUTO_APPROVAL": true, - "ENABLE_WAITING_ROOM_AUTO_REQUEST": true, - "ENABLE_TEXT_TRACKS": false + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, + "SDK_CODEC": "vp8", + "ENABLE_BREAKOUT_ROOM": true } From 37a32ebb475af962cb728873e63f77c1a90420f8 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 11:40:11 +0530 Subject: [PATCH 13/56] not for event mode --- .../src/components/controls/useControlPermissionMatrix.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/template/src/components/controls/useControlPermissionMatrix.tsx b/template/src/components/controls/useControlPermissionMatrix.tsx index 4b4302c35..4cf681b7c 100644 --- a/template/src/components/controls/useControlPermissionMatrix.tsx +++ b/template/src/components/controls/useControlPermissionMatrix.tsx @@ -47,7 +47,8 @@ export const controlPermissionMatrix: Record< breakoutRoom: () => $config.ENABLE_BREAKOUT_ROOM && ENABLE_AUTH && - !$config.ENABLE_CONVERSATIONAL_AI, + !$config.ENABLE_CONVERSATIONAL_AI && + !$config.EVENT_MODE, }; export const useControlPermissionMatrix = ( From 77b7f94917b7b189448cbc145251b76dd675c095 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 12:01:46 +0530 Subject: [PATCH 14/56] rename object --- .../src/pages/video-call/VideoCallStateWrapper.tsx | 6 +++++- template/src/rtm/RTMGlobalStateProvider.tsx | 11 +++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/template/src/pages/video-call/VideoCallStateWrapper.tsx b/template/src/pages/video-call/VideoCallStateWrapper.tsx index 2ac3eb6e7..c476cff43 100644 --- a/template/src/pages/video-call/VideoCallStateWrapper.tsx +++ b/template/src/pages/video-call/VideoCallStateWrapper.tsx @@ -418,7 +418,11 @@ const VideoCallStateWrapper = () => { isHost: rtcProps.role === ClientRoleType.ClientRoleBroadcaster, rtmToken: rtcProps.rtm, }}> - + ; + rtmLoginInfo: { + uid: UidType; + channel: string; + }; } // Context for message and storage handler registration @@ -83,10 +86,10 @@ const RTMGlobalStateContext = React.createContext<{ const RTMGlobalStateProvider: React.FC = ({ children, - mainChannelRtcProps, + rtmLoginInfo, }) => { - const mainChannelName = mainChannelRtcProps.channel; - const localUid = mainChannelRtcProps.uid; + const mainChannelName = rtmLoginInfo.channel; + const localUid = rtmLoginInfo.uid; const {client, isLoggedIn, registerCallbacks, unregisterCallbacks} = useRTMCore(); // Main room RTM users (RTM-specific data only) From f18172754353524b93b08103e862a4243d834630 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 12:38:45 +0530 Subject: [PATCH 15/56] config light --- config.light.json | 99 ++++++++++++++++++++++------------------------- 1 file changed, 47 insertions(+), 52 deletions(-) diff --git a/config.light.json b/config.light.json index 8f295ea69..76ac37dda 100644 --- a/config.light.json +++ b/config.light.json @@ -1,5 +1,22 @@ { + "PROJECT_ID": "8e5a647465fb0a5e9006", "APP_ID": "aae40f7b5ab348f2a27e992c9f3e13a7", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", + "PRODUCT_ID": "breakoutroomfeature", + "APP_NAME": "BreakoutRoomFeature", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "FRONTEND_ENDPOINT": "", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": true, + "PRECALL": true, + "CHAT": true, + "CLOUD_RECORDING": true, + "RECORDING_MODE": "MIX", + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, "ENABLE_APPLE_OAUTH": false, @@ -9,11 +26,6 @@ "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROJECT_ID": "8e5a647465fb0a5e9006", - "RECORDING_MODE": "MIX", - "APP_CERTIFICATE": "", - "CUSTOMER_ID": "", - "CUSTOMER_CERTIFICATE": "", "BUCKET_NAME": "", "BUCKET_ACCESS_KEY": "", "BUCKET_ACCESS_SECRET": "", @@ -32,74 +44,57 @@ "EVENT_MODE": false, "RAISE_HAND": false, "AUDIO_ROOM": false, - "PRODUCT_ID": "breakoutroomfeature", - "APP_NAME": "BreakoutRoomFeature", - "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", - "ICON": "", - "PRIMARY_COLOR": "#00AEFC", - "FRONTEND_ENDPOINT": "", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": false, - "PRECALL": true, - "CHAT": true, - "CHAT_ORG_NAME": "61394961", - "CHAT_APP_NAME": "1573238", - "CHAT_URL": "https://a61.chat.agora.io", - "CLOUD_RECORDING": true, - "SCREEN_SHARING": true, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", "BG": "", - "PRIMARY_FONT_COLOR": "#363636", - "SECONDARY_FONT_COLOR": "#FFFFFF", - "SENTRY_DSN": "", "PROFILE": "480p_8", "SCREEN_SHARE_PROFILE": "1080p_2", - "DISABLE_LANDSCAPE_MODE": true, - "ENABLE_IDP_AUTH": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_STT": true, - "ENABLE_TEXT_TRACKS": false, - "ENABLE_CONVERSATIONAL_AI": false, - "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", - "SECONDARY_ACTION_COLOR": "#FFFFFF", - "FONT_COLOR": "#FFFFFF", - "BACKGROUND_COLOR": "#111111", + "SECONDARY_ACTION_COLOR": "#19394D", + "FONT_COLOR": "#333333", + "BACKGROUND_COLOR": "#FFFFFF", "VIDEO_AUDIO_TILE_COLOR": "#222222", - "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#000004", + "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#80808080", "VIDEO_AUDIO_TILE_TEXT_COLOR": "#FFFFFF", - "VIDEO_AUDIO_TILE_AVATAR_COLOR": "#BDD0DB", + "VIDEO_AUDIO_TILE_AVATAR_COLOR": "#446881", "SEMANTIC_ERROR": "#FF414D", "SEMANTIC_SUCCESS": "#36B37E", "SEMANTIC_WARNING": "#FFAB00", "SEMANTIC_NEUTRAL": "#808080", - "INPUT_FIELD_BACKGROUND_COLOR": "#181818", - "INPUT_FIELD_BORDER_COLOR": "#262626", - "CARD_LAYER_1_COLOR": "#1D1D1D", - "CARD_LAYER_2_COLOR": "#262626", - "CARD_LAYER_3_COLOR": "#2D2D2D", - "CARD_LAYER_4_COLOR": "#333333", + "INPUT_FIELD_BACKGROUND_COLOR": "#FFFFFF", + "INPUT_FIELD_BORDER_COLOR": "#333333", + "CARD_LAYER_1_COLOR": "#FFFFFF", + "CARD_LAYER_2_COLOR": "#F0F0F0", + "CARD_LAYER_3_COLOR": "#E3E3E3", + "CARD_LAYER_4_COLOR": "#FFFFFF", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_BG_COLOR": "#242529", - "TOOLBAR_COLOR": "#111111", + "ICON_TEXT": true, + "ICON_BG_COLOR": "#EBF1F5", + "TOOLBAR_COLOR": "#FFFFFF00", "ACTIVE_SPEAKER": true, - "WHITEBOARD_APPIDENTIFIER": "", - "WHITEBOARD_REGION": "us-sv", - "ENABLE_NOISE_CANCELLATION": false, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_IDP_AUTH": true, + "ENABLE_STT": true, + "ENABLE_CAPTION": true, + "ENABLE_MEETING_TRANSCRIPT": true, + "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, "ENABLE_WHITEBOARD": false, "ENABLE_WHITEBOARD_FILE_UPLOAD": false, "ENABLE_CHAT_NOTIFICATION": true, "ENABLE_CHAT_OPTION": true, "ENABLE_WAITING_ROOM": false, - "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, - "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, + "WHITEBOARD_APPIDENTIFIER": "", + "WHITEBOARD_REGION": "us-sv", + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1573238", + "CHAT_URL": "https://a61.chat.agora.io", + "DISABLE_LANDSCAPE_MODE": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "AI_LAYOUT": "LAYOUT_TYPE_1", - "AI_AGENTS": null, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "SDK_CODEC": "vp8", - "ENABLE_BREAKOUT_ROOM": true + "ENABLE_BREAKOUT_ROOM": true, + "ENABLE_TEXT_TRACKS": false } From 5e32ffab51d24b463885eeb782d0521501eb56de Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 13:43:20 +0530 Subject: [PATCH 16/56] add correct announcement toast --- .../ui/BreakoutRoomGroupSettings.tsx | 7 +++++- .../chat-messages/useChatMessages.tsx | 24 +++++++++++++++---- .../src/components/chat/chatConfigure.tsx | 7 ++++-- template/src/subComponents/ChatContainer.tsx | 4 ++-- .../chat/ChatAnnouncementView.tsx | 10 ++++---- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index 361100d69..2db745f04 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -36,6 +36,7 @@ import {isWeb} from '../../../utils/common'; import {useRTMGlobalState} from '../../../rtm/RTMGlobalStateProvider'; import {useRaiseHand} from '../../raise-hand'; import Tooltip from '../../../atoms/Tooltip'; +import {useContent} from 'customization-api'; const BreakoutRoomGroupSettings = ({scrollOffset}) => { const { @@ -44,6 +45,7 @@ const BreakoutRoomGroupSettings = ({scrollOffset}) => { const localUid = useLocalUid(); const {sendChatSDKMessage} = useChatConfigure(); const {isUserHandRaised} = useRaiseHand(); + const {defaultContent} = useContent(); const { breakoutGroups, @@ -251,7 +253,10 @@ const BreakoutRoomGroupSettings = ({scrollOffset}) => { to: chat.group_id, ext: { from_platform: isWeb() ? 'web' : 'native', - isAnnouncementText: true, + announcement: { + sender: defaultContent[localUid]?.name, + heading: `${defaultContent[localUid]?.name} made an announcement.`, + }, }, }; sendChatSDKMessage(option); diff --git a/template/src/components/chat-messages/useChatMessages.tsx b/template/src/components/chat-messages/useChatMessages.tsx index 646bf8e4d..2da3a50bb 100644 --- a/template/src/components/chat-messages/useChatMessages.tsx +++ b/template/src/components/chat-messages/useChatMessages.tsx @@ -46,6 +46,11 @@ interface ChatMessagesProviderProps { children: React.ReactNode; callActive: boolean; } + +export enum ChatNotificationType { + ANNOUNCEMENT = 'ANNOUNCEMENT', +} + export enum ChatMessageType { /** * Text message. @@ -85,6 +90,10 @@ export enum ChatMessageType { COMBINE = 'combine', } +export interface AnnouncementMessage { + sender: string; + heading: string; +} export interface messageInterface { createdTimestamp: number; updatedTimestamp?: number; @@ -99,7 +108,7 @@ export interface messageInterface { reactions?: Reaction[]; replyToMsgId?: string; hide?: boolean; - isAnnouncementText?: boolean; + announcement?: AnnouncementMessage; } export enum SDKChatType { @@ -123,7 +132,7 @@ export interface ChatOption { channel?: string; msg?: string; replyToMsgId?: string; - isAnnouncementText?: boolean; + announcement?: AnnouncementMessage; }; url?: string; } @@ -180,6 +189,8 @@ interface ChatMessagesInterface { uid: string, isPrivateMessage?: boolean, msgType?: ChatMessageType, + forceStop?: boolean, + notificationType?: ChatNotificationType, ) => void; openPrivateChat: (toUid: UidType) => void; removeMessageFromStore: (msgId: string, isMsgRecalled: boolean) => void; @@ -358,7 +369,7 @@ const ChatMessagesProvider = (props: ChatMessagesProviderProps) => { fileName: body?.fileName, replyToMsgId: body?.replyToMsgId, hide: false, - isAnnouncementText: body?.isAnnouncementText, + announcement: body?.announcement, }, ]; }); @@ -526,6 +537,7 @@ const ChatMessagesProvider = (props: ChatMessagesProviderProps) => { isPrivateMessage: boolean = false, msgType: ChatMessageType, forceStop: boolean = false, + announcement: AnnouncementMessage, ) => { if (isUserBanedRef.current.isUserBaned) { return; @@ -690,8 +702,10 @@ const ChatMessagesProvider = (props: ChatMessagesProviderProps) => { primaryBtn: null, secondaryBtn: null, type: 'info', - leadingIconName: 'chat-nav', - text1: isPrivateMessage + leadingIconName: announcement ? 'announcement' : 'chat-nav', + text1: announcement + ? announcement.heading + : isPrivateMessage ? privateMessageLabel?.current() : //@ts-ignore defaultContentRef.current.defaultContent[uidAsNumber]?.name diff --git a/template/src/components/chat/chatConfigure.tsx b/template/src/components/chat/chatConfigure.tsx index b6f8694cb..47bfd374d 100644 --- a/template/src/components/chat/chatConfigure.tsx +++ b/template/src/components/chat/chatConfigure.tsx @@ -7,6 +7,7 @@ import {useLocalUid} from '../../../agora-rn-uikit'; import {UidType, useContent} from 'customization-api'; import { ChatMessageType, + ChatNotificationType, ChatOption, SDKChatType, useChatMessages, @@ -277,6 +278,8 @@ const ChatConfigure = ({children}) => { fromUser, false, message.type, + false, + message.ext?.announcement, ); addMessageToStore(Number(fromUser), { msg: message.msg.replace(/^(\n)+|(\n)+$/g, ''), @@ -285,7 +288,7 @@ const ChatConfigure = ({children}) => { isDeleted: false, type: ChatMessageType.TXT, replyToMsgId: message.ext?.replyToMsgId, - isAnnouncementText: message.ext?.isAnnouncementText || false, + announcement: message.ext?.announcement, }); } @@ -419,7 +422,7 @@ const ChatConfigure = ({children}) => { ext: option?.ext?.file_ext, fileName: option?.ext?.file_name, replyToMsgId: option?.ext?.replyToMsgId, - isAnnouncementText: option?.ext?.isAnnouncementText, + announcement: option?.ext?.announcement, }; // update local user message store diff --git a/template/src/subComponents/ChatContainer.tsx b/template/src/subComponents/ChatContainer.tsx index ed3679c3c..51e81cd0e 100644 --- a/template/src/subComponents/ChatContainer.tsx +++ b/template/src/subComponents/ChatContainer.tsx @@ -254,10 +254,10 @@ const ChatContainer = (props?: { ) : null} - {message?.isAnnouncementText ? ( + {message?.announcement ? ( ) : !message?.hide ? ( - Message from host: {senderName} + Message from host: {announcement.sender} {message} From 1a9dde8cd6b67f2c547d3dd8e4de5148e9dcaee2 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 16:17:27 +0530 Subject: [PATCH 17/56] add sdk events --- template/src/rtm/RTMGlobalStateProvider.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/template/src/rtm/RTMGlobalStateProvider.tsx b/template/src/rtm/RTMGlobalStateProvider.tsx index 7192c8533..a0e5e9aba 100644 --- a/template/src/rtm/RTMGlobalStateProvider.tsx +++ b/template/src/rtm/RTMGlobalStateProvider.tsx @@ -35,6 +35,7 @@ import { fetchChannelAttributesWithRetries, processUserAttributeForQueue, } from './rtm-presence-utils'; +import {SDKEvents} from '../utils/eventEmitter'; export enum UserType { ScreenShare = 'screenshare', @@ -199,6 +200,8 @@ const RTMGlobalStateProvider: React.FC = ({ RTMEngine.getInstance().addChannel(RTM_ROOMS.MAIN, mainChannelName); RTMEngine.getInstance().setActiveChannelName(RTM_ROOMS.MAIN); + SDKEvents.emit('_rtm-joined', mainChannelName); + subscribeTimerRef.current = 5; // Clear any pending retry timeout since we succeeded if (subscribeTimeoutRef.current) { @@ -420,7 +423,7 @@ const RTMGlobalStateProvider: React.FC = ({ if (!uid) { return; } - + SDKEvents.emit('_rtm-left', uid); // Mark user as offline (matching legacy channelMemberLeft behavior) setMainRoomRTMUsers(prev => { const updated = {...prev}; From 6d3a7ed29574b9b39d971b64d4a37658385113ca Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 17:58:28 +0530 Subject: [PATCH 18/56] hide features in breakout room --- template/src/components/Controls.tsx | 45 ++++++++++++------- .../controls/useControlPermissionMatrix.tsx | 31 ++++++++++--- .../recordings/RecordingsDateTable.tsx | 5 ++- 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/template/src/components/Controls.tsx b/template/src/components/Controls.tsx index aa49a6893..3ab10d0ab 100644 --- a/template/src/components/Controls.tsx +++ b/template/src/components/Controls.tsx @@ -486,7 +486,8 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { : false; // 2. whiteboard ends - if (isHost && $config.ENABLE_WHITEBOARD && isWebInternal()) { + const canAccessWhiteboard = useControlPermissionMatrix('whiteboardControl'); + if (canAccessWhiteboard) { actionMenuitems.push({ componentName: 'whiteboard', order: 2, @@ -528,7 +529,9 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { } // 3. host can see stt options and attendee can view only when stt is enabled by a host in the channel - if ($config.ENABLE_STT && $config.ENABLE_CAPTION) { + const canAccessCaption = useControlPermissionMatrix('captionsControl'); + const canAccessTranscripts = useControlPermissionMatrix('transcriptsControl'); + if (canAccessCaption) { actionMenuitems.push({ componentName: 'caption', order: 3, @@ -558,7 +561,8 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { }, }); // 4. Meeting transcript - if ($config.ENABLE_MEETING_TRANSCRIPT) { + + if (canAccessTranscripts) { actionMenuitems.push({ componentName: 'transcript', order: 4, @@ -595,7 +599,8 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { } // 5. view recordings - if (isHost && $config.CLOUD_RECORDING && isWeb()) { + const canAccessViewRecording = useControlPermissionMatrix('recordingControl'); + if (canAccessViewRecording && isWeb()) { actionMenuitems.push({ componentName: 'view-recordings', order: 5, @@ -682,7 +687,9 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { // 8. Screenshare const {permissions} = useBreakoutRoom(); - const canAccessBreakoutRoom = useControlPermissionMatrix('breakoutRoom'); + const canAccessBreakoutRoom = useControlPermissionMatrix( + 'breakoutRoomControl', + ); const canScreenshareInBreakoutRoom = permissions?.canScreenshare; const canAccessScreenshare = useControlPermissionMatrix('screenshareControl'); @@ -725,7 +732,9 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { } // 9. Recording - if (isHost && $config.CLOUD_RECORDING) { + const canAccessRecording = useControlPermissionMatrix('recordingControl'); + console.log('supriya-canAccessRecording: ', canAccessRecording); + if (canAccessRecording) { actionMenuitems.push({ hide: w => { return w >= BREAKPOINTS.sm ? true : false; @@ -828,9 +837,9 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { } // 13. Text-tracks to download - const canAccessAllTextTracks = - useControlPermissionMatrix('viewAllTextTracks'); - + const canAccessAllTextTracks = useControlPermissionMatrix( + 'viewAllTextTracksControl', + ); if (canAccessAllTextTracks) { actionMenuitems.push({ componentName: 'view-all-text-tracks', @@ -1160,18 +1169,18 @@ export const MoreButtonToolbarItem = (props?: { forceUpdate(); }, [isHost]); + const canAccessRecording = useControlPermissionMatrix('recordingControl'); + const canAccessWhiteboard = useControlPermissionMatrix('whiteboardControl'); + const canAccessCaptions = useControlPermissionMatrix('captionsControl'); return width < BREAKPOINTS.lg || - ($config.ENABLE_STT && - $config.ENABLE_CAPTION && - (isHost || (!isHost && isSTTActive))) || + (canAccessCaptions && (isHost || (!isHost && isSTTActive))) || $config.ENABLE_NOISE_CANCELLATION || - (isHost && $config.CLOUD_RECORDING && isWeb()) || + (canAccessRecording && isWeb()) || ($config.ENABLE_VIRTUAL_BACKGROUND && !$config.AUDIO_ROOM) || - (isHost && $config.ENABLE_WHITEBOARD && isWebInternal()) ? ( + canAccessWhiteboard ? ( {((!$config.AUTO_CONNECT_RTM && !isHost) || $config.AUTO_CONNECT_RTM) && - $config.ENABLE_WHITEBOARD && - isWebInternal() ? ( + canAccessWhiteboard ? ( ) : ( <> @@ -1314,6 +1323,8 @@ const Controls = (props: ControlsProps) => { const canAccessInvite = useControlPermissionMatrix('inviteControl'); const canAccessScreenshare = useControlPermissionMatrix('screenshareControl'); + const canAccessRecordings = useControlPermissionMatrix('recordingControl'); + const canAccessExitBreakoutRoomBtn = permissions?.canExitRoom; const defaultItems: ToolbarPresetProps['items'] = React.useMemo(() => { @@ -1364,7 +1375,7 @@ const Controls = (props: ControlsProps) => { }, recording: { align: 'center', - component: RecordingToolbarItem, + component: canAccessRecordings ? RecordingToolbarItem : null, order: 5, hide: w => { return w < BREAKPOINTS.sm ? true : false; diff --git a/template/src/components/controls/useControlPermissionMatrix.tsx b/template/src/components/controls/useControlPermissionMatrix.tsx index 4cf681b7c..e72aa0ccd 100644 --- a/template/src/components/controls/useControlPermissionMatrix.tsx +++ b/template/src/components/controls/useControlPermissionMatrix.tsx @@ -3,8 +3,9 @@ import {useContext} from 'react'; import {ClientRoleType, PropsContext} from '../../../agora-rn-uikit/src'; import {useRoomInfo} from '../room-info/useRoomInfo'; import {joinRoomPreference} from '../../utils/useJoinRoom'; -import {isWeb} from '../../utils/common'; +import {isWeb, isWebInternal} from '../../utils/common'; import {ENABLE_AUTH} from '../../auth/config'; +import {useBreakoutRoomInfo} from '../room-info/useSetBreakoutRoomInfo'; /** * ControlPermissionKey represents the different keys @@ -16,8 +17,12 @@ export type ControlPermissionKey = | 'participantControl' | 'screenshareControl' | 'settingsControl' - | 'viewAllTextTracks' - | 'breakoutRoom'; + | 'viewAllTextTracksControl' + | 'breakoutRoomControl' + | 'whiteboardControl' + | 'recordingControl' + | 'captionsControl' + | 'transcriptsControl'; /** * ControlPermissionRule defines the properties used to evaluate permission rules. @@ -26,6 +31,7 @@ export type ControlPermissionRule = { isHost: boolean; role: ClientRoleType; preference: joinRoomPreference; + isInBreakoutRoom: boolean; }; export const controlPermissionMatrix: Record< @@ -38,13 +44,24 @@ export const controlPermissionMatrix: Record< settingsControl: ({preference}) => !preference.disableSettings, screenshareControl: ({preference}) => $config.SCREEN_SHARING && !preference.disableScreenShare, - viewAllTextTracks: ({isHost}) => + + viewAllTextTracksControl: ({isHost, isInBreakoutRoom}) => isHost && $config.ENABLE_STT && $config.ENABLE_MEETING_TRANSCRIPT && $config.ENABLE_TEXT_TRACKS && - isWeb(), - breakoutRoom: () => + isWeb() && + !isInBreakoutRoom, + whiteboardControl: ({isHost, isInBreakoutRoom}) => + isHost && $config.ENABLE_WHITEBOARD && isWebInternal() && !isInBreakoutRoom, + recordingControl: ({isHost, isInBreakoutRoom}) => + isHost && $config.CLOUD_RECORDING && !isInBreakoutRoom, + captionsControl: ({isInBreakoutRoom}) => + $config.ENABLE_STT && $config.ENABLE_CAPTION && !isInBreakoutRoom, + transcriptsControl: ({isInBreakoutRoom}) => + $config.ENABLE_MEETING_TRANSCRIPT && !isInBreakoutRoom, + breakoutRoomControl: () => + isWeb() && $config.ENABLE_BREAKOUT_ROOM && ENABLE_AUTH && !$config.ENABLE_CONVERSATIONAL_AI && @@ -56,12 +73,14 @@ export const useControlPermissionMatrix = ( ): boolean => { const {data: roomData, roomPreference} = useRoomInfo(); const {rtcProps} = useContext(PropsContext); + const {breakoutRoomChannelData} = useBreakoutRoomInfo(); // Build the permission rule context for the current user. const rule: ControlPermissionRule = { isHost: roomData?.isHost || false, role: rtcProps.role, preference: {...roomPreference}, + isInBreakoutRoom: breakoutRoomChannelData?.isBreakoutMode || false, }; // Retrieve the permission function for the given key and evaluate it. const permissionFn = controlPermissionMatrix[key]; diff --git a/template/src/components/recordings/RecordingsDateTable.tsx b/template/src/components/recordings/RecordingsDateTable.tsx index 700f050bc..bfdf8acee 100644 --- a/template/src/components/recordings/RecordingsDateTable.tsx +++ b/template/src/components/recordings/RecordingsDateTable.tsx @@ -57,8 +57,9 @@ function RecordingsDateTable(props) { const [currentPage, setCurrentPage] = useState(defaultPageNumber); const {fetchRecordings} = useRecording(); - const canAccessAllTextTracks = - useControlPermissionMatrix('viewAllTextTracks'); + const canAccessAllTextTracks = useControlPermissionMatrix( + 'viewAllTextTracksControl', + ); // message for any download‐error popup const [errorSnack, setErrorSnack] = React.useState(); From 91a88f58cd21078f30d05dc541b46def210e842d Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 17:59:28 +0530 Subject: [PATCH 19/56] add condition check --- .../src/components/controls/useControlPermissionMatrix.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/template/src/components/controls/useControlPermissionMatrix.tsx b/template/src/components/controls/useControlPermissionMatrix.tsx index e72aa0ccd..1420a2500 100644 --- a/template/src/components/controls/useControlPermissionMatrix.tsx +++ b/template/src/components/controls/useControlPermissionMatrix.tsx @@ -65,7 +65,8 @@ export const controlPermissionMatrix: Record< $config.ENABLE_BREAKOUT_ROOM && ENABLE_AUTH && !$config.ENABLE_CONVERSATIONAL_AI && - !$config.EVENT_MODE, + !$config.EVENT_MODE && + !$config.RAISE_HAND, }; export const useControlPermissionMatrix = ( From dcb743407676da4a36cd433febd934a2e3301f6c Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 30 Sep 2025 19:13:34 +0530 Subject: [PATCH 20/56] rest role --- template/src/rtm/constants.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/template/src/rtm/constants.ts b/template/src/rtm/constants.ts index c8026e13d..d442e6060 100644 --- a/template/src/rtm/constants.ts +++ b/template/src/rtm/constants.ts @@ -11,5 +11,4 @@ export const RTM_EVENTS_ATTRIBUTES_TO_RESET_WHEN_ROOM_CHANGES = [ EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, // Breakout room raise hand ( will be made into independent) EventNames.STT_ACTIVE, EventNames.STT_LANGUAGE, - EventNames.ROLE_ATTRIBUTE, ] as const; From 441379b26fbf7533a5baf80f1a98e651390145ea Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 6 Oct 2025 13:21:51 +0530 Subject: [PATCH 21/56] fix rtm user data update --- .../src/rtm/RTMConfigureMainRoomProvider.tsx | 42 ++++++++++++------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/template/src/rtm/RTMConfigureMainRoomProvider.tsx b/template/src/rtm/RTMConfigureMainRoomProvider.tsx index 202f14721..d5fa58c17 100644 --- a/template/src/rtm/RTMConfigureMainRoomProvider.tsx +++ b/template/src/rtm/RTMConfigureMainRoomProvider.tsx @@ -254,17 +254,27 @@ const RTMConfigureMainRoomProvider: React.FC< Object.entries(mainRoomRTMUsers).forEach(([uidStr, rtmUser]) => { const uid = parseInt(uidStr, 10); - // Create only RTM data - const userData: RTMUserData = { - // RTM data - name: rtmUser.name || '', - screenUid: rtmUser.screenUid || 0, - offline: !!rtmUser.offline, - lastMessageTimeStamp: rtmUser.lastMessageTimeStamp || 0, - isInWaitingRoom: rtmUser?.isInWaitingRoom || false, - isHost: rtmUser.isHost, - type: rtmUser.type, - }; + let userData: Partial = {}; + // screenshare RTM data + if (rtmUser.type === 'screenshare') { + userData = { + // RTM data + name: rtmUser.name || '', + parentUid: rtmUser.parentUid || 0, + type: rtmUser.type, + }; + } else { + userData = { + // user RTM data + name: rtmUser.name || '', + screenUid: rtmUser.screenUid || 0, + offline: !!rtmUser.offline, + lastMessageTimeStamp: rtmUser.lastMessageTimeStamp || 0, + isInWaitingRoom: rtmUser?.isInWaitingRoom || false, + isHost: rtmUser.isHost, + type: rtmUser.type, + }; + } // Dispatch directly for each user dispatch({type: 'UpdateRenderList', value: [uid, userData]}); @@ -307,7 +317,7 @@ const RTMConfigureMainRoomProvider: React.FC< const init = async () => { // Set main room as active channel when this provider mounts again active const currentActiveChannel = RTMEngine.getInstance().getActiveChannelName(); - const wasInBreakoutRoom = currentActiveChannel === RTM_ROOMS.BREAKOUT; + // const wasInBreakoutRoom = currentActiveChannel === RTM_ROOMS.BREAKOUT; if (currentActiveChannel !== RTM_ROOMS.MAIN) { RTMEngine.getInstance().setActiveChannelName(RTM_ROOMS.MAIN); @@ -318,10 +328,10 @@ const RTMConfigureMainRoomProvider: React.FC< RTM_EVENTS_ATTRIBUTES_TO_RESET_WHEN_ROOM_CHANGES, ); - // Rehydrate session attributes ONLY when returning from breakout room - if (wasInBreakoutRoom) { - await rehydrateSessionAttributes(); - } + // // Rehydrate session attributes ONLY when returning from breakout room + // if (wasInBreakoutRoom) { + // await rehydrateSessionAttributes(); + // } await getChannelAttributes(); From 02475d96ea82395123aff94c0b768b4aa26457ef Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 6 Oct 2025 15:15:08 +0530 Subject: [PATCH 22/56] room name update --- .../ui/BreakoutRoomGroupSettings.tsx | 1 + .../ui/BreakoutRoomRenameModal.tsx | 40 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index 2db745f04..2bed1cbcf 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -308,6 +308,7 @@ const BreakoutRoomGroupSettings = ({scrollOffset}) => { currentRoomName={roomToEdit.name} updateRoomName={onRoomNameChange} setModalOpen={setRenameRoomModalOpen} + existingRoomNames={breakoutGroups.map(group => group.name)} /> )} diff --git a/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx b/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx index 5d2091473..0cb67f1b3 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomRenameModal.tsx @@ -8,17 +8,43 @@ interface BreakoutRoomRenameModalProps { setModalOpen: Dispatch>; currentRoomName: string; updateRoomName: (newName: string) => void; + existingRoomNames: string[]; } export default function BreakoutRoomRenameModal( props: BreakoutRoomRenameModalProps, ) { - const {currentRoomName, setModalOpen, updateRoomName} = props; + const {currentRoomName, setModalOpen, updateRoomName, existingRoomNames} = + props; const [roomName, setRoomName] = React.useState(currentRoomName); const MAX_ROOM_NAME_LENGTH = 30; + + // Helper function to normalize room name (trim and collapse multiple spaces) + const normalizeRoomName = (name: string) => { + return name.trim().replace(/\s+/g, ' '); + }; + + // Check if the normalized room name already exists in other rooms + const isDuplicateName = existingRoomNames.some(existingName => { + const normalizedExistingName = + normalizeRoomName(existingName).toLowerCase(); + const normalizedNewName = normalizeRoomName(roomName).toLowerCase(); + const normalizedCurrentName = + normalizeRoomName(currentRoomName).toLowerCase(); + + return ( + normalizedExistingName === normalizedNewName && + normalizedExistingName !== normalizedCurrentName + ); + }); + const disabled = - roomName.trim() === '' || roomName.trim().length > MAX_ROOM_NAME_LENGTH; + roomName.trim() === '' || + roomName.trim().length > MAX_ROOM_NAME_LENGTH || + normalizeRoomName(roomName).toLowerCase() === + normalizeRoomName(currentRoomName).toLowerCase() || + isDuplicateName; return ( MAX_ROOM_NAME_LENGTH && + (roomName.trim().length > MAX_ROOM_NAME_LENGTH || + isDuplicateName) && style.characterCountError, ]}> {roomName.trim().length}/{MAX_ROOM_NAME_LENGTH} @@ -62,6 +89,11 @@ export default function BreakoutRoomRenameModal( Room name cannot exceed {MAX_ROOM_NAME_LENGTH} characters )} + {isDuplicateName && ( + + A room with this name already exists + + )} @@ -83,7 +115,7 @@ export default function BreakoutRoomRenameModal( text={'Save'} disabled={disabled} onPress={() => { - updateRoomName(roomName); + updateRoomName(normalizeRoomName(roomName)); }} /> From d70aec9e216d273f9e5fd5085a23026b12b8940b Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 6 Oct 2025 18:03:51 +0530 Subject: [PATCH 23/56] add is host flag --- .../context/BreakoutRoomContext.tsx | 14 +++++++++++ .../components/breakout-room/state/reducer.ts | 23 ++++++++++++------- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 8c98721a5..35e4720d2 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -1240,12 +1240,26 @@ const BreakoutRoomProvider = ({ }, ); + // Check if user is a host + let isUserHost: boolean | undefined; + if (currentGroup) { + // User is moving from another breakout room + isUserHost = currentGroup.participants.hosts.includes(uid); + } else { + // User is moving from main room - check mainRoomRTMUsers + const rtmUser = mainRoomRTMUsers[uid]; + if (rtmUser) { + isUserHost = rtmUser.isHost === 'true'; + } + } + dispatch({ type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, payload: { uid, fromGroupId: currentGroup?.id, toGroupId, + isHost: isUserHost, }, }); } catch (error) { diff --git a/template/src/components/breakout-room/state/reducer.ts b/template/src/components/breakout-room/state/reducer.ts index 62b1fbdfa..66c9b1da9 100644 --- a/template/src/components/breakout-room/state/reducer.ts +++ b/template/src/components/breakout-room/state/reducer.ts @@ -176,6 +176,7 @@ export type BreakoutRoomAction = uid: UidType; fromGroupId: string; toGroupId: string; + isHost?: boolean; }; }; @@ -462,16 +463,22 @@ export const breakoutRoomReducer = ( } case BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP: { - const {uid, fromGroupId, toGroupId} = action.payload; + const {uid, fromGroupId, toGroupId, isHost} = action.payload; - // Determine if user was a host or attendee in their previous group - let wasHost = false; - if (fromGroupId) { + // Determine if user should be added as host or attendee + let shouldBeHost = false; + + if (isHost !== undefined) { + // Use explicit isHost flag if provided (from mainRoomRTMUsers for main room users) + shouldBeHost = isHost; + } else if (fromGroupId) { + // Fallback: check their role in the previous breakout group const sourceGroup = state.breakoutGroups.find( group => group.id === fromGroupId, ); - wasHost = sourceGroup?.participants.hosts.includes(uid) || false; + shouldBeHost = sourceGroup?.participants.hosts.includes(uid) || false; } + // If isHost is undefined and no fromGroupId, default to attendee return { ...state, @@ -489,16 +496,16 @@ export const breakoutRoomReducer = ( }, }; } - // Add to target group with same role as previous group + // Add to target group with determined role if (group.id === toGroupId) { return { ...group, participants: { ...group.participants, - hosts: wasHost + hosts: shouldBeHost ? [...group.participants.hosts, uid] : group.participants.hosts, - attendees: !wasHost + attendees: !shouldBeHost ? [...group.participants.attendees, uid] : group.participants.attendees, }, From a5ae2ee3a2e001d636eb66809d80be52aff3aaa5 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 11:21:22 +0530 Subject: [PATCH 24/56] remove presenter and add evet ordering check --- .../context/BreakoutRoomContext.tsx | 484 +++++++++++------- .../events/BreakoutRoomEventsConfigure.tsx | 167 ++++-- .../components/breakout-room/state/types.ts | 3 +- .../breakout-room/ui/BreakoutRoomView.tsx | 5 +- template/src/rtm-events/constants.ts | 3 + template/src/rtm/RTMGlobalStateProvider.tsx | 14 + 6 files changed, 450 insertions(+), 226 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 35e4720d2..aa4afa93e 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -28,6 +28,7 @@ import {useContent} from '../../../../customization-api'; import events, {PersistanceLevel} from '../../../rtm-events-api'; import {BreakoutRoomAction, initialBreakoutGroups} from '../state/reducer'; import {BreakoutRoomEventNames} from '../events/constants'; +import {EventNames} from '../../../rtm-events'; import {BreakoutRoomSyncStateEventPayload} from '../state/types'; import {IconsInterface} from '../../../atoms/CustomIcon'; import Toast from '../../../../react-native-toast-message'; @@ -178,6 +179,7 @@ const defaulBreakoutRoomPermission: BreakoutRoomPermissions = { }; interface BreakoutRoomContextValue { mainChannelId: string; + isBreakoutUILocked: boolean; breakoutSessionId: BreakoutRoomState['breakoutSessionId']; breakoutGroups: BreakoutRoomState['breakoutGroups']; assignmentStrategy: RoomAssignmentStrategy; @@ -200,9 +202,12 @@ interface BreakoutRoomContextValue { checkIfBreakoutRoomSessionExistsAPI: () => Promise; handleAssignParticipants: (strategy: RoomAssignmentStrategy) => void; // Presenters - onMakeMePresenter: (action: 'start' | 'stop') => void; - presenters: {uid: UidType; timestamp: number}[]; - clearAllPresenters: () => void; + // onMakeMePresenter: ( + // action: 'start' | 'stop', + // shouldSendEvent?: boolean, + // ) => void; + // presenters: {uid: UidType; timestamp: number}[]; + // clearAllPresenters: () => void; // State sync handleBreakoutRoomSyncState: ( data: BreakoutRoomSyncStateEventPayload['data'], @@ -230,6 +235,7 @@ interface BreakoutRoomContextValue { const BreakoutRoomContext = React.createContext({ mainChannelId: '', + isBreakoutUILocked: false, breakoutSessionId: undefined, unassignedParticipants: [], breakoutGroups: [], @@ -251,9 +257,9 @@ const BreakoutRoomContext = React.createContext({ getRoomMemberDropdownOptions: () => [], upsertBreakoutRoomAPI: async () => {}, checkIfBreakoutRoomSessionExistsAPI: async () => false, - onMakeMePresenter: () => {}, - presenters: [], - clearAllPresenters: () => {}, + // onMakeMePresenter: () => {}, + // presenters: [], + // clearAllPresenters: () => {}, handleBreakoutRoomSyncState: () => {}, // Multi-host coordination handlers handleHostOperationStart: () => {}, @@ -279,7 +285,8 @@ const BreakoutRoomProvider = ({ }) => { const {store} = useContext(StorageContext); const {defaultContent, activeUids} = useContent(); - const {mainRoomRTMUsers} = useRTMGlobalState(); + const {mainRoomRTMUsers, customRTMMainRoomData, setCustomRTMMainRoomData} = + useRTMGlobalState(); const localUid = useLocalUid(); const { data: {isHost, roomId: joinRoomId}, @@ -308,8 +315,14 @@ const BreakoutRoomProvider = ({ string | undefined >(undefined); - // Timestamp tracking for event ordering + // Timestamp Server (authoritative ordering) + const lastProcessedServerTsRef = useRef(0); + // 2Self join guard (prevent stale reverts) (when self join happens) + const lastSelfJoinRef = useRef<{roomId: string; ts: number} | null>(null); + // Timestamp client tracking for event ordering client side const lastSyncedTimestampRef = useRef(0); + const isBreakoutUILocked = + isBreakoutUpdateInFlight || !!currentOperatingHostName; const lastSyncedSnapshotRef = useRef<{ session_id: string; switch_room: boolean; @@ -335,10 +348,12 @@ const BreakoutRoomProvider = ({ // Presenter const {isScreenshareActive, stopScreenshare} = useScreenshare(); - const [canIPresent, setICanPresent] = useState(false); - const [presenters, setPresenters] = useState< - {uid: UidType; timestamp: number}[] - >([]); + // const [canIPresent, setICanPresent] = useState(false); + // Get presenters from custom RTM main room data (memoized to maintain stable reference) + // const presenters = React.useMemo( + // () => customRTMMainRoomData.breakout_room_presenters || [], + // [customRTMMainRoomData], + // ); // State version tracker to force dependent hooks to re-compute const [breakoutRoomVersion, setBreakoutRoomVersion] = useState(0); @@ -384,8 +399,29 @@ const BreakoutRoomProvider = ({ useEffect(() => { return () => { isMountedRef.current = false; + + // // Clear presenter attribute on unmount if user is presenting + // if (canIPresent && !isHostRef.current) { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // 'Clearing presenter attribute on unmount', + // {localUid}, + // ); + + // // Send event to clear presenter status + // events.send( + // EventNames.BREAKOUT_PRESENTER_ATTRIBUTE, + // JSON.stringify({ + // uid: localUid, + // isPresenter: false, + // timestamp: Date.now(), + // }), + // PersistanceLevel.Sender, + // ); + // } }; - }, []); + }, [localUid]); // Timeouts const timeoutsRef = useRef>>(new Set()); @@ -1240,6 +1276,38 @@ const BreakoutRoomProvider = ({ }, ); + // // Clean up presenter status if user is switching rooms + // const isPresenting = presenters.some(p => p.uid === uid); + // if (isPresenting) { + // setCustomRTMMainRoomData(prev => ({ + // ...prev, + // breakout_room_presenters: ( + // prev.breakout_room_presenters || [] + // ).filter((p: any) => p.uid !== uid), + // })); + + // // Notify the user that their presenter access was removed + // try { + // events.send( + // BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, + // JSON.stringify({ + // uid: uid, + // timestamp: Date.now(), + // action: 'stop', + // }), + // PersistanceLevel.None, + // uid, + // ); + // } catch (error) { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // 'Error sending presenter stop event on room switch', + // {error: error.message}, + // ); + // } + // } + // Check if user is a host let isUserHost: boolean | undefined; if (currentGroup) { @@ -1329,11 +1397,12 @@ const BreakoutRoomProvider = ({ currentlyInRoom && hasAvailableRooms && (isHostRef.current || allowAttendeeSwitch), - canScreenshare: isHostRef.current - ? true - : currentlyInRoom - ? canIPresent - : true, + canScreenshare: true, + // isHostRef.current + // ? true + // : currentlyInRoom + // ? canIPresent + // : true, canRaiseHands: !isHostRef.current && !!current.session_id, canAssignParticipants: isHostRef.current && !currentlyInRoom, canHostManageMainRoom: isHostRef.current, @@ -1345,7 +1414,7 @@ const BreakoutRoomProvider = ({ }; setPermissions(nextPermissions); } - }, [breakoutRoomVersion, canIPresent, isBreakoutMode, localUid]); + }, [breakoutRoomVersion, isBreakoutMode, localUid]); const joinRoom = ( toRoomId: string, @@ -1380,7 +1449,7 @@ const BreakoutRoomProvider = ({ toRoomName: stateRef.current.breakoutGroups.find(r => r.id === toRoomId) ?.name, }); - + lastSelfJoinRef.current = {roomId: toRoomId, ts: Date.now()}; moveUserIntoGroup(localUid, toRoomId); if (!isHostRef.current) { setSelfJoinRoomId(toRoomId); @@ -1408,6 +1477,18 @@ const BreakoutRoomProvider = ({ // you are not in const localUser = defaultContentRef.current[localUid]; + // // Clean up presenter status if user is presenting + // const isPresenting = presenters.some(p => p.uid === localUid); + // if (isPresenting) { + // setCustomRTMMainRoomData(prev => ({ + // ...prev, + // breakout_room_presenters: ( + // prev.breakout_room_presenters || [] + // ).filter((p: any) => p.uid !== localUid), + // })); + // setICanPresent(false); + // } + try { if (localUser) { // Use breakout-specific exit (doesn't destroy main RTM) @@ -1490,6 +1571,9 @@ const BreakoutRoomProvider = ({ }, ); + // Clear all presenters when closing all rooms + // clearAllPresenters(); + dispatch({type: BreakoutGroupActionTypes.CLOSE_ALL_GROUPS}); }; @@ -1521,116 +1605,139 @@ const BreakoutRoomProvider = ({ : []; }; - const isUserPresenting = useCallback( - (uid?: UidType) => { - if (uid !== undefined) { - return presenters.some(presenter => presenter.uid === uid); - } - // fall back to current user - return canIPresent; - }, - [presenters, canIPresent], - ); + // const isUserPresenting = useCallback( + // (uid?: UidType) => { + // if (uid !== undefined) { + // return presenters.some(presenter => presenter.uid === uid); + // } + // // fall back to current user + // return canIPresent; + // }, + // [presenters, canIPresent], + // ); - // User wants to start presenting - const makePresenter = (uid: UidType, action: 'start' | 'stop') => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - `Make presenter - ${action}`, - { - targetUserId: uid, - action, - isHost: isHostRef.current, - }, - ); - if (!uid) { - return; - } - try { - // Host can make someone a presenter - events.send( - BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, - JSON.stringify({ - uid: uid, - timestamp: Date.now(), - action, - }), - PersistanceLevel.None, - uid, - ); - if (action === 'start') { - addPresenter(uid); - } else if (action === 'stop') { - removePresenter(uid); - } - } catch (error) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Error making user presenter', - { - targetUserId: uid, - action, - error: error.message, - }, - ); - } - }; + // // User wants to start presenting + // const makePresenter = (uid: UidType, action: 'start' | 'stop') => { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // `Make presenter - ${action}`, + // { + // targetUserId: uid, + // action, + // isHost: isHostRef.current, + // }, + // ); + // if (!uid) { + // return; + // } + // try { + // const timestamp = Date.now(); + // console.log('supriya-presenter sending make presenter'); + // // Host sends BREAKOUT_ROOM_MAKE_PRESENTER event to the attendee + // events.send( + // BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, + // JSON.stringify({ + // uid: uid, + // timestamp, + // action, + // }), + // PersistanceLevel.None, + // uid, + // ); - // Presenter management functions (called by event handlers) - const addPresenter = useCallback((uid: UidType) => { - setPresenters(prev => { - // Check if already presenting to avoid duplicates - const exists = prev.find(presenter => presenter.uid === uid); - if (exists) { - return prev; - } - return [...prev, {uid, timestamp: Date.now()}]; - }); - }, []); + // // Host immediately updates their own customRTMMainRoomData + // if (action === 'start') { + // setCustomRTMMainRoomData(prev => { + // const currentPresenters = prev.breakout_room_presenters || []; + // // Check if already presenting to avoid duplicates + // const exists = currentPresenters.find( + // (presenter: any) => presenter.uid === uid, + // ); + // if (exists) { + // return prev; + // } + // return { + // ...prev, + // breakout_room_presenters: [...currentPresenters, {uid, timestamp}], + // }; + // }); + // } else if (action === 'stop') { + // setCustomRTMMainRoomData(prev => ({ + // ...prev, + // breakout_room_presenters: ( + // prev.breakout_room_presenters || [] + // ).filter((presenter: any) => presenter.uid !== uid), + // })); + // } + // } catch (error) { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // 'Error making user presenter', + // { + // targetUserId: uid, + // action, + // error: error.message, + // }, + // ); + // } + // }; - const removePresenter = useCallback((uid: UidType) => { - if (uid) { - setPresenters(prev => prev.filter(presenter => presenter.uid !== uid)); - } - }, []); + // const onMakeMePresenter = useCallback( + // (action: 'start' | 'stop', shouldSendEvent: boolean = true) => { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // `User became presenter - ${action}`, + // ); - const onMakeMePresenter = useCallback( - (action: 'start' | 'stop') => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - `User became presenter - ${action}`, - ); + // const timestamp = Date.now(); + + // // Send event only if requested (not when restoring from attribute) + // if (shouldSendEvent) { + // // Attendee sends BREAKOUT_PRESENTER_ATTRIBUTE event to persist their presenter status + // events.send( + // EventNames.BREAKOUT_PRESENTER_ATTRIBUTE, + // JSON.stringify({ + // uid: localUid, + // isPresenter: action === 'start', + // timestamp, + // }), + // PersistanceLevel.Sender, + // ); + // } - if (action === 'start') { - setICanPresent(true); - // Show toast notification when presenter permission is granted - Toast.show({ - type: 'success', - text1: 'You can now present in this breakout room', - visibilityTime: 3000, - }); - } else if (action === 'stop') { - if (isScreenshareActive) { - stopScreenshare(); - } - setICanPresent(false); - // Show toast notification when presenter permission is removed - Toast.show({ - type: 'info', - text1: 'Your presenter access has been removed', - visibilityTime: 3000, - }); - } - }, - [isScreenshareActive], - ); + // if (action === 'start') { + // setICanPresent(true); + // // Show toast notification when presenter permission is granted + // Toast.show({ + // type: 'success', + // text1: 'You can now present in this breakout room', + // visibilityTime: 3000, + // }); + // } else if (action === 'stop') { + // if (isScreenshareActive) { + // stopScreenshare(); + // } + // setICanPresent(false); + // // Show toast notification when presenter permission is removed + // Toast.show({ + // type: 'info', + // text1: 'Your presenter access has been removed', + // visibilityTime: 3000, + // }); + // } + // }, + // [isScreenshareActive, localUid], + // ); - const clearAllPresenters = useCallback(() => { - setPresenters([]); - }, []); + // const clearAllPresenters = useCallback(() => { + // setCustomRTMMainRoomData(prev => ({ + // ...prev, + // breakout_room_presenters: [], + // })); + // }, [setCustomRTMMainRoomData]); const getRoomMemberDropdownOptions = useCallback( (memberUid: UidType) => { @@ -1673,28 +1780,29 @@ const BreakoutRoomProvider = ({ }); }); - // Make presenter option is available only for host - // and if the incoming member is also a host we dont - // need to show this option as they can already present - const isUserHost = - currentRoom?.participants.hosts.includes(memberUid) || false; - if (isUserHost) { - return options; - } - if (isHostRef.current) { - const userIsPresenting = isUserPresenting(memberUid); - const title = userIsPresenting ? 'Stop presenter' : 'Make a Presenter'; - const action = userIsPresenting ? 'stop' : 'start'; - options.push({ - type: 'make-presenter', - icon: 'promote-filled', - title, - onOptionPress: () => makePresenter(memberUid, action), - }); - } + // // Make presenter option is available only for host + // // and if the incoming member is also a host we dont + // // need to show this option as they can already present + // const isUserHost = + // currentRoom?.participants.hosts.includes(memberUid) || false; + // if (isUserHost) { + // return options; + // } + // if (isHostRef.current) { + // const userIsPresenting = isUserPresenting(memberUid); + // const title = userIsPresenting ? 'Stop presenter' : 'Make a Presenter'; + // const action = userIsPresenting ? 'stop' : 'start'; + // options.push({ + // type: 'make-presenter', + // icon: 'promote-filled', + // title, + // onOptionPress: () => makePresenter(memberUid, action), + // }); + // } return options; }, - [isUserPresenting, presenters, breakoutRoomVersion], + // [isUserPresenting, presenters, breakoutRoomVersion], + [breakoutRoomVersion], ); // const handleBreakoutRoomSyncState = useCallback( @@ -1898,7 +2006,8 @@ const BreakoutRoomProvider = ({ (operationName: string, hostUid: UidType, hostName: string) => { // Only process if current user is also a host and it's not their own event console.log('supriya-state-sync host operation started', operationName); - if (!isHostRef.current || hostUid === localUid) { + // if (!isHostRef.current || hostUid === localUid) { + if (hostUid === localUid) { return; } @@ -1909,16 +2018,9 @@ const BreakoutRoomProvider = ({ {operationName, hostUid, hostName}, ); - // Show toast notification - showDeduplicatedToast(`host-operation-start-${hostUid}`, { - type: 'info', - text1: `${hostName} is managing breakout rooms`, - text2: 'Please wait for them to finish', - visibilityTime: 5000, - }); setCurrentOperatingHostName(hostName); }, - [localUid, showDeduplicatedToast], + [localUid], ); const handleHostOperationEnd = useCallback( @@ -1926,7 +2028,8 @@ const BreakoutRoomProvider = ({ // Only process if current user is also a host and it's not their own event console.log('supriya-state-sync host operation ended', operationName); - if (!isHostRef.current || hostUid === localUid) { + // if (!isHostRef.current || hostUid === localUid) { + if (hostUid === localUid) { return; } @@ -2059,35 +2162,59 @@ const BreakoutRoomProvider = ({ timestamp: number, ) => { console.log( - 'supriya-api-sync response', + 'supriya-sync-ordering exact response', timestamp, JSON.stringify(payload), ); + const {srcuid, data} = payload; + const { + session_id, + switch_room, + breakout_room, + assignment_type, + sts = 0, + } = data; + console.log('supriya-sync-ordering Sync state event received', { + sessionId: session_id, + incomingRoom: breakout_room || [], + currentRoom: stateRef.current.breakoutGroups || [], + switchRoom: switch_room, + assignmentType: assignment_type, + }); - // Skip events older than the last processed timestamp - if (timestamp && timestamp <= lastSyncedTimestampRef.current) { - console.log('supriya-api-sync Skipping old breakout room sync event', { - timestamp, - lastProcessed: lastSyncedTimestampRef.current, - }); + // global server ordering + if (sts <= lastProcessedServerTsRef.current) { + console.log( + `supriya-sync-ordering [BreakoutSync] Ignoring out-of-order state (sts=${sts}, last=${lastProcessedServerTsRef.current})`, + ); return; } + lastProcessedServerTsRef.current = sts; - const {srcuid, data} = payload; - const {session_id, switch_room, breakout_room, assignment_type} = data; + // Self-join race protection — ignore stale reverts right after joining + if ( + lastSelfJoinRef.current && + Date.now() - lastSelfJoinRef.current.ts < 2000 && // 2s cooldown + !findUserRoomId(localUid, breakout_room) + ) { + console.log( + 'supriya-sync-ordering [SyncGuard] Ignoring stale sync conflicting with recent self-join to', + lastSelfJoinRef.current.roomId, + ); + return; + } - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Sync state event received', - { - sessionId: session_id, - incomingRoomCount: breakout_room?.length || 0, - currentRoomCount: stateRef.current.breakoutGroups.length, - switchRoom: switch_room, - assignmentType: assignment_type, - }, - ); + // Local duplicate protection (client-side ordering) Skip events older than the last processed timestamp + if (timestamp && timestamp <= lastSyncedTimestampRef.current) { + console.log( + 'supriya-sync-ordering Skipping old breakout room sync event', + { + timestamp, + lastProcessed: lastSyncedTimestampRef.current, + }, + ); + return; + } // Snapshot before applying const prevSnapshot = lastSyncedSnapshotRef?.current; @@ -2314,6 +2441,7 @@ const BreakoutRoomProvider = ({ = ({children}) => { const { - onMakeMePresenter, + // onMakeMePresenter, handleBreakoutRoomSyncState, handleHostOperationStart, handleHostOperationEnd, } = useBreakoutRoom(); + // const {setCustomRTMMainRoomData} = useRTMGlobalState(); const localUid = useLocalUid(); const { data: {isHost}, } = useRoomInfo(); const isHostRef = React.useRef(isHost); const localUidRef = React.useRef(localUid); - const onMakeMePresenterRef = useRef(onMakeMePresenter); + // const onMakeMePresenterRef = useRef(onMakeMePresenter); const handleBreakoutRoomSyncStateRef = useRef(handleBreakoutRoomSyncState); const handleHostOperationStartRef = useRef(handleHostOperationStart); const handleHostOperationEndRef = useRef(handleHostOperationEnd); @@ -35,9 +38,9 @@ const BreakoutRoomEventsConfigure: React.FC = ({children}) => { useEffect(() => { localUidRef.current = localUid; }, [localUid]); - useEffect(() => { - onMakeMePresenterRef.current = onMakeMePresenter; - }, [onMakeMePresenter]); + // useEffect(() => { + // onMakeMePresenterRef.current = onMakeMePresenter; + // }, [onMakeMePresenter]); useEffect(() => { handleBreakoutRoomSyncStateRef.current = handleBreakoutRoomSyncState; }, [handleBreakoutRoomSyncState]); @@ -49,32 +52,35 @@ const BreakoutRoomEventsConfigure: React.FC = ({children}) => { }, [handleHostOperationEnd]); useEffect(() => { - const handlePresenterStatusEvent = (evtData: any) => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'BREAKOUT_ROOM_MAKE_PRESENTER event recevied', - evtData, - ); - try { - const {sender, payload} = evtData; - if (sender === `${localUidRef.current}`) { - return; - } - const data = JSON.parse(payload); - if (data.action === 'start' || data.action === 'stop') { - onMakeMePresenterRef.current(data.action); - } - } catch (error) {} - }; + // const handleMakePresenterEvent = (evtData: any) => { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // 'BREAKOUT_ROOM_MAKE_PRESENTER event received', + // evtData, + // ); + // try { + // const {payload} = evtData; + // const data = JSON.parse(payload); + // console.log('supriya-presenter handleMakePresenterEvent data: ', data); + // const {uid, action} = data; + + // // Only process if it's for the local user + // if (uid === localUidRef.current) { + // onMakeMePresenterRef.current(action); + // } + // } catch (error) { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // 'Error handling make presenter event', + // error, + // ); + // } + // }; const handleBreakoutRoomSyncStateEvent = (evtData: any) => { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'BREAKOUT_ROOM_SYNC_STATE event recevied', - evtData, - ); + console.log('BREAKOUT_ROOM_SYNC_STATE event recevied', evtData); const {ts, payload} = evtData; const data: BreakoutRoomSyncStateEventPayload = JSON.parse(payload); if (data.data.act === 'SYNC_STATE') { @@ -98,10 +104,10 @@ const BreakoutRoomEventsConfigure: React.FC = ({children}) => { if (sender === `${localUidRef.current}`) { return; } - // Only process if current user is also a host - if (!isHostRef.current) { - return; - } + // // Only process if current user is also a host + // if (!isHostRef.current) { + // return; + // } const data = JSON.parse(payload); const {operationName, hostUid, hostName} = data; @@ -130,10 +136,10 @@ const BreakoutRoomEventsConfigure: React.FC = ({children}) => { if (sender === `${localUidRef.current}`) { return; } - // Only process if current user is also a host - if (!isHostRef.current) { - return; - } + // // Only process if current user is also a host + // if (!isHostRef.current) { + // return; + // } const data = JSON.parse(payload); const {operationName, hostUid, hostName} = data; @@ -149,14 +155,81 @@ const BreakoutRoomEventsConfigure: React.FC = ({children}) => { } }; + // const handlePresenterAttributeEvent = (evtData: any) => { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // 'BREAKOUT_PRESENTER_ATTRIBUTE event received', + // evtData, + // ); + // try { + // const {payload} = evtData; + // const data = JSON.parse(payload); + // console.log('supriya-presenter handlePresenterAttributeEvent', data); + + // const {uid, isPresenter, timestamp} = data; + + // // If this is the local user's presenter attribute, restore their state + // // Pass shouldSendEvent: false to avoid sending the event again (infinite loop) + // if (uid === localUidRef.current && !isHostRef.current) { + // if (isPresenter) { + // onMakeMePresenterRef.current('start', false); + // } else { + // onMakeMePresenterRef.current('stop', false); + // } + // } + + // // Host updates customRTMMainRoomData with presenter status + // // This is mainly for syncing state when host rejoins and reads persisted attributes + // if (isHostRef.current) { + // if (isPresenter) { + // setCustomRTMMainRoomData(prev => { + // const currentPresenters = prev.breakout_room_presenters || []; + // // Check if already in the list (avoid duplicate from makePresenter) + // const exists = currentPresenters.find((p: any) => p.uid === uid); + // if (exists) { + // return prev; + // } + // return { + // ...prev, + // breakout_room_presenters: [ + // ...currentPresenters, + // {uid, timestamp}, + // ], + // }; + // }); + // } else { + // // Remove from presenters list + // setCustomRTMMainRoomData(prev => ({ + // ...prev, + // breakout_room_presenters: ( + // prev.breakout_room_presenters || [] + // ).filter((p: any) => p.uid !== uid), + // })); + // } + // } + // } catch (error) { + // logger.log( + // LogSource.Internals, + // 'BREAKOUT_ROOM', + // 'Error handling presenter attribute event', + // error, + // ); + // } + // }; + // events.on( // BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT, // handleAnnouncementEvent, // ); - events.on( - BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, - handlePresenterStatusEvent, - ); + // events.on( + // EventNames.BREAKOUT_PRESENTER_ATTRIBUTE, + // handlePresenterAttributeEvent, + // ); + // events.on( + // BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, + // handleMakePresenterEvent, + // ); events.on( BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, handleBreakoutRoomSyncStateEvent, @@ -172,10 +245,14 @@ const BreakoutRoomEventsConfigure: React.FC = ({children}) => { return () => { // events.off(BreakoutRoomEventNames.BREAKOUT_ROOM_ANNOUNCEMENT); - events.off( - BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, - handlePresenterStatusEvent, - ); + // events.off( + // EventNames.BREAKOUT_PRESENTER_ATTRIBUTE, + // handlePresenterAttributeEvent, + // ); + // events.off( + // BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, + // handleMakePresenterEvent, + // ); events.off( BreakoutRoomEventNames.BREAKOUT_ROOM_SYNC_STATE, handleBreakoutRoomSyncStateEvent, diff --git a/template/src/components/breakout-room/state/types.ts b/template/src/components/breakout-room/state/types.ts index 4247fff47..3223a1cc1 100644 --- a/template/src/components/breakout-room/state/types.ts +++ b/template/src/components/breakout-room/state/types.ts @@ -39,8 +39,9 @@ export interface BreakoutRoomSyncStateEventPayload { data: { switch_room: boolean; session_id: string; - breakout_room: BreakoutGroup[]; assignment_type: RoomAssignmentStrategy; + breakout_room: BreakoutGroup[]; + sts: number; }; act: 'SYNC_STATE'; srcuid: number; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx index 85aa7d05f..96b362029 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx @@ -34,8 +34,9 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { upsertBreakoutRoomAPI, closeAllRooms, permissions, - isBreakoutUpdateInFlight, + isBreakoutUILocked, } = useBreakoutRoom(); + console.log('supriya-isBreakoutUILocked: ', isBreakoutUILocked); useEffect(() => { const init = async () => { @@ -61,7 +62,7 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { }, []); // Disable all actions when API is in flight or another host is operating - const disableAllActions = isBreakoutUpdateInFlight; + const disableAllActions = isBreakoutUILocked; return ( <> diff --git a/template/src/rtm-events/constants.ts b/template/src/rtm-events/constants.ts index 70ffe9db6..26e26f0f7 100644 --- a/template/src/rtm-events/constants.ts +++ b/template/src/rtm-events/constants.ts @@ -49,6 +49,8 @@ const SPOTLIGHT_USER_CHANGED = 'SPOTLIGHT_USER_CHANGED'; const BREAKOUT_RAISE_HAND_ATTRIBUTE = 'breakout_raise_hand'; // 10. Cross-room raise hand notifications (messages, not attributes) const CROSS_ROOM_RAISE_HAND_NOTIFICATION = 'cross_room_raise_hand_notification'; +// 11. Breakout room presenter attribute +const BREAKOUT_PRESENTER_ATTRIBUTE = 'breakout_presenter'; const EventNames = { RECORDING_STATE_ATTRIBUTE, @@ -73,6 +75,7 @@ const EventNames = { SPOTLIGHT_USER_CHANGED, BREAKOUT_RAISE_HAND_ATTRIBUTE, CROSS_ROOM_RAISE_HAND_NOTIFICATION, + BREAKOUT_PRESENTER_ATTRIBUTE, }; /** ***** EVENT NAMES ENDS ***** */ diff --git a/template/src/rtm/RTMGlobalStateProvider.tsx b/template/src/rtm/RTMGlobalStateProvider.tsx index a0e5e9aba..8bb38f427 100644 --- a/template/src/rtm/RTMGlobalStateProvider.tsx +++ b/template/src/rtm/RTMGlobalStateProvider.tsx @@ -68,6 +68,11 @@ const RTMGlobalStateContext = React.createContext<{ setMainRoomRTMUsers: React.Dispatch< React.SetStateAction<{[uid: number]: RTMUserData}> >; + // Custom state for developer features (main room scope, cross-room accessible) + customRTMMainRoomData: {[key: string]: any}; + setCustomRTMMainRoomData: React.Dispatch< + React.SetStateAction<{[key: string]: any}> + >; registerMainChannelMessageHandler: ( handler: (message: MessageEvent) => void, ) => void; @@ -79,6 +84,8 @@ const RTMGlobalStateContext = React.createContext<{ }>({ mainRoomRTMUsers: {}, setMainRoomRTMUsers: () => {}, + customRTMMainRoomData: {}, + setCustomRTMMainRoomData: () => {}, registerMainChannelMessageHandler: () => {}, unregisterMainChannelMessageHandler: () => {}, registerMainChannelStorageHandler: () => {}, @@ -98,6 +105,11 @@ const RTMGlobalStateProvider: React.FC = ({ [uid: number]: RTMUserData; }>({}); + // Custom state for developer features (main room scope, cross-room accessible) + const [customRTMMainRoomData, setCustomRTMMainRoomData] = useState<{ + [key: string]: any; + }>({}); + // Timeout Refs const isRTMMounted = useRef(true); const hasInitRef = useRef(false); @@ -676,6 +688,8 @@ const RTMGlobalStateProvider: React.FC = ({ value={{ mainRoomRTMUsers, setMainRoomRTMUsers, + customRTMMainRoomData, + setCustomRTMMainRoomData, registerMainChannelMessageHandler, unregisterMainChannelMessageHandler, registerMainChannelStorageHandler, From 89da03cd3d77812950632668ea7cf65b8adf40f9 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 11:24:31 +0530 Subject: [PATCH 25/56] user intiating the toast shoudld not see the toast --- .../context/BreakoutRoomContext.tsx | 98 +++++++++++-------- 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index aa4afa93e..8a66f7763 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -2260,24 +2260,30 @@ const BreakoutRoomProvider = ({ console.log('supriya-sync-ordering 1. all room closed: '); // 1. User is in breakout toom and the exits if (prevRoomId) { - showDeduplicatedToast('all-rooms-closed', { - leadingIconName: 'close-room', - type: 'info', - text1: `Host: ${senderName} has closed all breakout rooms.`, - text2: 'Returning to the main room...', - visibilityTime: 3000, - }); + // Don't show toast if the user is the author + if (srcuid !== localUid) { + showDeduplicatedToast('all-rooms-closed', { + leadingIconName: 'close-room', + type: 'info', + text1: `Host: ${senderName} has closed all breakout rooms.`, + text2: 'Returning to the main room...', + visibilityTime: 3000, + }); + } // Set transition flag - user will remount in main room and need fresh data sessionStorage.setItem('breakout_room_transition', 'true'); return exitRoom(true); } else { // 2. User is in main room recevies just notification - showDeduplicatedToast('all-rooms-closed', { - leadingIconName: 'close-room', - type: 'info', - text1: `Host: ${senderName} has closed all breakout rooms`, - visibilityTime: 4000, - }); + // Don't show toast if the user is the author + if (srcuid !== localUid) { + showDeduplicatedToast('all-rooms-closed', { + leadingIconName: 'close-room', + type: 'info', + text1: `Host: ${senderName} has closed all breakout rooms`, + visibilityTime: 4000, + }); + } } } @@ -2289,24 +2295,30 @@ const BreakoutRoomProvider = ({ const roomStillExists = breakout_room.some(r => r.id === prevRoomId); // Case A: Room deleted if (!roomStillExists) { - showDeduplicatedToast(`current-room-closed-${prevRoomId}`, { - leadingIconName: 'close-room', - type: 'error', - text1: `Host: ${senderName} has closed "${ - prevRoom?.name || '' - }" room.`, - text2: 'Returning to main room...', - visibilityTime: 3000, - }); + // Don't show toast if the user is the author + if (srcuid !== localUid) { + showDeduplicatedToast(`current-room-closed-${prevRoomId}`, { + leadingIconName: 'close-room', + type: 'error', + text1: `Host: ${senderName} has closed "${ + prevRoom?.name || '' + }" room.`, + text2: 'Returning to main room...', + visibilityTime: 3000, + }); + } } else { // Host removed user from room (handled here) // (Room still exists for others, but you were unassigned) - showDeduplicatedToast(`moved-to-main-${prevRoomId}`, { - leadingIconName: 'arrow-up', - type: 'info', - text1: `Host: ${senderName} has moved you to main room.`, - visibilityTime: 3000, - }); + // Don't show toast if the user is the author + if (srcuid !== localUid) { + showDeduplicatedToast(`moved-to-main-${prevRoomId}`, { + leadingIconName: 'arrow-up', + type: 'info', + text1: `Host: ${senderName} has moved you to main room.`, + visibilityTime: 3000, + }); + } } // Set transition flag - user will remount in main room and need fresh data @@ -2336,13 +2348,16 @@ const BreakoutRoomProvider = ({ // 4. Rooms control switched if (switch_room && !prevSwitchRoom) { console.log('supriya-sync-ordering 4. switch_room changed: '); - showDeduplicatedToast('switch-room-toggle', { - leadingIconName: 'open-room', - type: 'info', - text1: `Host:${senderName} has opened breakout rooms.`, - text2: 'Please choose a room to join.', - visibilityTime: 3000, - }); + // Don't show toast if the user is the author + if (srcuid !== localUid) { + showDeduplicatedToast('switch-room-toggle', { + leadingIconName: 'open-room', + type: 'info', + text1: `Host:${senderName} has opened breakout rooms.`, + text2: 'Please choose a room to join.', + visibilityTime: 3000, + }); + } } // 5. Group renamed @@ -2350,11 +2365,14 @@ const BreakoutRoomProvider = ({ const after = breakout_room.find(r => r.id === prevRoom.id); if (after && after.name !== prevRoom.name) { console.log('supriya-sync-ordering 5. group renamed '); - showDeduplicatedToast(`room-renamed-${after.id}`, { - type: 'info', - text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`, - visibilityTime: 3000, - }); + // Don't show toast if the user is the author + if (srcuid !== localUid) { + showDeduplicatedToast(`room-renamed-${after.id}`, { + type: 'info', + text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`, + visibilityTime: 3000, + }); + } } }); From 0982749449ba1fca99d1cb8ce6d97c81395a1e98 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 11:39:10 +0530 Subject: [PATCH 26/56] restore the attribute when rejoining breakout room --- .../src/rtm/RTMConfigureMainRoomProvider.tsx | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/template/src/rtm/RTMConfigureMainRoomProvider.tsx b/template/src/rtm/RTMConfigureMainRoomProvider.tsx index d5fa58c17..e8649ec06 100644 --- a/template/src/rtm/RTMConfigureMainRoomProvider.tsx +++ b/template/src/rtm/RTMConfigureMainRoomProvider.tsx @@ -294,17 +294,18 @@ const RTMConfigureMainRoomProvider: React.FC< attr.items.forEach(item => { try { // Check if this is a room-aware session attribute for current room - if (item.key && item.key.startsWith(`${RTM_ROOMS.MAIN}__`)) { - const parsed = JSON.parse(item.value); - if (parsed.persistLevel === PersistanceLevel.Session) { - // Replay into eventDispatcher so state gets rebuilt - eventDispatcher( - {evt: item.key, value: item.value}, - uid, - Date.now(), - ); - } + // if (item.key && item.key.startsWith(`${RTM_ROOMS.MAIN}__`)) { + const parsed = JSON.parse(item.value); + if (parsed.persistLevel === PersistanceLevel.Session) { + // Put into queuue + const data = {evt: item.key, value: item.value}; + EventsQueue.enqueue({ + data, + uid, + ts: Date.now(), + }); } + // } } catch (e) { console.log('Failed to rehydrate session attribute', item.key, e); } @@ -317,7 +318,7 @@ const RTMConfigureMainRoomProvider: React.FC< const init = async () => { // Set main room as active channel when this provider mounts again active const currentActiveChannel = RTMEngine.getInstance().getActiveChannelName(); - // const wasInBreakoutRoom = currentActiveChannel === RTM_ROOMS.BREAKOUT; + const wasInBreakoutRoom = currentActiveChannel === RTM_ROOMS.BREAKOUT; if (currentActiveChannel !== RTM_ROOMS.MAIN) { RTMEngine.getInstance().setActiveChannelName(RTM_ROOMS.MAIN); @@ -329,9 +330,9 @@ const RTMConfigureMainRoomProvider: React.FC< ); // // Rehydrate session attributes ONLY when returning from breakout room - // if (wasInBreakoutRoom) { - // await rehydrateSessionAttributes(); - // } + if (wasInBreakoutRoom) { + await rehydrateSessionAttributes(); + } await getChannelAttributes(); From 05cd34292d7cf394f1430320b7bdf2ecd5504ab7 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 11:48:53 +0530 Subject: [PATCH 27/56] style light theme fix --- .../components/breakout-room/ui/BreakoutRoomParticipants.tsx | 3 ++- .../src/components/breakout-room/ui/BreakoutRoomSettings.tsx | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx b/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx index 6eb34b09b..840ddaad7 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomParticipants.tsx @@ -17,10 +17,11 @@ import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; import ThemeConfig from '../../../theme'; import ImageIcon from '../../../atoms/ImageIcon'; import Tooltip from '../../../atoms/Tooltip'; +import {BreakoutRoomUser} from '../state/reducer'; interface Props { isHost?: boolean; - participants?: {uid: UidType; user: ContentInterface}[]; + participants?: {uid: UidType; user: BreakoutRoomUser}[]; } const BreakoutRoomParticipants: React.FC = ({ diff --git a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx index dfa7e7981..6a2a77975 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx @@ -64,7 +64,7 @@ export default function BreakoutRoomSettings() { - + @@ -93,7 +94,7 @@ export default function BreakoutRoomSettings() { disabled={$config.EVENT_MODE} isEnabled={canUserSwitchRoom} toggleSwitch={toggleRoomSwitchingAllowed} - circleColor={$config.FONT_COLOR} + circleColor={$config.PRIMARY_ACTION_TEXT_COLOR} /> From 9fa95328da28787c134e3d1afaf5c01757a2599a Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 11:50:15 +0530 Subject: [PATCH 28/56] room header padding --- .../components/breakout-room/ui/BreakoutRoomGroupSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index 2bed1cbcf..4f32c72d7 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -325,7 +325,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', - paddingHorizontal: 12, + // paddingHorizontal: 12, paddingVertical: 16, }, headerLeft: {}, From ac4161df316db98ff49d53a5473d702f5f793a51 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 12:29:18 +0530 Subject: [PATCH 29/56] Review/add missing commits (#755) * fix name retry and add announcement icon * add callback fresh state * fix announcement icon and name * fix manual participant ui * handle user-left --- template/src/atoms/Dropdown.tsx | 5 + .../ui/BreakoutRoomGroupSettings.tsx | 106 +++-- .../breakout-room/ui/BreakoutRoomView.tsx | 1 + .../ui/ParticipantManualAssignmentModal.tsx | 403 +++++++++++++----- .../chat-messages/useChatMessages.tsx | 18 +- .../src/components/chat/chatConfigure.tsx | 1 + template/src/components/useUserPreference.tsx | 6 + template/src/rtm/RTMGlobalStateProvider.tsx | 16 +- .../rtm/hooks/useMainRoomUserDisplayName.ts | 26 +- template/src/rtm/rtm-presence-utils.ts | 25 +- 10 files changed, 426 insertions(+), 181 deletions(-) diff --git a/template/src/atoms/Dropdown.tsx b/template/src/atoms/Dropdown.tsx index 99a72c0cb..01fb9e00a 100644 --- a/template/src/atoms/Dropdown.tsx +++ b/template/src/atoms/Dropdown.tsx @@ -14,6 +14,8 @@ import { Modal, View, Image, + StyleProp, + ViewStyle, } from 'react-native'; import {isWebInternal} from '../utils/common'; import ThemeConfig from '../theme'; @@ -28,6 +30,7 @@ interface Props { onSelect: (item: {label: string; value: string}) => void; enabled: boolean; selectedValue: string; + containerStyle?: StyleProp; } const Dropdown: FC = ({ @@ -37,6 +40,7 @@ const Dropdown: FC = ({ enabled, selectedValue, icon, + containerStyle = {}, }) => { const DropdownButton = useRef(); const [visible, setVisible] = useState(false); @@ -148,6 +152,7 @@ const Dropdown: FC = ({ ref={DropdownButton} style={[ styles.dropdownOptionContainer, + containerStyle, !enabled || !data || !data.length ? {opacity: ThemeConfig.EmphasisOpacity.disabled} : {}, diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index 4f32c72d7..d5f4e85c2 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -43,6 +43,7 @@ const BreakoutRoomGroupSettings = ({scrollOffset}) => { data: {isHost, uid, chat}, } = useRoomInfo(); const localUid = useLocalUid(); + const {defaultContent} = useContent(); const {sendChatSDKMessage} = useChatConfigure(); const {isUserHandRaised} = useRaiseHand(); const {defaultContent} = useContent(); @@ -91,48 +92,61 @@ const BreakoutRoomGroupSettings = ({scrollOffset}) => { }; const renderMember = (memberUId: UidType) => { - // Hide offline users from UI - check mainRoomRTMUsers for offline status + // Check for offline status using mainRoomRTMUsers const rtmMemberData = mainRoomRTMUsers[memberUId]; - - // If user is offline in RTM data, don't render them - if (rtmMemberData && rtmMemberData?.offline) { - return null; - } + const isOffline = rtmMemberData?.offline; return ( - + - - {getDisplayName(memberUId)} - + + + {getDisplayName(memberUId)} + + {isOffline && ( + (user is offline) + )} + - - {isUserHandRaised(memberUId) ? ( - - - - ) : ( - <> - )} - {permissions.canHostManageMainRoom && memberUId !== localUid ? ( - - - - ) : ( - <> - )} - + {!isOffline && ( + + {isUserHandRaised(memberUId) ? ( + + + + ) : ( + <> + )} + {permissions.canHostManageMainRoom && memberUId !== localUid ? ( + + + + ) : ( + <> + )} + + )} ); }; @@ -449,12 +463,20 @@ const styles = StyleSheet.create({ minHeight: 64, gap: 8, }, + memberItemOffline: { + opacity: 0.5, + backgroundColor: $config.CARD_LAYER_2_COLOR, + }, memberInfo: { flexDirection: 'row', alignItems: 'center', flex: 1, gap: 8, }, + memberNameContainer: { + flex: 1, + flexDirection: 'column', + }, memberDragHandle: { marginRight: 12, width: 16, @@ -479,12 +501,22 @@ const styles = StyleSheet.create({ fontWeight: '600', }, memberName: { - flex: 1, fontSize: ThemeConfig.FontSize.small, lineHeight: 20, fontWeight: '400', color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, }, + memberNameOffline: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, + }, + memberOfflineStatus: { + fontSize: ThemeConfig.FontSize.tiny, + lineHeight: 14, + fontWeight: '400', + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, + fontStyle: 'italic', + marginTop: 2, + }, memberMenu: { padding: 8, marginLeft: 'auto', @@ -523,12 +555,18 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'center', }, + userAvatarOffline: { + backgroundColor: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, + }, userAvatarText: { fontSize: ThemeConfig.FontSize.tiny, lineHeight: 12, fontWeight: '600', color: $config.BACKGROUND_COLOR, }, + userAvatarTextOffline: { + color: $config.BACKGROUND_COLOR + '80', + }, expandIcon: { width: 32, height: 32, diff --git a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx index 96b362029..44a004424 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx @@ -70,6 +70,7 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { - - No text-tracks found for this meeting - + No participants found ); @@ -47,7 +41,7 @@ function ParticipantRow({ onSelectionChange, localUid, }: { - participant: {uid: UidType; user: ContentInterface}; + participant: {uid: UidType; user: BreakoutRoomUser}; assignment: ManualParticipantAssignment; rooms: {label: string; value: string}[]; onAssignmentChange: (uid: UidType, roomId: string | null) => void; @@ -56,33 +50,49 @@ function ParticipantRow({ }) { const selectedValue = assignment?.roomId || 'unassigned'; const displayName = - participant?.uid === localUid - ? `${participant?.user?.name} (me)` - : participant?.user?.name; + participant.uid === localUid + ? `${participant.user.name} (me)` + : participant.user.name; return ( - - + + onSelectionChange(participant.uid)} - label={displayName} + label={''} + checkBoxStyle={style.checkboxIcon} /> - - { - onAssignmentChange( - participant.uid, - value === 'unassigned' ? null : value, - ); - }} - selectedValue={selectedValue} - /> + + + + + {displayName} + + + + + + { + onAssignmentChange( + participant.uid, + value === 'unassigned' ? null : value, + ); + }} + containerStyle={style.dropdownContainer} + selectedValue={selectedValue} + /> + ); @@ -106,6 +116,7 @@ export default function ParticipantManualAssignmentModal( } = useBreakoutRoom(); // Local state for assignments + const [searchQuery, setSearchQuery] = useState(''); const [localAssignments, setLocalAssignments] = useState< ManualParticipantAssignment[] >(() => { @@ -118,7 +129,7 @@ export default function ParticipantManualAssignmentModal( return unassignedParticipants.map(participant => ({ uid: participant.uid, roomId: null, // Start unassigned - isHost: participant.user?.isHost || false, + isHost: participant.user.isHost, isSelected: false, })); }); @@ -232,6 +243,16 @@ export default function ParticipantManualAssignmentModal( }; const selectedCount = localAssignments.filter(a => a.isSelected).length; + const unassignedCount = localAssignments.filter(a => !a.roomId).length; + + // Filter participants based on search query + const filteredParticipants = unassignedParticipants.filter(participant => { + const displayName = + participant.uid === localUid + ? `${participant.user.name} (me)` + : participant.user.name; + return displayName.toLowerCase().includes(searchQuery.toLowerCase()); + }); return ( - 0 ? {} : style.titleLowOpacity, - ]}> - + {/* Search Bar */} + + + - {localAssignments.length} - - ({localAssignments.filter(a => !a.roomId).length} Unassigned) - - - - toggleSelectAll()} - label={getSelectAllLabel()} + + {/* Participant Count */} + + + + + + {localAssignments.length} + + + ({unassignedCount} Unassigned) + + {selectedCount > 0 && ( @@ -279,39 +314,62 @@ export default function ParticipantManualAssignmentModal( )} + + {/* Select All Controls */} - - - } - bodyStyle={style.tbodyContainer} - renderRow={participant => { - const assignment = localAssignments.find( - a => a.uid === participant.uid, - ); - return ( - + + + toggleSelectAll()} + label={''} + checkBoxStyle={style.checkboxIcon} + // label={getSelectAllLabel()} /> - ); - }} - emptyComponent={} - /> + + + + Name + + + + + Room + + + + + {/* Participants List */} + + {filteredParticipants.length === 0 ? ( + + ) : ( + + {filteredParticipants.map(participant => { + const assignment = localAssignments.find( + a => a.uid === participant.uid, + ); + return ( + + ); + })} + + )} + @@ -356,7 +414,8 @@ const style = StyleSheet.create({ }, mbody: { flex: 1, - padding: 12, + padding: 20, + gap: 16, borderTopColor: $config.CARD_LAYER_3_COLOR, borderTopWidth: 1, borderBottomColor: $config.CARD_LAYER_3_COLOR, @@ -374,66 +433,182 @@ const style = StyleSheet.create({ borderBottomLeftRadius: 8, borderBottomRightRadius: 8, }, - titleLowOpacity: { - opacity: 0.2, + // Search Container + searchContainer: { + position: 'relative', + width: '100%', + }, + searchIcon: { + position: 'absolute', + left: 8, + top: 12, }, - titleContainer: { + searchInput: { + height: 36, + borderWidth: 1, + borderColor: $config.INPUT_FIELD_BORDER_COLOR, + borderRadius: 4, + paddingHorizontal: 12, + paddingLeft: 30, + fontSize: ThemeConfig.FontSize.small, + color: $config.FONT_COLOR, + backgroundColor: $config.INPUT_FIELD_BACKGROUND_COLOR, + }, + + // Participant Count + participantSummaryContainer: { display: 'flex', flexDirection: 'row', - gap: 4, alignItems: 'center', - paddingVertical: 16, + justifyContent: 'space-between', }, - title: { - color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, - fontSize: ThemeConfig.FontSize.small, - lineHeight: 16, - fontWeight: '500', + participantCountContainer: { + display: 'flex', + flexDirection: 'row', + gap: 4, + alignItems: 'center', }, - infotextContainer: { + participantCountTextContainer: { display: 'flex', - flex: 1, + flexDirection: 'row', alignItems: 'center', - justifyContent: 'center', + gap: 4, }, - infoText: { - fontSize: 14, + participantCount: { + fontSize: ThemeConfig.FontSize.small, fontWeight: '500', - fontFamily: 'Source Sans Pro', + lineHeight: 16, + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + }, + participantCountLowOpacity: { color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, }, - participantTableControls: { - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', - flexDirection: 'row', - paddingBottom: 12, + dropdownContainer: { + paddingHorizontal: 12, + paddingVertical: 8, + backgroundColor: $config.CARD_LAYER_2_COLOR, + borderRadius: 4, + borderWidth: 0, + }, + checkboxIconContainer: { + paddingRight: 24, + }, + checkboxIcon: { + width: 17, + height: 17, }, participantTable: { flex: 1, - backgroundColor: $config.BACKGROUND_COLOR, }, - tHeadRow: { + participantTableHeader: { + display: 'flex', + flexShrink: 0, + alignItems: 'center', + flexDirection: 'row', borderTopLeftRadius: 2, borderTopRightRadius: 2, + backgroundColor: $config.CARD_LAYER_2_COLOR, + paddingHorizontal: 8, + height: 40, + }, + participantTableHeaderRow: { + flex: 1, + alignSelf: 'stretch', + flexDirection: 'row', + }, + participantTableHeaderRowCell: { + flex: 1, + alignSelf: 'stretch', + justifyContent: 'center', }, - tbodyContainer: { + participantTableHeaderRowCellText: { + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium, + fontSize: ThemeConfig.FontSize.small, + fontFamily: ThemeConfig.FontFamily.sansPro, + lineHeight: 16, + }, + // Participants List + participantsList: { + flex: 1, + padding: 8, + paddingBottom: 0, backgroundColor: $config.BACKGROUND_COLOR, - borderRadius: 2, }, - tbrow: { + participantsScrollView: { + flex: 1, + }, + // Participant Row + participantRow: { + // display: 'flex', + // flexDirection: 'row', + // alignItems: 'center', + // justifyContent: 'space-between', + // paddingHorizontal: 16, + // paddingVertical: 12, + // borderBottomWidth: 1, + // borderBottomColor: $config.CARD_LAYER_3_COLOR + '40', + // minHeight: 60, + }, + participantBodyRow: { display: 'flex', alignSelf: 'stretch', - minHeight: 48, + // minHeight: 50, flexDirection: 'row', - paddingVertical: 8, + paddingBottom: 8, + // paddingTop: 20, }, - td: { + participantBodytRowCell: { flex: 1, alignSelf: 'center', justifyContent: 'center', gap: 10, }, + checkboxCell: { + maxWidth: 50, + }, + participantInfo: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + flex: 1, + gap: 12, + }, + participantAvatar: { + width: 32, + height: 32, + borderRadius: 16, + backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR, + }, + participantAvatarText: { + fontSize: ThemeConfig.FontSize.tiny, + fontWeight: '600', + color: $config.BACKGROUND_COLOR, + }, + participantName: { + flex: 1, + fontSize: ThemeConfig.FontSize.small, + fontWeight: '400', + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, + }, + participantDropdown: { + minWidth: 120, + }, + // Empty State + infotextContainer: { + display: 'flex', + flex: 1, + alignItems: 'center', + justifyContent: 'center', + paddingVertical: 32, + gap: 12, + }, + infoText: { + fontSize: ThemeConfig.FontSize.small, + fontWeight: '500', + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, + textAlign: 'center', + }, + // Buttons actionBtnText: { color: $config.SECONDARY_ACTION_COLOR, fontSize: ThemeConfig.FontSize.small, diff --git a/template/src/components/chat-messages/useChatMessages.tsx b/template/src/components/chat-messages/useChatMessages.tsx index 2da3a50bb..0d465add4 100644 --- a/template/src/components/chat-messages/useChatMessages.tsx +++ b/template/src/components/chat-messages/useChatMessages.tsx @@ -290,11 +290,20 @@ const ChatMessagesProvider = (props: ChatMessagesProviderProps) => { //commented for v1 release //const fromText = useString('messageSenderNotificationLabel'); - const fromText = (name: string, msgType: ChatMessageType) => { + const fromText = ( + name: string, + msgType: ChatMessageType, + announcement?: AnnouncementText, + ) => { let text = ''; switch (msgType) { case ChatMessageType.TXT: - text = txtToastHeading?.current(name); + if (announcement?.text) { + text = `${announcement.sender}: made an announcement in public chat`; + } else { + text = txtToastHeading?.current(name); + } + break; case ChatMessageType.IMAGE: text = imgToastHeading?.current(name); @@ -708,13 +717,16 @@ const ChatMessagesProvider = (props: ChatMessagesProviderProps) => { : isPrivateMessage ? privateMessageLabel?.current() : //@ts-ignore - defaultContentRef.current.defaultContent[uidAsNumber]?.name + announcement?.sender + ? announcement.sender + : defaultContentRef.current.defaultContent[uidAsNumber]?.name ? fromText( trimText( //@ts-ignore defaultContentRef.current.defaultContent[uidAsNumber]?.name, ), msgType, + announcement, ) : '', text2: isPrivateMessage diff --git a/template/src/components/chat/chatConfigure.tsx b/template/src/components/chat/chatConfigure.tsx index 47bfd374d..799420341 100644 --- a/template/src/components/chat/chatConfigure.tsx +++ b/template/src/components/chat/chatConfigure.tsx @@ -273,6 +273,7 @@ const ChatConfigure = ({children}) => { if (message.chatType === SDKChatType.GROUP_CHAT) { // show to notifcation- group msg received + showMessageNotification( message.msg, fromUser, diff --git a/template/src/components/useUserPreference.tsx b/template/src/components/useUserPreference.tsx index 64219d794..97074c561 100644 --- a/template/src/components/useUserPreference.tsx +++ b/template/src/components/useUserPreference.tsx @@ -189,6 +189,7 @@ const UserPreferenceProvider = (props: { name: getScreenShareName(displayName || userText), type: 'screenshare', }); + console.log('user-attribute name check displayName 1', displayName); if (hasUserJoinedRTM && callActive) { //set local uids @@ -201,9 +202,14 @@ const UserPreferenceProvider = (props: { }, }; }); + console.log('user-attribute name check displayName 2', displayName); + + syncUserState(localUid, {name: displayName || userText}); } if (hasUserJoinedRTM) { + console.log('user-attribute name check displayName 3', displayName); + //update remote state for user and screenshare events.send( EventNames.NAME_ATTRIBUTE, diff --git a/template/src/rtm/RTMGlobalStateProvider.tsx b/template/src/rtm/RTMGlobalStateProvider.tsx index 8bb38f427..dc446cc55 100644 --- a/template/src/rtm/RTMGlobalStateProvider.tsx +++ b/template/src/rtm/RTMGlobalStateProvider.tsx @@ -109,6 +109,9 @@ const RTMGlobalStateProvider: React.FC = ({ const [customRTMMainRoomData, setCustomRTMMainRoomData] = useState<{ [key: string]: any; }>({}); + useEffect(() => { + console.log('mainRoomRTMUsers user-attributes changed', mainRoomRTMUsers); + }, [mainRoomRTMUsers]); // Timeout Refs const isRTMMounted = useRef(true); @@ -268,7 +271,7 @@ const RTMGlobalStateProvider: React.FC = ({ }, ); console.log( - `supriya rtm backoffAttributes for ${member.userId}`, + `supriya rtm backoffAttributes user-attributes for ${member.userId}`, userAttributes, ); mapUserAttributesToState( @@ -276,18 +279,7 @@ const RTMGlobalStateProvider: React.FC = ({ member.userId, updateMainRoomUser, ); - console.log( - `supriya rtm backoffAttributes for ${member.userId}`, - userAttributes, - ); - // setting screenshare data - // name of the screenUid, isActive: false, (when the user starts screensharing it becomes true) - // isActive to identify all active screenshare users in the call - console.log( - 'supriya-session-test userAttributes', - userAttributes, - ); userAttributes?.items?.forEach(item => { processUserAttributeForQueue( item, diff --git a/template/src/rtm/hooks/useMainRoomUserDisplayName.ts b/template/src/rtm/hooks/useMainRoomUserDisplayName.ts index 56eb54e86..a8753bd5d 100644 --- a/template/src/rtm/hooks/useMainRoomUserDisplayName.ts +++ b/template/src/rtm/hooks/useMainRoomUserDisplayName.ts @@ -9,6 +9,7 @@ information visit https://appbuilder.agora.io. ********************************************* */ +import {useCallback} from 'react'; import {videoRoomUserFallbackText} from '../../language/default-labels/videoCallScreenLabels'; import {useString} from '../../utils/useString'; import {UidType} from '../../../agora-rn-uikit'; @@ -23,13 +24,20 @@ export const useMainRoomUserDisplayName = () => { const {defaultContent} = useContent(); const remoteUserDefaultLabel = useString(videoRoomUserFallbackText)(); - return (uid: UidType): string => { - // Priority: Local defaultContent → Global mainRoomRTMUsers → UID fallback - // TODO:SUP add trimText - return ( - defaultContent?.[uid]?.name || - mainRoomRTMUsers?.[uid]?.name || - remoteUserDefaultLabel - ); - }; + const sanitize = (name?: string) => name?.trim() || undefined; + + // 👇 useCallback ensures the returned function updates whenever + // defaultContent or mainRoomRTMUsers change + return useCallback( + (uid: UidType): string => { + console.log('supriya-name defaultContent', defaultContent); + console.log('supriya-name mainRoomRTMUsers', mainRoomRTMUsers); + return ( + sanitize(defaultContent?.[uid]?.name) || + sanitize(mainRoomRTMUsers?.[uid]?.name) || + remoteUserDefaultLabel + ); + }, + [defaultContent, mainRoomRTMUsers, remoteUserDefaultLabel], + ); }; diff --git a/template/src/rtm/rtm-presence-utils.ts b/template/src/rtm/rtm-presence-utils.ts index ac95b231c..c9a35df96 100644 --- a/template/src/rtm/rtm-presence-utils.ts +++ b/template/src/rtm/rtm-presence-utils.ts @@ -105,20 +105,20 @@ export const fetchUserAttributesWithRetries = async ( console.log('rudra-core-client: RTM attributes for member not found'); throw new Error('No attribute items found'); } - console.log('sup-attribute-check attributes', attr); + console.log('sup-attribute-check user-attributes', attr); // 2. Partial update allowed (screenUid, isHost, etc.) const hasAny = attr.items.some(i => i.value); if (!hasAny) { - throw new Error('No usable attributes yet'); + throw new Error('No usable user-attributes yet'); } - console.log('sup-attribute-check hasAny', hasAny); + console.log('sup-attribute-check user-attributes hasAny', hasAny); // 3. If name exists, return immediately const hasNameAttribute = attr.items.find( i => i.key === 'name' && i.value, ); - console.log('sup-attribute-check name', hasNameAttribute); + console.log('sup-attribute-check user-attributes name', hasNameAttribute); if (hasNameAttribute) { return attr; } @@ -130,12 +130,14 @@ export const fetchUserAttributesWithRetries = async ( if (opts?.isMounted && !opts?.isMounted) { throw new Error(`Component unmounted while retrying ${userId}`); } - console.log('sup-attribute-check inside name backoff'); + console.log( + 'sup-attribute-check user-attributes inside name backoff', + ); const retriedAttributes: NativeGetUserMetadataResponse = await client.storage.getUserMetadata({userId}); console.log( - 'sup-attribute-check retriedAttributes', + 'sup-attribute-check user-attributes retriedAttributes', retriedAttributes, ); @@ -143,22 +145,27 @@ export const fetchUserAttributesWithRetries = async ( i => i.key === 'name' && i.value, ); console.log( - 'sup-attribute-check hasNameAttributeRetry', + 'sup-attribute-check user-attributes hasNameAttributeRetry', hasNameAttributeRetry, ); if (!hasNameAttributeRetry) { - throw new Error('Name still not found'); + throw new Error('user-attributes Name still not found'); } if (opts?.isMounted) { - console.log('sup-attribute-check onNameFound'); + console.log( + 'sup-attribute-check user-attributes onNameFound', + retriedAttributes, + ); opts?.onNameFound?.(retriedAttributes); } return retriedAttributes; }, { retry: () => true, + maxDelay: Infinity, // No maximum delay limit for name + numOfAttempts: Infinity, // Infinite attempts for name }, ).catch(() => { console.log( From d0ba0ce6dfe75c161a39bf98f9797b1381cce010 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 12:31:38 +0530 Subject: [PATCH 30/56] font weight --- .../breakout-room/ui/SelectParticipantAssignmentStrategy.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx b/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx index 7e31b7aef..7ee59eb40 100644 --- a/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx +++ b/template/src/components/breakout-room/ui/SelectParticipantAssignmentStrategy.tsx @@ -50,7 +50,7 @@ const style = StyleSheet.create({ fontWeight: '400', fontSize: ThemeConfig.FontSize.small, lineHeight: 16, - color: $config.FONT_COLOR, + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, fontFamily: ThemeConfig.FontFamily.sansPro, }, }); From 7d70c90d697edfff0896090b9238bc1bee2924ee Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 12:34:44 +0530 Subject: [PATCH 31/56] duplicate toast --- .../components/breakout-room/ui/BreakoutRoomGroupSettings.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index d5f4e85c2..7c20d9eef 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -46,7 +46,6 @@ const BreakoutRoomGroupSettings = ({scrollOffset}) => { const {defaultContent} = useContent(); const {sendChatSDKMessage} = useChatConfigure(); const {isUserHandRaised} = useRaiseHand(); - const {defaultContent} = useContent(); const { breakoutGroups, From 9feed2f18f844628bfe45dabb9ebf4ff7d6f2fa1 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 14:35:14 +0530 Subject: [PATCH 32/56] css fixes --- .../ui/ParticipantManualAssignmentModal.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx b/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx index 32506621d..4e822d62f 100644 --- a/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx +++ b/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx @@ -488,12 +488,17 @@ const style = StyleSheet.create({ paddingVertical: 8, backgroundColor: $config.CARD_LAYER_2_COLOR, borderRadius: 4, - borderWidth: 0, + borderLeftWidth: 0, + borderTopWidth: 0, + borderRightWidth: 0, + borderBottomWidth: 0, }, checkboxIconContainer: { paddingRight: 24, }, checkboxIcon: { + borderColor: $config.SECONDARY_ACTION_COLOR, + borderRadius: 2, width: 17, height: 17, }, @@ -522,7 +527,7 @@ const style = StyleSheet.create({ justifyContent: 'center', }, participantTableHeaderRowCellText: { - color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium, + color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.high, fontSize: ThemeConfig.FontSize.small, fontFamily: ThemeConfig.FontFamily.sansPro, lineHeight: 16, From 92494d9eb2064353f52e36f2fbbbaf5d176929c7 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 14:52:31 +0530 Subject: [PATCH 33/56] fix avatars --- .../breakout-room/ui/BreakoutRoomGroupSettings.tsx | 6 +++--- .../breakout-room/ui/BreakoutRoomMainRoomUsers.tsx | 2 +- .../breakout-room/ui/ParticipantManualAssignmentModal.tsx | 6 +++++- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index 7c20d9eef..2bfa2b2e0 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -404,7 +404,7 @@ const styles = StyleSheet.create({ height: 28, }, roomActionBtnText: { - color: $config.SECONDARY_ACTION_COLOR, + color: $config.PRIMARY_ACTION_TEXT_COLOR, fontSize: ThemeConfig.FontSize.small, lineHeight: 16, fontWeight: '600', @@ -546,20 +546,20 @@ const styles = StyleSheet.create({ fontStyle: 'italic', }, userAvatarContainer: { - backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR, width: 24, height: 24, borderRadius: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', + backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR, }, userAvatarOffline: { backgroundColor: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.low, }, userAvatarText: { fontSize: ThemeConfig.FontSize.tiny, - lineHeight: 12, + lineHeight: 24, fontWeight: '600', color: $config.BACKGROUND_COLOR, }, diff --git a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx index 75c3ff927..50c6e0b77 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomMainRoomUsers.tsx @@ -134,7 +134,7 @@ const style = StyleSheet.create({ }, userAvatarText: { fontSize: ThemeConfig.FontSize.tiny, - lineHeight: 12, + lineHeight: 24, fontWeight: '600', color: $config.BACKGROUND_COLOR, display: 'flex', diff --git a/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx b/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx index 4e822d62f..eab44d699 100644 --- a/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx +++ b/template/src/components/breakout-room/ui/ParticipantManualAssignmentModal.tsx @@ -582,10 +582,14 @@ const style = StyleSheet.create({ width: 32, height: 32, borderRadius: 16, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', backgroundColor: $config.VIDEO_AUDIO_TILE_AVATAR_COLOR, }, participantAvatarText: { fontSize: ThemeConfig.FontSize.tiny, + lineHeight: 32, fontWeight: '600', color: $config.BACKGROUND_COLOR, }, @@ -615,7 +619,7 @@ const style = StyleSheet.create({ }, // Buttons actionBtnText: { - color: $config.SECONDARY_ACTION_COLOR, + color: $config.PRIMARY_ACTION_TEXT_COLOR, fontSize: ThemeConfig.FontSize.small, lineHeight: 16, }, From 28a5f221b6763825da906f7d9e9534cea574acdf Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 7 Oct 2025 22:31:05 +0530 Subject: [PATCH 34/56] add anouncement icon --- .../components/breakout-room/ui/BreakoutRoomGroupSettings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index 2bfa2b2e0..d96d82acc 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -288,7 +288,7 @@ const BreakoutRoomGroupSettings = ({scrollOffset}) => { Date: Wed, 8 Oct 2025 10:30:44 +0530 Subject: [PATCH 35/56] solve mediastream error --- .../beauty-effect/useBeautyEffects.tsx | 71 +++++++++++++++---- 1 file changed, 58 insertions(+), 13 deletions(-) diff --git a/template/src/components/beauty-effect/useBeautyEffects.tsx b/template/src/components/beauty-effect/useBeautyEffects.tsx index b4e4017ec..5b3adbe79 100644 --- a/template/src/components/beauty-effect/useBeautyEffects.tsx +++ b/template/src/components/beauty-effect/useBeautyEffects.tsx @@ -1,6 +1,5 @@ import {createHook} from 'customization-implementation'; -import React, {useState} from 'react'; -import {useEffect, useRef} from 'react'; +import React, {useState, useEffect, useRef} from 'react'; import AgoraRTC, {ILocalVideoTrack} from 'agora-rtc-sdk-ng'; import BeautyExtension from 'agora-extension-beauty-effect'; import {useRoomInfo, useRtc} from 'customization-api'; @@ -77,19 +76,51 @@ const BeautyEffectProvider: React.FC = ({children}) => { const {RtcEngineUnsafe} = useRtc(); //@ts-ignore - const localVideoTrack = RtcEngineUnsafe?.localStream?.video; + const localVideoTrack: ILocalVideoTrack | undefined = + RtcEngineUnsafe?.localStream?.video; + + // ✅ useRef to persist timeout across renders + const timeoutRef = useRef | null>(null); + + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } if (!roomPreference?.disableVideoProcessors) { - if ($config.ENABLE_VIRTUAL_BACKGROUND) { - localVideoTrack - ?.pipe(beautyProcessor) - .pipe(vbProcessor) - .pipe(localVideoTrack?.processorDestination); - } else { - localVideoTrack - ?.pipe(beautyProcessor) - .pipe(localVideoTrack?.processorDestination); - } + console.log( + 'supriya-trackstatus', + localVideoTrack?.getMediaStreamTrack()?.readyState, + ); + + /** + * Small delay to ensure the new track is stable + * when we move from main room to breakout room the track changes + * from live to ended instantly as the user audio or video preferences are applied + * It solves the error 'MediaStreamTrackProcessor': Input track cannot be ended' + */ + timeoutRef.current = setTimeout(() => { + const trackStatus = localVideoTrack?.getMediaStreamTrack()?.readyState; + if (trackStatus === 'live') { + console.log('supriya-trackstatus applying'); + try { + if ($config.ENABLE_VIRTUAL_BACKGROUND) { + localVideoTrack + ?.pipe(beautyProcessor) + .pipe(vbProcessor) + .pipe(localVideoTrack?.processorDestination); + } else { + localVideoTrack + ?.pipe(beautyProcessor) + .pipe(localVideoTrack?.processorDestination); + } + } catch (err) { + console.error('Error applying processors:', err); + } + } else { + console.warn('Track not live after delay, skipping pipe'); + } + }, 300); } useEffect(() => { @@ -113,6 +144,20 @@ const BeautyEffectProvider: React.FC = ({children}) => { lighteningContrastLevel, ]); + // Proper cleanup for both processor and timeout + useEffect(() => { + return () => { + console.log('supriya-trackstatus cleanup'); + beautyProcessor?.disable(); + beautyProcessor?.unpipe?.(); + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + console.log('supriya-trackstatus timeout cleared'); + } + }; + }, []); + const removeBeautyEffect = async () => { await beautyProcessor.disable(); }; From 94e9d4c0e18233cac6b7b2c2df41cf602dd01406 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Wed, 8 Oct 2025 11:24:59 +0530 Subject: [PATCH 36/56] add rtm status banner --- template/src/rtm/RTMStatusBanner.tsx | 99 ++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 template/src/rtm/RTMStatusBanner.tsx diff --git a/template/src/rtm/RTMStatusBanner.tsx b/template/src/rtm/RTMStatusBanner.tsx new file mode 100644 index 000000000..74acd206d --- /dev/null +++ b/template/src/rtm/RTMStatusBanner.tsx @@ -0,0 +1,99 @@ +import React, {useState, useEffect} from 'react'; +import {View, Text, StyleSheet} from 'react-native'; +import {useRTMCore} from './RTMCoreProvider'; +import {nativeLinkStateMapping} from '../../bridge/rtm/web/Types'; + +export const RTMStatusBanner = () => { + const {connectionState, error, isLoggedIn} = useRTMCore(); + + // internal debounced copy of the state to prevent flicker + const [visibleState, setVisibleState] = useState(connectionState); + const [visibleError, setVisibleError] = useState(error); + + useEffect(() => { + const timeout = setTimeout(() => { + setVisibleState(connectionState); + setVisibleError(error); + }, 700); // debounce 700 ms + return () => clearTimeout(timeout); + }, [connectionState, error]); + + // Don't show banner if connected and logged in with no errors + if ( + visibleState === nativeLinkStateMapping.CONNECTED && + isLoggedIn && + !visibleError + ) { + return null; + } + + let message = ''; + let isError = false; + + if (visibleError || visibleState === nativeLinkStateMapping.FAILED) { + // Login failed - critical error + message = + 'RTM connection failed. App might not work correctly. Retrying...'; + isError = true; + } else if (visibleState === nativeLinkStateMapping.DISCONNECTED) { + // RTM disconnected in the middle of the call - retrying + message = 'RTM disconnected — retrying connection...'; + isError = false; + } else if ( + visibleState === nativeLinkStateMapping.IDLE || + visibleState === nativeLinkStateMapping.CONNECTING + ) { + // RTM is idle or connecting + message = 'RTM connecting...'; + isError = false; + } else if (!isLoggedIn) { + // Not logged in but no explicit error yet + message = 'RTM connecting...'; + isError = false; + } + + // Don't render banner if there's no message to show + if (!message) { + return null; + } + + return ( + + + {message} + + + ); +}; + +const styles = StyleSheet.create({ + banner: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + paddingVertical: 8, + paddingHorizontal: 16, + zIndex: 9999, + alignItems: 'center', + justifyContent: 'center', + }, + error: { + backgroundColor: '#f8d7da', + }, + warning: { + backgroundColor: '#fff3cd', + }, + text: { + fontSize: 14, + fontWeight: '500', + textAlign: 'center', + }, + errorText: { + color: '#721c24', + }, + warningText: { + color: '#856404', + }, +}); From 7e2a7e1584e914ff4ae11b81f7d2bc3e045dc4e0 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Wed, 8 Oct 2025 11:25:59 +0530 Subject: [PATCH 37/56] exponential backoff increased for login --- template/src/rtm/RTMCoreProvider.tsx | 45 +++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/template/src/rtm/RTMCoreProvider.tsx b/template/src/rtm/RTMCoreProvider.tsx index ceb335b7a..44a8eae02 100644 --- a/template/src/rtm/RTMCoreProvider.tsx +++ b/template/src/rtm/RTMCoreProvider.tsx @@ -23,6 +23,7 @@ import {isWeb, isWebInternal} from '../utils/common'; import isSDK from '../utils/isSDK'; import {useAsyncEffect} from '../utils/useAsyncEffect'; import {nativeLinkStateMapping} from '../../bridge/rtm/web/Types'; +import {RTMStatusBanner} from './RTMStatusBanner'; // ---- Helpers ---- // const delay = (ms: number) => new Promise(r => setTimeout(r, ms)); @@ -45,11 +46,13 @@ async function loginWithBackoff( } catch (e: any) { attempt += 1; onAttempt?.(attempt); + if (attempt > maxAttempts) { - throw new Error(`RTM login failed: ${e?.message ?? e}`); + const errorMsg = `RTM login failed: ${e?.message ?? e}`; + throw new Error(errorMsg); } const backoff = - Math.min(1000 * 2 ** (attempt - 1), 30_000) + + Math.min(5000 * 2 ** (attempt - 1), 60_000) + Math.floor(Math.random() * 300); await delay(backoff); } @@ -108,6 +111,7 @@ export const RTMCoreProvider: React.FC = ({ const mountedRef = useRef(true); const cleaningRef = useRef(false); const callbackRegistry = useRef>(new Map()); + const errorRef = useRef(null); useEffect(() => { mountedRef.current = true; @@ -116,6 +120,22 @@ export const RTMCoreProvider: React.FC = ({ }; }, []); + // Sync error ref with state to prevent state clearing + useEffect(() => { + errorRef.current = error; + }, [error]); + + // Keep error persistent if we have a failed state + useEffect(() => { + if ( + connectionState === nativeLinkStateMapping.FAILED && + !error && + errorRef.current + ) { + setError(errorRef.current); + } + }, [connectionState, error]); + // Memoize userInfo const stableUserInfo = useMemo( () => ({ @@ -240,7 +260,16 @@ export const RTMCoreProvider: React.FC = ({ const onLink = async (evt: LinkStateEvent) => { setConnectionState(evt.currentState); - if (evt.currentState === nativeLinkStateMapping.DISCONNECTED) { + + if (evt.currentState === nativeLinkStateMapping.FAILED) { + setIsLoggedIn(false); + // Set error if we're in FAILED state and don't have one + if (!errorRef.current) { + const failedError = new Error('RTM connection failed'); + errorRef.current = failedError; + setError(failedError); + } + } else if (evt.currentState === nativeLinkStateMapping.DISCONNECTED) { setIsLoggedIn(false); if (stableUserInfo.rtmToken) { try { @@ -249,15 +278,22 @@ export const RTMCoreProvider: React.FC = ({ return; } setIsLoggedIn(true); + // Clear error only after successful login + errorRef.current = null; + setError(null); } catch (err: any) { if (!mountedRef.current) { return; } + errorRef.current = err; setError(err); } } } else if (evt.currentState === nativeLinkStateMapping.CONNECTED) { setIsLoggedIn(true); + // Clear error on successful connection + errorRef.current = null; + setError(null); } }; @@ -274,7 +310,6 @@ export const RTMCoreProvider: React.FC = ({ } (async () => { - console.log('supriya-rtm-lifecycle init'); // 1, Check if engine is already connected // 2. Initialize RTM Engine if (!RTMEngine.getInstance()?.isEngineReady) { @@ -300,6 +335,7 @@ export const RTMCoreProvider: React.FC = ({ if (!mountedRef.current) { return; } + errorRef.current = err; setError(err); } })(); @@ -374,6 +410,7 @@ export const RTMCoreProvider: React.FC = ({ registerCallbacks, unregisterCallbacks, }}> + {/* */} {children} ); From 4e69e642e6e2523dd21ea569bac6655ccf628146 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Thu, 9 Oct 2025 10:07:31 +0530 Subject: [PATCH 38/56] fix remove channel from rtm engine --- .../events/BreakoutRoomEventsConfigure.tsx | 2 -- .../breakout-room/events/constants.ts | 2 -- template/src/rtm-events-api/Events.ts | 6 ++++- .../rtm/RTMConfigureBreakoutRoomProvider.tsx | 23 ++++++++++++++++--- template/src/rtm/RTMGlobalStateProvider.tsx | 2 +- 5 files changed, 26 insertions(+), 9 deletions(-) diff --git a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx index 95fca8c73..0ed265a50 100644 --- a/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx +++ b/template/src/components/breakout-room/events/BreakoutRoomEventsConfigure.tsx @@ -6,8 +6,6 @@ import {BreakoutRoomSyncStateEventPayload} from '../state/types'; import {useLocalUid} from '../../../../agora-rn-uikit'; import {useRoomInfo} from '../../../components/room-info/useRoomInfo'; import {logger, LogSource} from '../../../logger/AppBuilderLogger'; -import {EventNames} from '../../../rtm-events'; -import {useRTMGlobalState} from '../../../rtm/RTMGlobalStateProvider'; interface Props { children: React.ReactNode; diff --git a/template/src/components/breakout-room/events/constants.ts b/template/src/components/breakout-room/events/constants.ts index 3606d5d89..b0714cee1 100644 --- a/template/src/components/breakout-room/events/constants.ts +++ b/template/src/components/breakout-room/events/constants.ts @@ -3,7 +3,6 @@ const BREAKOUT_ROOM_JOIN_DETAILS = 'BREAKOUT_ROOM_BREAKOUT_ROOM_JOIN_DETAILS'; const BREAKOUT_ROOM_SYNC_STATE = 'BREAKOUT_ROOM_BREAKOUT_ROOM_STATE'; const BREAKOUT_ROOM_ANNOUNCEMENT = 'BREAKOUT_ROOM_ANNOUNCEMENT'; const BREAKOUT_ROOM_MAKE_PRESENTER = 'BREAKOUT_ROOM_MAKE_PRESENTER'; -const BREAKOUT_ROOM_ATTENDEE_RAISE_HAND = 'BREAKOUT_ROOM_ATTENDEE_RAISE_HAND'; const BREAKOUT_ROOM_HOST_OPERATION_START = 'BREAKOUT_ROOM_HOST_OPERATION_START'; const BREAKOUT_ROOM_HOST_OPERATION_END = 'BREAKOUT_ROOM_HOST_OPERATION_END'; @@ -12,7 +11,6 @@ const BreakoutRoomEventNames = { BREAKOUT_ROOM_SYNC_STATE, BREAKOUT_ROOM_ANNOUNCEMENT, BREAKOUT_ROOM_MAKE_PRESENTER, - BREAKOUT_ROOM_ATTENDEE_RAISE_HAND, BREAKOUT_ROOM_HOST_OPERATION_START, BREAKOUT_ROOM_HOST_OPERATION_END, }; diff --git a/template/src/rtm-events-api/Events.ts b/template/src/rtm-events-api/Events.ts index ea833be5f..444700e6b 100644 --- a/template/src/rtm-events-api/Events.ts +++ b/template/src/rtm-events-api/Events.ts @@ -11,7 +11,7 @@ */ ('use strict'); -import {type RTMClient} from 'agora-react-native-rtm'; +import {RtmMessageType, type RTMClient} from 'agora-react-native-rtm'; import RTMEngine from '../rtm/RTMEngine'; import { EventUtils, @@ -167,6 +167,8 @@ class Events { } await rtmEngine.publish(toChannelId, text, { channelType: nativeChannelTypeMapping.MESSAGE, // 1 is message + // customType: 'PlainText', + // messageType: RtmMessageType.string, }); } catch (error) { logger.error( @@ -189,6 +191,8 @@ class Events { try { await rtmEngine.publish(`${adjustedUID}`, text, { channelType: nativeChannelTypeMapping.USER, // user + // customType: 'PlainText', + // messageType: RtmMessageType.string, }); } catch (error) { logger.error( diff --git a/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx b/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx index b84b81581..87daf9b24 100644 --- a/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx +++ b/template/src/rtm/RTMConfigureBreakoutRoomProvider.tsx @@ -128,6 +128,7 @@ const RTMConfigureBreakoutRoomProvider = ( const {callActive, currentChannel} = props; const {dispatch} = useContext(DispatchContext); const {defaultContent, activeUids} = useContent(); + console.log('rudra-core-client: activeUids: ', activeUids); const { waitingRoomStatus, data: {isHost}, @@ -232,6 +233,10 @@ const RTMConfigureBreakoutRoomProvider = ( await subscribeChannel(); await getMembersWithAttributes(); await getChannelAttributes(); + const result = await RTMEngine.getInstance().engine.presence.whereNow( + `${localUid}`, + ); + console.log('rudra-core-client: user is now at channels ', result); setHasUserJoinedRTM(true); await runQueuedEvents(); setIsInitialQueueCompleted(true); @@ -247,6 +252,8 @@ const RTMConfigureBreakoutRoomProvider = ( '🚫 RTM already subscribed channel skipping', currentChannel, ); + const channelids = RTMEngine.getInstance().allChannelIds; + console.log('rudra-core-client: alreadt subscribed', channelids); } else { await client.subscribe(currentChannel, { withMessage: true, @@ -291,6 +298,11 @@ const RTMConfigureBreakoutRoomProvider = ( 'API', 'RTM presence.getOnlineUsers(getMembers) start', ); + console.log( + 'rudra-core-client: fetchOnlineMembersWithRetries inside breakout room ', + client, + currentChannel, + ); const {allMembers, totalOccupancy} = await fetchOnlineMembersWithRetries( client, currentChannel, @@ -319,7 +331,7 @@ const RTMConfigureBreakoutRoomProvider = ( }, ); console.log( - 'supriya rtm [breakout] attr backoffAttributes', + `rudra-core-client: getting user attributes for user ${member.userId}`, userAttributes, ); mapUserAttributesToState( @@ -358,7 +370,11 @@ const RTMConfigureBreakoutRoomProvider = ( }, }, ); - + console.log( + 'rudra-core-client: totalOccupancy', + allMembers, + totalOccupancy, + ); logger.debug( LogSource.AgoraSDK, 'Log', @@ -804,7 +820,7 @@ const RTMConfigureBreakoutRoomProvider = ( setHasUserJoinedRTM(false); setIsInitialQueueCompleted(false); currentClient.unsubscribe(channel); - RTMEngine.getInstance().removeChannel(channel); + RTMEngine.getInstance().removeChannel(RTM_ROOMS.BREAKOUT); logger.log(LogSource.AgoraSDK, 'API', 'RTM destroy done'); if (isIOS() || isAndroid()) { EventUtils.clear(); @@ -830,6 +846,7 @@ const RTMConfigureBreakoutRoomProvider = ( logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); } return async () => { + console.log('rudra-core-client: cleaning up for channel', currentChannel); const currentClient = RTMEngine.getInstance().engine; hasInitRef.current = false; isRTMMounted.current = false; diff --git a/template/src/rtm/RTMGlobalStateProvider.tsx b/template/src/rtm/RTMGlobalStateProvider.tsx index dc446cc55..db83d6739 100644 --- a/template/src/rtm/RTMGlobalStateProvider.tsx +++ b/template/src/rtm/RTMGlobalStateProvider.tsx @@ -655,7 +655,7 @@ const RTMGlobalStateProvider: React.FC = ({ unregisterCallbacks(mainChannelName); if (RTMEngine.getInstance().hasChannel(mainChannelName)) { client?.unsubscribe(mainChannelName).catch(() => {}); - RTMEngine.getInstance().removeChannel(mainChannelName); + RTMEngine.getInstance().removeChannel(RTM_ROOMS.MAIN); } } From 786a7fa1c054044d05cfd568f286f57e3582cec7 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Thu, 9 Oct 2025 12:16:50 +0530 Subject: [PATCH 39/56] local user should appear first --- .../ui/BreakoutRoomGroupSettings.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx index d96d82acc..7bdbbad33 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomGroupSettings.tsx @@ -230,10 +230,19 @@ const BreakoutRoomGroupSettings = ({scrollOffset}) => { {room.participants.hosts.length > 0 || room.participants.attendees.length > 0 ? ( <> - {room.participants.hosts.map(member => renderMember(member))} - {room.participants.attendees.map(member => - renderMember(member), - )} + {/* Combine and sort members - local user first */} + {[...room.participants.hosts, ...room.participants.attendees] + .sort((a, b) => { + // Local user always comes first + if (a === localUid) { + return -1; + } + if (b === localUid) { + return 1; + } + return 0; // Keep others in original order + }) + .map(member => renderMember(member))} ) : ( From b433291768407ace7624059d14b1bc66f1a5608a Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Thu, 9 Oct 2025 14:51:10 +0530 Subject: [PATCH 40/56] reset attribute when user lowers hand --- .../raise-hand/RaiseHandProvider.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/template/src/components/raise-hand/RaiseHandProvider.tsx b/template/src/components/raise-hand/RaiseHandProvider.tsx index fa1b060d7..31603fe82 100644 --- a/template/src/components/raise-hand/RaiseHandProvider.tsx +++ b/template/src/components/raise-hand/RaiseHandProvider.tsx @@ -69,9 +69,26 @@ export const RaiseHandProvider: React.FC = ({ } = useRoomInfo(); const {isInBreakoutRoute} = useCurrentRoomInfo(); const {breakoutRoomChannelData} = useBreakoutRoomInfo(); + // Get current user's hand state const isHandRaised = raisedHands[localUid]?.raised || false; + // Detect room changes and lower hand if raised + useEffect(() => { + // Send RTM event to reset attribute + return () => { + events.send( + EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, + JSON.stringify({ + uid: localUid, + raised: false, + timestamp: Date.now(), + }), + PersistanceLevel.Sender, + ); + }; + }, [localUid]); + // Check if any user has hand raised const isUserHandRaised = useCallback( (uid: number): boolean => { @@ -114,7 +131,7 @@ export const RaiseHandProvider: React.FC = ({ type: 'raise_hand', uid: localUid, userName: userName, - roomName: breakoutRoomChannelData?.breakoutRoomName || '', + roomName: breakoutRoomChannelData?.room_name || '', timestamp, }), PersistanceLevel.None, @@ -141,6 +158,7 @@ export const RaiseHandProvider: React.FC = ({ getDisplayName, isInBreakoutRoute, mainChannelId, + breakoutRoomChannelData?.room_name, ]); // Lower hand action From bd0f17fc3b32a28afd378670784ef4b175193bac Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Thu, 9 Oct 2025 15:48:40 +0530 Subject: [PATCH 41/56] raise hand event trigger --- .../context/BreakoutRoomContext.tsx | 6 +++-- .../raise-hand/RaiseHandProvider.tsx | 26 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 8a66f7763..8bbb49bf7 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -2259,7 +2259,7 @@ const BreakoutRoomProvider = ({ if (breakout_room.length === 0 && prevGroups.length > 0) { console.log('supriya-sync-ordering 1. all room closed: '); // 1. User is in breakout toom and the exits - if (prevRoomId) { + if (prevRoomId && isBreakoutMode) { // Don't show toast if the user is the author if (srcuid !== localUid) { showDeduplicatedToast('all-rooms-closed', { @@ -2272,6 +2272,7 @@ const BreakoutRoomProvider = ({ } // Set transition flag - user will remount in main room and need fresh data sessionStorage.setItem('breakout_room_transition', 'true'); + lastSyncedSnapshotRef.current = null; return exitRoom(true); } else { // 2. User is in main room recevies just notification @@ -2288,7 +2289,7 @@ const BreakoutRoomProvider = ({ } // 2. User's room deleted (they were in a room → now not) - if (userLeftBreakoutRoom) { + if (userLeftBreakoutRoom && isBreakoutMode) { console.log('supriya-sync-ordering 2. they were in a room → now not: '); const prevRoom = prevGroups.find(r => r.id === prevRoomId); @@ -2323,6 +2324,7 @@ const BreakoutRoomProvider = ({ // Set transition flag - user will remount in main room and need fresh data sessionStorage.setItem('breakout_room_transition', 'true'); + lastSyncedSnapshotRef.current = null; return exitRoom(true); } diff --git a/template/src/components/raise-hand/RaiseHandProvider.tsx b/template/src/components/raise-hand/RaiseHandProvider.tsx index 31603fe82..2d5bd97e9 100644 --- a/template/src/components/raise-hand/RaiseHandProvider.tsx +++ b/template/src/components/raise-hand/RaiseHandProvider.tsx @@ -75,19 +75,21 @@ export const RaiseHandProvider: React.FC = ({ // Detect room changes and lower hand if raised useEffect(() => { - // Send RTM event to reset attribute + // Send RTM event to reset attribute only if hand is raised return () => { - events.send( - EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, - JSON.stringify({ - uid: localUid, - raised: false, - timestamp: Date.now(), - }), - PersistanceLevel.Sender, - ); + if (isHandRaised) { + events.send( + EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, + JSON.stringify({ + uid: localUid, + raised: false, + timestamp: Date.now(), + }), + PersistanceLevel.Sender, + ); + } }; - }, [localUid]); + }, [localUid, isHandRaised]); // Check if any user has hand raised const isUserHandRaised = useCallback( @@ -149,6 +151,7 @@ export const RaiseHandProvider: React.FC = ({ // Show toast notification Toast.show({ type: 'success', + leadingIconName: 'raise-hand', text1: 'Hand raised', visibilityTime: 2000, }); @@ -219,6 +222,7 @@ export const RaiseHandProvider: React.FC = ({ if (uid !== localUid) { const userName = getDisplayName(uid) || `User ${uid}`; Toast.show({ + leadingIconName: 'lower-hand', type: raised ? 'success' : 'info', text1: raised ? `${userName} raised hand` From 383bad4e6ce64b1775b8b1621eec174c894ed5c4 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Thu, 9 Oct 2025 16:50:33 +0530 Subject: [PATCH 42/56] add raise hand --- template/src/components/raise-hand/RaiseHandProvider.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/template/src/components/raise-hand/RaiseHandProvider.tsx b/template/src/components/raise-hand/RaiseHandProvider.tsx index 2d5bd97e9..62279da39 100644 --- a/template/src/components/raise-hand/RaiseHandProvider.tsx +++ b/template/src/components/raise-hand/RaiseHandProvider.tsx @@ -247,6 +247,7 @@ export const RaiseHandProvider: React.FC = ({ if (type === 'raise_hand') { Toast.show({ type: 'info', + leadingIconName: 'raise-hand', text1: `${userName} raised hand in ${roomName}`, visibilityTime: 4000, }); From 7d992347301fde8fb375bde3b5b269de2ace7d00 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Thu, 9 Oct 2025 16:53:16 +0530 Subject: [PATCH 43/56] fix the icon --- template/src/components/raise-hand/RaiseHandProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/template/src/components/raise-hand/RaiseHandProvider.tsx b/template/src/components/raise-hand/RaiseHandProvider.tsx index 62279da39..2fd4cbc89 100644 --- a/template/src/components/raise-hand/RaiseHandProvider.tsx +++ b/template/src/components/raise-hand/RaiseHandProvider.tsx @@ -222,8 +222,8 @@ export const RaiseHandProvider: React.FC = ({ if (uid !== localUid) { const userName = getDisplayName(uid) || `User ${uid}`; Toast.show({ - leadingIconName: 'lower-hand', - type: raised ? 'success' : 'info', + leadingIconName: raised ? 'raise-hand' : 'lower-hand', + type: 'info', text1: raised ? `${userName} raised hand` : `${userName} lowered hand`, From 8cb072ef1b96c59a6f5ec0a13f688a40dba439dc Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Thu, 9 Oct 2025 17:07:35 +0530 Subject: [PATCH 44/56] remove unused code from usermutetogglelocal --- .../UserGlobalPreferenceProvider.tsx | 3 ++- template/src/utils/useMuteToggleLocal.ts | 18 ------------------ 2 files changed, 2 insertions(+), 19 deletions(-) diff --git a/template/src/components/UserGlobalPreferenceProvider.tsx b/template/src/components/UserGlobalPreferenceProvider.tsx index 3944c794c..e3d7f281d 100644 --- a/template/src/components/UserGlobalPreferenceProvider.tsx +++ b/template/src/components/UserGlobalPreferenceProvider.tsx @@ -21,6 +21,7 @@ import { ToggleState, PermissionState, DefaultContentInterface, + ContentInterface, } from '../../agora-rn-uikit'; import {MUTE_LOCAL_TYPE} from '../utils/useMuteToggleLocal'; @@ -48,7 +49,7 @@ interface UserGlobalPreferenceInterface { userGlobalPreferences: UserGlobalPreferences; syncUserPreferences: (prefs: Partial) => void; applyUserPreferences: ( - currentUserData: DefaultContentInterface, + currentUserData: ContentInterface, toggleMuteFn: (type: number, action?: number) => Promise, ) => Promise; } diff --git a/template/src/utils/useMuteToggleLocal.ts b/template/src/utils/useMuteToggleLocal.ts index de8248ba1..46267adcb 100644 --- a/template/src/utils/useMuteToggleLocal.ts +++ b/template/src/utils/useMuteToggleLocal.ts @@ -96,15 +96,6 @@ function useMuteToggleLocal() { value: [newAudioState], }); - // Sync audio preference to RTM (only saves in main room) - try { - syncUserPreferences({ - audioMuted: newAudioState === ToggleState.disabled, - }); - } catch (error) { - console.warn('Failed to sync audio preference:', error); - } - handleQueue(); } catch (e) { dispatch({ @@ -173,15 +164,6 @@ function useMuteToggleLocal() { value: [newVideoState], }); - // Sync video preference to RTM (only saves in main room) - try { - syncUserPreferences({ - videoMuted: newVideoState === ToggleState.disabled, - }); - } catch (error) { - console.warn('Failed to sync video preference:', error); - } - handleQueue(); } catch (e) { dispatch({ From cc4b65890dd4b3995ae2a0885aa154b3a3751575 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Fri, 10 Oct 2025 11:19:49 +0530 Subject: [PATCH 45/56] disable waiting room for now --- .../src/components/controls/useControlPermissionMatrix.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/template/src/components/controls/useControlPermissionMatrix.tsx b/template/src/components/controls/useControlPermissionMatrix.tsx index 1420a2500..eada87fa9 100644 --- a/template/src/components/controls/useControlPermissionMatrix.tsx +++ b/template/src/components/controls/useControlPermissionMatrix.tsx @@ -66,7 +66,8 @@ export const controlPermissionMatrix: Record< ENABLE_AUTH && !$config.ENABLE_CONVERSATIONAL_AI && !$config.EVENT_MODE && - !$config.RAISE_HAND, + !$config.RAISE_HAND && + !$config.ENABLE_WAITING_ROOM, }; export const useControlPermissionMatrix = ( From 0e1fb9a3e09fe48f8447cc899620a89e62c414f6 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Fri, 10 Oct 2025 19:53:54 +0530 Subject: [PATCH 46/56] reset attributes on unmount --- template/src/rtm/RTMEngine.ts | 17 ++++++++++++----- template/src/rtm/constants.ts | 2 -- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/template/src/rtm/RTMEngine.ts b/template/src/rtm/RTMEngine.ts index 5f0a936c4..a63d641b2 100644 --- a/template/src/rtm/RTMEngine.ts +++ b/template/src/rtm/RTMEngine.ts @@ -187,25 +187,32 @@ class RTMEngine { } console.log('supriya-rtm-lifecycle unsubscribing from all channel'); - // Unsubscribe from all tracked channels + // 1. Unsubscribe from all tracked channels for (const channel of this.allChannelIds) { + console.log('supriya-stt channel: ', channel); try { await this._engine.unsubscribe(channel); } catch (err) { console.warn(`Failed to unsubscribe from '${channel}':`, err); } } + // 2. Remove user metadata + try { + console.log('supriya-rtm-lifecycle removing user metadata'); + await this._engine?.storage.removeUserMetadata(); + } catch (err) { + console.warn('Failed to remove user metadata:', err); + } - // 2. Remove all listeners if supported + // 3. Remove all listeners try { console.log('supriya-rtm-lifecycle remove all listeners '); - - await this._engine.removeAllListeners?.(); + this._engine.removeAllListeners?.(); } catch { console.warn('Failed to remove listeners:'); } - // 3. Logout and release resources + // 4. Logout and release resources try { await this._engine.logout(); console.log('supriya-rtm-lifecycle logged out '); diff --git a/template/src/rtm/constants.ts b/template/src/rtm/constants.ts index d442e6060..babee888e 100644 --- a/template/src/rtm/constants.ts +++ b/template/src/rtm/constants.ts @@ -9,6 +9,4 @@ export enum RTM_ROOMS { export const RTM_EVENTS_ATTRIBUTES_TO_RESET_WHEN_ROOM_CHANGES = [ EventNames.RAISED_ATTRIBUTE, // (livestream) EventNames.BREAKOUT_RAISE_HAND_ATTRIBUTE, // Breakout room raise hand ( will be made into independent) - EventNames.STT_ACTIVE, - EventNames.STT_LANGUAGE, ] as const; From c217d6e282c2d893c327ac27b127af828a4d9ebb Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Fri, 10 Oct 2025 19:59:45 +0530 Subject: [PATCH 47/56] clean up whiteboard --- .../whiteboard/WhiteboardConfigure.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/template/src/components/whiteboard/WhiteboardConfigure.tsx b/template/src/components/whiteboard/WhiteboardConfigure.tsx index 229c7525a..3a564e0fc 100644 --- a/template/src/components/whiteboard/WhiteboardConfigure.tsx +++ b/template/src/components/whiteboard/WhiteboardConfigure.tsx @@ -444,6 +444,33 @@ const WhiteboardConfigure: React.FC = props => { } }, [whiteboardActive]); + // Cleanup whiteboard on unmount + useEffect(() => { + return () => { + if ( + whiteboardRoom.current && + Object.keys(whiteboardRoom.current)?.length + ) { + try { + whiteboardRoom.current?.disconnect(); + whiteboardRoom.current?.bindHtmlElement(null); + logger.log( + LogSource.Internals, + 'WHITEBOARD', + 'Whiteboard disconnected on unmount', + ); + } catch (err) { + logger.error( + LogSource.Internals, + 'WHITEBOARD', + 'Error disconnecting whiteboard on unmount', + err, + ); + } + } + }; + }, []); + return ( Date: Mon, 13 Oct 2025 11:08:04 +0530 Subject: [PATCH 48/56] new project config.json --- config.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config.json b/config.json index 8f295ea69..f6f2be54c 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,5 @@ { - "APP_ID": "aae40f7b5ab348f2a27e992c9f3e13a7", + "APP_ID": "a32ad7a7333e40dbaccdeff5d543ef56", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, "ENABLE_APPLE_OAUTH": false, @@ -9,7 +9,7 @@ "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROJECT_ID": "8e5a647465fb0a5e9006", + "PROJECT_ID": "2b8279fa91bda33fcf84", "RECORDING_MODE": "MIX", "APP_CERTIFICATE": "", "CUSTOMER_ID": "", @@ -32,18 +32,18 @@ "EVENT_MODE": false, "RAISE_HAND": false, "AUDIO_ROOM": false, - "PRODUCT_ID": "breakoutroomfeature", - "APP_NAME": "BreakoutRoomFeature", + "PRODUCT_ID": "breakoutroomfeaturetesting", + "APP_NAME": "BreakoutRoomFeatureTesting", "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", "ICON": "", "PRIMARY_COLOR": "#00AEFC", - "FRONTEND_ENDPOINT": "", + "FRONTEND_ENDPOINT": "2b8279fa91bda33fcf84-7y25qrqsf-agoraio.vercel.app", "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", "PSTN": false, "PRECALL": true, "CHAT": true, "CHAT_ORG_NAME": "61394961", - "CHAT_APP_NAME": "1573238", + "CHAT_APP_NAME": "1610292", "CHAT_URL": "https://a61.chat.agora.io", "CLOUD_RECORDING": true, "SCREEN_SHARING": true, @@ -67,7 +67,7 @@ "FONT_COLOR": "#FFFFFF", "BACKGROUND_COLOR": "#111111", "VIDEO_AUDIO_TILE_COLOR": "#222222", - "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#000004", + "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#00000040", "VIDEO_AUDIO_TILE_TEXT_COLOR": "#FFFFFF", "VIDEO_AUDIO_TILE_AVATAR_COLOR": "#BDD0DB", "SEMANTIC_ERROR": "#FF414D", @@ -85,12 +85,12 @@ "ICON_BG_COLOR": "#242529", "TOOLBAR_COLOR": "#111111", "ACTIVE_SPEAKER": true, - "WHITEBOARD_APPIDENTIFIER": "", + "WHITEBOARD_APPIDENTIFIER": "WUjVACgwEe2QlOX96Oc4TA/DXlhL5JAksoOSQ", "WHITEBOARD_REGION": "us-sv", - "ENABLE_NOISE_CANCELLATION": false, + "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, - "ENABLE_WHITEBOARD": false, - "ENABLE_WHITEBOARD_FILE_UPLOAD": false, + "ENABLE_WHITEBOARD": true, + "ENABLE_WHITEBOARD_FILE_UPLOAD": true, "ENABLE_CHAT_NOTIFICATION": true, "ENABLE_CHAT_OPTION": true, "ENABLE_WAITING_ROOM": false, From 6e3f87e91d766c7a7554846559dc00f1ae14d0fa Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 13 Oct 2025 11:40:37 +0530 Subject: [PATCH 49/56] add base dark config files --- audio-livecast.config.json | 104 ++++++++++++++++++++++--------------- live-streaming.config.json | 104 ++++++++++++++++++++++--------------- voice-chat.config.json | 104 ++++++++++++++++++++++--------------- 3 files changed, 189 insertions(+), 123 deletions(-) diff --git a/audio-livecast.config.json b/audio-livecast.config.json index bfdc42871..02ec4c190 100644 --- a/audio-livecast.config.json +++ b/audio-livecast.config.json @@ -1,42 +1,70 @@ { - "PRODUCT_ID": "helloworld", - "APP_NAME": "HelloWorld", - "LOGO": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKIAAAA5CAYAAAC1U/CbAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABFwSURBVHgB7Z0NnFTVdcDPue/NzJuFBSV+EBEFAvxsCjZVUxXUfGgNajWkiRoTg4GdXVBQSLStpmUFTWt+KthE4rK7s7uuJmqoaaOm1aZNFaI0mlhav1JTMVsMMcSA4sLMezPv3ZNzZ0H36933MW83+Nv5/37+lpl75r77cd6955577hUhBuNb6CgvC7PA9Y4hgJQAKEgydgsLXil8AV8Pm8+4LppMrndqodF8CKpk0jdogpMuT5cpMZMI6oAAoQos03j4rcX4FlTBuPZ9R0svMwNSOJXLlOEC2QS0B8rmdvtK7IFqaN0zMYP1p6Ew/9fO4f+H/t0assZPLs3wTOPEYs58AGJS10rvl+jORsTJXDdTGNBb4rq5wnwBIrSb1UrHUcr9g9CdZbWXPwooPsv/XMj/He0vib8ApM0A8h67IfX4cBLpDvdTQoqVLPcR/mjbOSMLMchspFloykWscxci0Inq4ZAQHooPlRvwfyAiVr58FkpYSEJ8hj9O1Yju4pd4iyB3U7Ex8yBEfQ53IBjygALi04TY7DTgD3zlO7mtPe8qErgACSao77jdw7fXespmJsJ8lN4V/LwF3NJH+MoS/JzTt3jotZYb0j8dtjx5N8f1X4aAJ7NsT2BBrHZ3MXf09YgwG6LTA5LW2k3m3ZVPm8jI7PU28luU6ycTWRHNtvI8fgX/lmusFDkx5XsXfM3OieOi/CKTLy8Awq9y3U6G6PSw4t7h7IANsIbVOAQDFbEP7tgNzgSxCi5B7+B36TzNEUDf5NSzBucRShEfJzPzilyJSDdwu7wPIsIzwLNCwLriEvN+9XnSt2jC/gLdh4IueEeIFdH0y2BCniaVyNsAiJdV0dPTQGCX1eGdbpbEavdtuRwGKmFkMp30JZByLVexHkYM+cOwkqqdHHS/giS+zA0at6mmoZRft6bQn0FLscm+MtsDMeCHr7B6YbfNE7D6nMkTzxReVxwFUtR10DHeduriQejcuO+7GvFIwn2ZvHeSUxB/s7/grkEhLhgqNwzZPM0nkN8F7RQcmV0++YUaEVWjSKD7gCqj4EjyWyiXPhxGGbijZyLIfwP1wiUFwmus1NcWc/gPOrHhRsR3kdxGJo+A8mZdHroRMdvhXiIJ21hgIiQEj9hvcX6HDUngEVEM/q4u714gyVMjQpJKCNXkpxpdEv1oFJSQeMq4JpQSttFsVsInIUklrJQApvIgsKky8sfOQjwUpIQ6ePS6iggfSFIJFcMq4QEGKKLVQtM8wk1s52TgkIEQDI9HB5oBIwiv/N5EFBcftGV0ZFtoCorKSJj0y/oOPFWvt/LFs2L8VNvhQaS7nBP49xtgRGxvf95VxFZKQYp+yPZAHRxCWHn1ZuOfwAiDAtNc918EChIJSknlDYi0mIlXqPS36lvfPgJGCTUQCc98FEZZCRXvKKJleq0hR53dRPI2cuV5WBbH1lmCh28xjb87l4u/kTsqvE8rAF6xX85//jpIjkezAo/iD/L0/TkwStPrCmJipiQmeeSdznbJ1SzxUwjOZJz05COHt5J2OrI6YTX/mQVB2fXZQ3fwqvEi1U7jUUww9hUn81B3JiffAsqjEJzJ1LIx7uuQHHuAJHsb5DkiI44ZkpqmjRDC1OC6qT6+CYQ8W6CYonSAbZoPsElwHrdjN6fthohUND/dRXOFJ58LFCa57rCjzebXL8KCr1DrTi7X5Os559UQDt/FCq+2t3OtZwQUaiuWjEuKV+JOnRi7MS5mm6490O4haLYbjeHtq06aannyZa5bVl8kuDNjiuYgh3g2763gTr0TAuAOvsjJ4SP9v9MvVoYpE8j14503b3xj+VH7hkvvW2HLhwMzknCT/StxM7uZXD8R5ch30WrmFfNVEK5wfYsVIeWXg2TZz7Wy2Ji6TquEiqVTCtyRzfyvr0AVpDvdzwQqIUC3LYyzg5RQUVKrUBQnsftI33kIq6CLrOGSLA8+H6SEnH5DsdG4JsyuTDFnbBCSLuR6FvRZUmD/+MGzRRklXVrMpa71U8LKM4iWafNRIzznYzcZN+qUULG/cfwuJ2eyq67PjRQGAbxdx0/5vE6IK/MlZwl+AyLAo9wtXLvFEBPeefmiVoCgxW4Qi3k7yYaQ8M7Dq+CiWgDotiEnpV3v/GFTUC7V/I5997LZbjC+BhEoNJnfZ0VboornL0UfTW90PgjxWFlsMjfpBKx2ms59db5GhKQU5wblMxhui7XcT2vDyIp0yjuH/6Z8JRC6nEbj7yEGdoN5NxFF3r6Cu+hw7lZdw/SkHLGaRzeCqGVaijt4FNK+/ULgpYO/y3bQ6aCznxCfLjWmYrlMio3md7irb9XJiLT5KYhON/ddS5AQGXChNp3k7eUm/AnEgGfHNdw4TwTJcZvDOToB8kSkN3zIA8BYBxHJmO7Jqmf90jmhpXcFRjaID8Kj0MPsJ9vuKyBhiNtESnceaCBP/h1UgS3NdfxW7ffNX6L2+cPBo9htYeTQ8z6hSSYBZqTZcJgnfDVIgrch4QT/ZHrGacKfQxUUG/HH7KANXAgNKJQQ2v1aiSL6KDsIFN69/okwWRnc/b/i1eGp4M8up8l4BKphKe4V5B+0wK/lSRABVuptpSZ8MZSswBP886F/4T78JVSB/UtQ7i7twMGLFZztX0DaAgkgAJ+MIs+rxOP90+DViq1XLQg/0iWXIH3swOdWpmafMtG2ylq5SiS4/nvcRJPhHhoHIeEVcLgXYw2ZXPLpfskCcTNUyxqUbAf/u05ErZr9vfAevQYJIDGa4vAI6uvE5Qol46d0TG2ZBIiBuyboHzggkKqaNQ7Cnolf6dKz+8PvmJA0Q9l09e/vVXn6mkGeFNshAaQkbd1ExQTwAw0HkoANqCji/Ib6jy4IHiQA1oMb6QdS004SItXPD/RA7wEwI+x4SDdU/aSoN2AUYNNCq0uCAHs1P54MSRSC8KhIPwDSGe3HQhLsc3VBq1x3Y6DPDcXbfrK8qxArzGrIMw393rUloRcSZn/Jf4GkQCwfCUmAOEWXzItmucsvkVeWyezxCjwlkjyQr3HML8eMoG24MEgD5+rSyYSBTnKSvoY/D9/zIQkknqFJLbzJCxpImuXILxy+4ZeMYPwxJAAPeCfq0tkWNfzD4RHmqbMgUA0tveww1zbwUAxTZ5ekC+idCVXCq+DzfBMJHKdnYAAE+0Of8RPn+XJGppviRLAPzujj/on0PIwY0r+9CRbygkZAFVhdNI3b6I90Muzbpaf9Ennb57BCVl4N1RTCrGtUf6L8Bh3QrrBQwF9CFWTuKs5gM3Sh/wNo65CQfYF6D4ILseMHFVaHu4j/+K5eed/2WRghSMV6+j/46OwU71KoAt6WuSFIRqRlWbvRzT6m6ypbQDFQYUU8bV0PEenbO9ZFzOCZmby3EuLA9oZIpf9JK4MwJCbRwTe3cF187UQkuUwdnIIYqHbiWUO7Fca7G9+FEQIN85916YS4Xp3agxjUddJJvPhsCpITvU0Wux7oP/0E1KjIQ/fj2c7CVIiA1U3HgUmb2aYbDzGQhNqG56H+tnS7++cQkUyH10YIWnsF3NK/Dvmu4chenp8e1f4OxP1Wno6HCBzWxe2bpqDjBj12Y+o/YISwl+BmtuF0TuvJZNAj9d0UaVFmtRene1KGKnff3C/hm1ophONJZrao8HgIQaqdToOyt4W1OHbwaKksWrlxdMZ5ig3cTaxYfwEhqL+Pjsjk3Yd5igs4vEX32kvrdgybQkLfTgDHsNTWrIocCoE6DmtL+RRnPFMnxy9d0HOrhv2zrbp0NmVOLpfl01ZLcRqEINNWPgcw/VTY4wZ9fik2RjNT5E9CbSMh3M3eyW47Z24eEHTQSilLuPP5O7YJ8TKA0D4v/3jEvKfCycJEb7zAfrOvpTLpx3qvGLgHPb6F5rhp+Wm2g1axEuodwsSWnluapTuzksnL73GnfRIC4OnsQbYr25xe2ALX4Ls+NG5r61j3DBZYxA7sy3jGqQso0yt2lrc8L8cBZkFQPKIKXHaWpR6DsHRQvUXeS9xtQe4x5cft9ki0l3n7dkCKOvtcD7ww9VYh4vkQlv7nmit2Skr+DEIuLNQGPXesWsnZ/OF9hJWVUZwjnv6n+LhiVj2xwtOHISyIL3F5dvNLlSVSB9wp/LkSFcYVEEFTOWIp5csRTI4yd8rzXBZuL5gggY6PcqbEtN05+1ZkhriOAgNjDfExe3Fw1Et/xrWVP+EJEVp5WQf2ct1U2cr835EkWQfiHDUZfMA+2+6tYvvpDhhdtMdJMx00A8l7Ju7Z3LCoEcxpEBeHkbXy7he4PCokfmTPdpC8mV+M5uGSgkdEMdtZhv8HEeFZSM1AzTCaDD5OWlRxh1WGMyWNCnBwhXER/7MAI8eLWaFMinCwWXIvv/nXwcjS7qeEIXg1jhIqeFC4kUfvNhhlhjgq7aUpdVjpJjiEcJfgVhelipv8NSSNpCdTKfGRqBcuFRuM9expvLYSv54w6jin3SCWQkxIQlXxg06jWEYgbodRZFiPuXorUNDnlA0ACaDOOwgQy7mJX4aYmAVXKWHoYwFhIQNf7l0EeyAGxSZjPYHxyaROLnI7OazW1xVzxtVxos8P0OOURQdUBZKTQ+WNuBEgmSATrtxvePNEbSIM65Hw3bpRB82xLD7EjRx44Fz/fHxIkJhbyOFdbLYvgDDHKAdh3VmcDtnME5D0rQpQifRpsDrl3crRDTFQp+t4P/YM/nV10xnREzwKneI0Ro9o78cOdtef3bd/XD08IN3Ei56Z/FL8AOLC7gpWgg6zKOaUGs2HyBZ/CsMoY6jGT+edOQLNVZyhCikPjn7BSjRuOyvgJlbAbf2Tsp00lXcJ1nIBv3jgOEDg3TcV53hJ+SUxkrM4Cjz83O7kjFA+ST8qO0kpdzlvRy3kms0M8cy3BMG3IS3uKV6Bz0AE+i9WqG+m2GA6Yt3+5Zi8+QKVfptHnsc7JLxHjxQmmup11oM8mqKV6zYggKTioUl7t/AK8bOVL8JcSzeYVHvpNBCpEwzyVMSJcnBnePPXYd/a81LScwbIZws7U9uCrldTOxDkwQIwvHOdnPnpoOdmumgWuPK/gtwm3Ckuy+zl9zAd3Z1Ei9RCBBJAvbzsQzmRXVynsIvjD9kYHc8bB8rN8RIPvs+ZKe/H++emXoRTsAwxOKCIt3I9N2dNcX+1l4qGhgtvtbnzKWXORSlP5c8zWJHUFTUFJPxvHv5ekBlja2kR/iwoK+XQBwMuQPQ+OLLuh4QxO8pnGKSmwiGOabbxZCsK+H5xn/nsQQeyutm2ZHhnCoHKNrk8KH91/006LWYNdorXGHneU4qosDrKH+OpTzld0+qzulXBkmJ1UKxeppU+gEbl4iRtAAePqLfyFP1XUGNUec8pooIdyldw0bukgNWlJerm2JDk906ycNxjbJvodmr2WYaYOmpTXY0KVQU8/r5gO66bR8WPR1JCRW7iHts0VECs7uDUeNv1Am3WGsnynlREhd0YbR/1HZT9R7JBJ8IukDi3KtSogvesIlYDb509ob8Gg+bF9SvWiMeYVEQF+1l9o7RZAw/P5mEK1Bg1xqwiCkNoL+90oZzMsdUaoRi7I2IZtPe5iITOKtcIx5hVRPZC6uvuUjKb/TVCMWYVET1XawOiYWpvQKiRLGNWEaUU+vM5aO+AGqPG2J2aBehO871h57LJ3DpWIxQmjEEOHIT3PdtMI3irQo3hGXMj4sRvq/u5RbdOBqX7HagxqowpRVRHQZ0iPQX6SO89dsmu7hriGpEZM1OzivKWZakuUgqK8v5HWDGhFo84yoyZEdHOwk5CDLjaDd+AcilaRE+NRBg7U/Ml6DmA6mTiNj8RBLwm7v+0u0Z1jK3FSgP2OoZQkTVbh6QRrC3m8AGo8Xth7PkR1f8yLW1c1v8sMrtrvmfvFIfUpQJjjTEbc1e5U0d6TwLio7YnlsHSeKfpatSomsolmTUOCX4Hn8i63ZAyQc4AAAAASUVORK5CYII=", - "ICON": "logoSquare.png", - "APP_ID": "a569f8fb0309417780b793786b534a86", - "PROJECT_ID": "49c705c1c9efb71000d7", - "FRONTEND_ENDPOINT": "https://app-builder-core-audio-livecast-git-preprod-agoraio.vercel.app", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "MIX", - "SCREEN_SHARING": false, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform", + "APP_ID": "a32ad7a7333e40dbaccdeff5d543ef56", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, + "ENABLE_APPLE_OAUTH": false, "ENABLE_SLACK_OAUTH": false, "ENABLE_MICROSOFT_OAUTH": false, - "ENABLE_APPLE_OAUTH": false, "GOOGLE_CLIENT_ID": "", "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROFILE": "720p_3", - "SCREEN_SHARE_PROFILE": "1080p_2", + "PROJECT_ID": "2b8279fa91bda33fcf84", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", + "BUCKET_NAME": "", + "BUCKET_ACCESS_KEY": "", + "BUCKET_ACCESS_SECRET": "", + "GOOGLE_CLIENT_SECRET": "", + "MICROSOFT_CLIENT_SECRET": "", + "SLACK_CLIENT_SECRET": "", + "APPLE_PRIVATE_KEY": "", + "APPLE_KEY_ID": "", + "APPLE_TEAM_ID": "", + "PSTN_EMAIL": "", + "PSTN_ACCOUNT": "", + "PSTN_PASSWORD": "", + "RECORDING_REGION": 3, + "GEO_FENCING": true, + "LOG_ENABLED": true, "EVENT_MODE": true, "RAISE_HAND": true, - "LOG_ENABLED": true, - "GEO_FENCING": true, - "GEO_FENCING_INCLUDE_AREA": "GLOBAL", - "GEO_FENCING_EXCLUDE_AREA": "CHINA", "AUDIO_ROOM": true, + "PRODUCT_ID": "breakoutroomfeaturetesting", + "APP_NAME": "BreakoutRoomFeatureTesting", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "2b8279fa91bda33fcf84-7y25qrqsf-agoraio.vercel.app", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1610292", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "BG": "", + "PRIMARY_FONT_COLOR": "#363636", + "SECONDARY_FONT_COLOR": "#FFFFFF", + "SENTRY_DSN": "", + "PROFILE": "480p_8", + "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, + "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", "SECONDARY_ACTION_COLOR": "#FFFFFF", "FONT_COLOR": "#FFFFFF", - "BG": "", "BACKGROUND_COLOR": "#111111", "VIDEO_AUDIO_TILE_COLOR": "#222222", "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#00000040", @@ -54,30 +82,24 @@ "CARD_LAYER_4_COLOR": "#333333", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_TEXT": true, "ICON_BG_COLOR": "#242529", "TOOLBAR_COLOR": "#111111", "ACTIVE_SPEAKER": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_IDP_AUTH": false, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, + "WHITEBOARD_APPIDENTIFIER": "WUjVACgwEe2QlOX96Oc4TA/DXlhL5JAksoOSQ", + "WHITEBOARD_REGION": "us-sv", "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, "ENABLE_WHITEBOARD": true, - "ENABLE_WHITEBOARD_FILE_UPLOAD": false, - "ENABLE_WAITING_ROOM": true, - "WHITEBOARD_APPIDENTIFIER": "EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "41754367", - "CHAT_APP_NAME": "1042822", - "CHAT_URL": "https://a41.chat.agora.io", - "ENABLE_NOISE_CANCELLATION_BY_DEFAULT": true, - "DISABLE_LANDSCAPE_MODE": false, + "ENABLE_WHITEBOARD_FILE_UPLOAD": true, + "ENABLE_CHAT_NOTIFICATION": true, + "ENABLE_CHAT_OPTION": true, + "ENABLE_WAITING_ROOM": false, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false, - "ENABLE_TEXT_TRACKS": false + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, + "SDK_CODEC": "vp8", + "ENABLE_BREAKOUT_ROOM": false } diff --git a/live-streaming.config.json b/live-streaming.config.json index fcb611b9a..538c7beb5 100644 --- a/live-streaming.config.json +++ b/live-streaming.config.json @@ -1,42 +1,70 @@ { - "PRODUCT_ID": "helloworld", - "APP_NAME": "HelloWorld", - "LOGO": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAR0AAABiCAYAAABtaQfbAAAACXBIWXMAAAsSAAALEgHS3X78AAAPH0lEQVR4nO2d3VFbSROGZ1V7xQ3KABwBcgSICCxHsEIJrByBIQKbBDBEsCICiwgsRbCQgXWjS7w1/t7DN4gjnXP6Z35EP1WUt2pBM+f0zDs9o+nuP379+uUMwzBi8ad2OwfXT0PnXPXTd86d7Pj1e+fcT+fcwjk3X096c2bbx865EX4GzrnD9aT3B+czCX0Y4Nn9v74/pzHbr+FyPeldxGjo4PqpHzx7G/svnXMPUvbv2Nc5bLNE+7P1pDcT+uxR8B4q+6vaAWO/arP6Odzy6ys88yJ49w/M9n17Y/TB2/x+Pen5/3bing4GWjXRPzA/zr+MWdcBgBfuDfrX5v+LITrBC/fv4Ei7vY58lJpMdSjY34vBjWaf3UvRCXn042g96d0QPq8ag6Mtk11cdNBmNe52iXsbHjH3vnYRIDgZX2valxed4IGnOxSVg38JN3gJP3f0Y4yHru2Dpuig7amAwTV5x13F6oD9pxgDyexPZYvoVHgPfNSm3V0L3gZiooOJfqHoRd9DfLd6nlhsbnYsNM+i0+P2xjd2cP3kH/hf59xnpQHn4DH4z384uH6abumLf+hvin2oxRv94PrpAW3nLDiP0oKzYf+/I9k/yvYw4BTtDnb9UvAemgRHBN8fiOV35W27/+zvvi0I3AsgOPO2ni1LdLBXfcBgiIUf1F8Orp8W4SCA4EQxdtCmn3AzGD23bVQdnbcJu0ho/89e5OsmgHK7c3gyL8DkX8R6Dxh33pv/EfmMsBKfrxCaUHBaL7akg2Q09DX2JN/AP+SPg+unSwz82IIzwiSO6lUxWMFmbDKx/xEmQLSDcdh6hkPZ38QeB1hoZ4kXOe/RjvDsdec3O+ns6UDp54kHXMhnbGuiATf6n4IEx0mdheRof7j9/UjtnVTbO5zhRRsHaO9HJl71EdXT6iQ6UNlF5ucWqmAbF3M7IcFSwsvJ2P6n2PrEEp4pzhWjLXax29OktehgwM0LW91FSXFuJIDfVo25Xk4B9j+JKDy/zxUjtPMbnN9Ea0+bVqJjglO04AzXk96C8yEF2T+m8EQBW6q/9+V5XBvRCb5/f8uCMy1QcPyWaiAgOKXZ/wQHrcWDg9q92FKFtPF0bt74Gc6wUNe2j5ASLiXa/zTBXR5RcGAvesUhF3aKDlZ47lX2YglW+RI54va9cPt/brrMlzl7u7vYek8HE05ytVjC7fVnA4vwYBOqXgUF5hSvdCHYl+c4Mn+vaHPbA4+qegdSE/2D/1xK4GRk+/fx7CNh+9+Ed2oSc4fnXzRteSH2Upf+qvi1aty9GAsYd8fB3FMXuq2xV4IHp7eI2+gaNKYSS9I29gqr5A+BJjsHDWISToXi2Hzow6tbtC36sC/2/7Se9BqvCzTEXlGpLmS2viMF2z8I2H2F8TPr2PZIeLGt2B3wCc/jX2YjS3xVSz7IxOCbSapvB9HhDsIVJhv5fkyLILq2nHcUvZzsf8OcAN4Ox00TT0F0rmD/TudqOIvi3gMjtR30oVr0JO+jNQZ8ct3q2/Wkx/7mBK7gMaJcowEvhzMAH/FVNetCnh8060nPrzznzGfvas+c7D/A1oTKISZQLFYQ+SlBcPrMvvq2zyhth2Dc+TFwhs8U5ZXoYJXjuNX+hY+lOokXMISbHguO4UW+qg6Bl/KeMQCOcN+jkUztP2LaX6w/DVT3oqgH+JztdNW2WOIzfNZQWnjqPJ0R4/OuGC98JxjI6h4PVhvqpKsML57vBSLGmTxt/5Zj/1tl+1M9niPcedGGexGTal+RS6B14DNFhadOdKirvN+zabuxIw13r6YNKiqCU4HseZfEPz+tS8tQA3Xg30t6OFsYY+tKQVt0PjHPrwaMs6uphuBU4LPF5vYL0cGgpDz4KoYLiwmt3Q51cF5qGr4Ce+0l8c935p+B/akXAXO3v6bo3HPP75hir36XDG1wztae2fR0qIbplEeVA1Z7zW0WJTGUWK6allBXnSb7UpNiXUa0/5xo/0PFpF8S95mofYt1XuWkvJ1N0aE+eMwJ54QvrT0DF5dykKeSt3cbmHgUb6fJvvtufw3RuRc6vKV4mLexxN79b9w9SHyhsyk6nS+R4cGjTTj3/0lH3dvvgnp7NUWoBGWiHzZEYFOe/64g+2vcTpbIU0QVwxSBrew2N0WHorbRahNtoPHCKaK7jLnaBFDf+66JR7F/qohuSrviKS+ESuNQxt1KuyxPHWiT9WXOs+i0/GajjlSio9EuZcVJ8vwQOspqX2tnRg6akuwvHeYgdbZImXvqX1potR16OiTRSbTKO8Sn5EDKflDa3mZn0tYjof1TTroc+pBK7Nltc+teRQ1PCInx9XRL3urAc4ntn8OiI3WWFbOUTnLYxfYMwzC6YKJjGEZUuKKTLEES4+BbmpT9SJ2gKmZ1yRfsU/L1TM6nohGKDmV/2nTvQxMTHVrb2+xMOiNJaP+SU5FuQpl7KZ+f1faz6DAOZlMdgmm0SzmYjRG9/ApMdsq9mlo7Mw5mS7I/NWZNG4ropDx8lhEdQLn3kWTSKbVLMf5JotWe+vy7xGXf7Z/LNYtNKAv+YYrE88xo+N9sig7l4UexJx0eXKMsCtXbixl0x2lz1eDR7Lv9cz07ofYrZkZEsTY3RYeyvYidDtJptccI3Iv6/IjVoRziNj3fvts/9b2mWhC7RvIyYwp+kLidhYToOBSUj/LwmHCa1TYpOUOOIhd3o7alITousv0HVPtLpvJUgBJHdRg5wv9CokjCC9HBYTJFcQ8jRlprv2TOxFPfYzNrIu0c2IXYn9qOSAIqRajj7i/FPEHPoA2Rmup193Sokasf2ib/poJaTNolbqmD+vfE01zxIWpUL6dtNDzH/qrbLKb9s65vzozenmneW8OYFnt/daLD8SS+aQmPYPG3nWB/TV0V/YSYawgPBGfOcG/b2pVj/y+Z2n8VI6WnAJwFb6Y07vrMcfeKV6KD1ZATyCcuPLEEJ4Az8cSFB64tx/CrtisV7M/ZinyT9ngE7B87syEViXEntsUPFjrR3YVWsT0/8NjK613Gg+unRWTB4eThrfBGepAoe4ID6u/MlaZrOlXuJP2Skf1j568mI5AOtBIetujjM8QFx20THYFJ51AK10+8i66DD4PtK0rbap/hbIMrvF4k/vHlaikHfd5bPLh+ehAo7dp50sH+3INXrv1vhOwfNX+1ABLj7gtj3A1RYvmL5JYqpLaWuZOrZ11Ruff+Z143CNDeEPcAuLW7t9K2ljn6NBPsyxJ79kXdV7eYmAM8/0iwgH2nOuZBfzTsP4f9Xx1oK9n/EdVWG0WHWMv8EiWBRBGqZ17xiHE333ZlAOI0xIVTqXG3yXMt862i4+Qf/lUn8C81hohER9Hp4+q8huKvgpuoWtHaz4amoGz/JcJOqLXW2vCxbR7hzESnj7Gh8V4eg3AQzXe/yfNY3JnaglnYrYlT/KTaPjWiXNzvMHgHGqy4t0dhf63sgCd4dq1BfxUhcbnKZUOMO62YtqNg3MUSnBe0yacTo5RvtmDgXhXY9ZHQWUaJ9l9q1UbbQLOEtPd0zrU+PyWNooP9t2gB9dJAjfbcb7SGnEtd+YdwlWT/lXZN+aod7TzdOItjF7fLjVaZA6ULqBfKOON8LCFX0hfhYP9UKSy6EEtwXKwbzutJb7xvwtM6XSkG8vlb9XiCFT9ZBYQWfIJXJg48p7OM7V8JTqz0FdFuOO+b8HTKkQzhebNbLS88OIHPcQD4LZXqJTgIT4729x7ocUTBkapf3hoIT4lni6/onJgdhh1ktNW4jWkMXA3PsU5RlNwqsP9xRh7fbcQtlYPgpkjaVp0tfsxE9H0fPlF0gFQNwh8uryc9P/kuKX8vxAr3MMaxDnqDWJQkXzU28EEr2HSTwOO7TDgBQvvHvHE8TlnoD9+mDhKL/j0uXX6FAHcaA6wSNLjH8S7BC7iCOx0e5o01hUcgyjsGalHudcD+gwTf7NXZPwbnCdp8BRb9Ic5YKfmPqDziHQwr4YXn22nLzS62F7yAswji413pd96z2VzdsPqOlLdaOQtOxUnkQ84HvPezCOKz1f7K+Mn2Prf0GL4/60nvOIL4PGIrNah7B4HwtNpq7QyDoIAYmqlg/FAVs3TTdqAhluSi7rZvlzCIms/13tQ36t9H5krrm6xdwP5jwTiezvYn9rsuDKIKli0iaBRZDcaCsWt3eO+tvTuEzkxrFuh2sVdcgkPXIdzwNoNwibiTrcGBHV5AFcQ2rNrmiI4rT3jOUuYFhgCNYPu2FRzuERvEtn8XNkTnrgpQLixC/RkI0CCYe01eehULuMB7J28jgwTuYfBuHNHZ0qHjLZUpF8orWR/uIXsSMgMhHwPjVvSDiSm5hWsdZR2LIJr+BamTpmOB/JnykFgbPOOr8z7td18lFquuNEQXnX2hYza7R7jps6ZBDQNV21MJAUqyzTKMbZjoMGghPN5lnRLz2fQhPhKpJd7t8wpulIWJDpMdeVjucKeDtbWB58OtgnGLG62GkRwTHSZBtvxQFESTOwUlQDi5d8zbMbKAfU/nrRMEglZ3FD5JZ5MLbgBzQk/sXMfIAhMdAYJMb1fKQZcc4SkhNYXxBrDtVWHgjOcHsdfvI0ZiG0Yt5ukUBkSDGuphh8lGckx0yuSCGN0tVv3RMKiY6BQIzpAo19S1Kk8YRmtMdMqFFBsjWevaMCiY6BQKIyAvSq4dw9iGiU7ZUPIX5Zhq1XhDmOiUjd0wNorDRKdsTHSM4jDRMQwjKiY6ZWOHwkZxmOiUjX39bRSHiU7ZUETHYq+MpJjoFAou+VHSmRaZaNzYH0x0yoUUvJk6AbphmOiUC0V0cqk/b7xhTHQKBCVwKFsr83KM5JjoFEZQQZWCiY6RHBOd8rghejmrHIr/G4aJTkGgzhY1J44JjpEFJjqF0LGiaB2aCeMNozV/2qvKG9S8ugkK0VO4t4TsRi6Y6OSPF4sjZi9F63AZBgfbXuXPiJiEveLOLgQaOWGikznYFg2JwrOyyp5GbpjoFACEhyIeF1a/3MgNq/BZEAfXTz704VvLHvttlZUSNrLDPJ2CWE96/lus8xY9Xlo1TyNXzNMpkIY7O/4cZ2DbKiNXzNMpkPWk572Y25qee8EZmuAYOWOiUygQnrug90sIjl0CNLLGLgeWzRiR4z4b4Ag1zg0ja+xMxzCMeDjn/gOfmMxfrwK3DwAAAABJRU5ErkJggg==", - "ICON": "logoSquare.png", - "APP_ID": "a569f8fb0309417780b793786b534a86", - "PROJECT_ID": "49c705c1c9efb71000d7", - "FRONTEND_ENDPOINT": "https://app-builder-core-live-streaming-git-preprod-agoraio.vercel.app", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "MIX", - "SCREEN_SHARING": true, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform", + "APP_ID": "a32ad7a7333e40dbaccdeff5d543ef56", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, + "ENABLE_APPLE_OAUTH": false, "ENABLE_SLACK_OAUTH": false, "ENABLE_MICROSOFT_OAUTH": false, - "ENABLE_APPLE_OAUTH": false, "GOOGLE_CLIENT_ID": "", "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROFILE": "720p_3", - "SCREEN_SHARE_PROFILE": "1080p_2", + "PROJECT_ID": "2b8279fa91bda33fcf84", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", + "BUCKET_NAME": "", + "BUCKET_ACCESS_KEY": "", + "BUCKET_ACCESS_SECRET": "", + "GOOGLE_CLIENT_SECRET": "", + "MICROSOFT_CLIENT_SECRET": "", + "SLACK_CLIENT_SECRET": "", + "APPLE_PRIVATE_KEY": "", + "APPLE_KEY_ID": "", + "APPLE_TEAM_ID": "", + "PSTN_EMAIL": "", + "PSTN_ACCOUNT": "", + "PSTN_PASSWORD": "", + "RECORDING_REGION": 3, + "GEO_FENCING": true, + "LOG_ENABLED": true, "EVENT_MODE": true, "RAISE_HAND": true, - "LOG_ENABLED": true, - "GEO_FENCING": true, - "GEO_FENCING_INCLUDE_AREA": "GLOBAL", - "GEO_FENCING_EXCLUDE_AREA": "CHINA", "AUDIO_ROOM": false, + "PRODUCT_ID": "breakoutroomfeaturetesting", + "APP_NAME": "BreakoutRoomFeatureTesting", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "2b8279fa91bda33fcf84-7y25qrqsf-agoraio.vercel.app", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1610292", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "BG": "", + "PRIMARY_FONT_COLOR": "#363636", + "SECONDARY_FONT_COLOR": "#FFFFFF", + "SENTRY_DSN": "", + "PROFILE": "480p_8", + "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, + "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", "SECONDARY_ACTION_COLOR": "#FFFFFF", "FONT_COLOR": "#FFFFFF", - "BG": "", "BACKGROUND_COLOR": "#111111", "VIDEO_AUDIO_TILE_COLOR": "#222222", "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#00000040", @@ -54,30 +82,24 @@ "CARD_LAYER_4_COLOR": "#333333", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_TEXT": true, "ICON_BG_COLOR": "#242529", "TOOLBAR_COLOR": "#111111", "ACTIVE_SPEAKER": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_IDP_AUTH": false, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, + "WHITEBOARD_APPIDENTIFIER": "WUjVACgwEe2QlOX96Oc4TA/DXlhL5JAksoOSQ", + "WHITEBOARD_REGION": "us-sv", "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, "ENABLE_WHITEBOARD": true, - "ENABLE_WHITEBOARD_FILE_UPLOAD": false, - "ENABLE_WAITING_ROOM": true, - "WHITEBOARD_APPIDENTIFIER": "EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "41754367", - "CHAT_APP_NAME": "1042822", - "CHAT_URL": "https://a41.chat.agora.io", - "ENABLE_NOISE_CANCELLATION_BY_DEFAULT": true, - "DISABLE_LANDSCAPE_MODE": false, + "ENABLE_WHITEBOARD_FILE_UPLOAD": true, + "ENABLE_CHAT_NOTIFICATION": true, + "ENABLE_CHAT_OPTION": true, + "ENABLE_WAITING_ROOM": false, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false, - "ENABLE_TEXT_TRACKS": false + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, + "SDK_CODEC": "vp8", + "ENABLE_BREAKOUT_ROOM": false } diff --git a/voice-chat.config.json b/voice-chat.config.json index 2d668e279..4f9f9d710 100644 --- a/voice-chat.config.json +++ b/voice-chat.config.json @@ -1,42 +1,70 @@ { - "PRODUCT_ID": "helloworld", - "APP_NAME": "HelloWorld", - "LOGO": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKIAAAA5CAYAAAC1U/CbAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABFwSURBVHgB7Z0NnFTVdcDPue/NzJuFBSV+EBEFAvxsCjZVUxXUfGgNajWkiRoTg4GdXVBQSLStpmUFTWt+KthE4rK7s7uuJmqoaaOm1aZNFaI0mlhav1JTMVsMMcSA4sLMezPv3ZNzZ0H36933MW83+Nv5/37+lpl75r77cd6955577hUhBuNb6CgvC7PA9Y4hgJQAKEgydgsLXil8AV8Pm8+4LppMrndqodF8CKpk0jdogpMuT5cpMZMI6oAAoQos03j4rcX4FlTBuPZ9R0svMwNSOJXLlOEC2QS0B8rmdvtK7IFqaN0zMYP1p6Ew/9fO4f+H/t0assZPLs3wTOPEYs58AGJS10rvl+jORsTJXDdTGNBb4rq5wnwBIrSb1UrHUcr9g9CdZbWXPwooPsv/XMj/He0vib8ApM0A8h67IfX4cBLpDvdTQoqVLPcR/mjbOSMLMchspFloykWscxci0Inq4ZAQHooPlRvwfyAiVr58FkpYSEJ8hj9O1Yju4pd4iyB3U7Ex8yBEfQ53IBjygALi04TY7DTgD3zlO7mtPe8qErgACSao77jdw7fXespmJsJ8lN4V/LwF3NJH+MoS/JzTt3jotZYb0j8dtjx5N8f1X4aAJ7NsT2BBrHZ3MXf09YgwG6LTA5LW2k3m3ZVPm8jI7PU28luU6ycTWRHNtvI8fgX/lmusFDkx5XsXfM3OieOi/CKTLy8Awq9y3U6G6PSw4t7h7IANsIbVOAQDFbEP7tgNzgSxCi5B7+B36TzNEUDf5NSzBucRShEfJzPzilyJSDdwu7wPIsIzwLNCwLriEvN+9XnSt2jC/gLdh4IueEeIFdH0y2BCniaVyNsAiJdV0dPTQGCX1eGdbpbEavdtuRwGKmFkMp30JZByLVexHkYM+cOwkqqdHHS/giS+zA0at6mmoZRft6bQn0FLscm+MtsDMeCHr7B6YbfNE7D6nMkTzxReVxwFUtR10DHeduriQejcuO+7GvFIwn2ZvHeSUxB/s7/grkEhLhgqNwzZPM0nkN8F7RQcmV0++YUaEVWjSKD7gCqj4EjyWyiXPhxGGbijZyLIfwP1wiUFwmus1NcWc/gPOrHhRsR3kdxGJo+A8mZdHroRMdvhXiIJ21hgIiQEj9hvcX6HDUngEVEM/q4u714gyVMjQpJKCNXkpxpdEv1oFJSQeMq4JpQSttFsVsInIUklrJQApvIgsKky8sfOQjwUpIQ6ePS6iggfSFIJFcMq4QEGKKLVQtM8wk1s52TgkIEQDI9HB5oBIwiv/N5EFBcftGV0ZFtoCorKSJj0y/oOPFWvt/LFs2L8VNvhQaS7nBP49xtgRGxvf95VxFZKQYp+yPZAHRxCWHn1ZuOfwAiDAtNc918EChIJSknlDYi0mIlXqPS36lvfPgJGCTUQCc98FEZZCRXvKKJleq0hR53dRPI2cuV5WBbH1lmCh28xjb87l4u/kTsqvE8rAF6xX85//jpIjkezAo/iD/L0/TkwStPrCmJipiQmeeSdznbJ1SzxUwjOZJz05COHt5J2OrI6YTX/mQVB2fXZQ3fwqvEi1U7jUUww9hUn81B3JiffAsqjEJzJ1LIx7uuQHHuAJHsb5DkiI44ZkpqmjRDC1OC6qT6+CYQ8W6CYonSAbZoPsElwHrdjN6fthohUND/dRXOFJ58LFCa57rCjzebXL8KCr1DrTi7X5Os559UQDt/FCq+2t3OtZwQUaiuWjEuKV+JOnRi7MS5mm6490O4haLYbjeHtq06aannyZa5bVl8kuDNjiuYgh3g2763gTr0TAuAOvsjJ4SP9v9MvVoYpE8j14503b3xj+VH7hkvvW2HLhwMzknCT/StxM7uZXD8R5ch30WrmFfNVEK5wfYsVIeWXg2TZz7Wy2Ji6TquEiqVTCtyRzfyvr0AVpDvdzwQqIUC3LYyzg5RQUVKrUBQnsftI33kIq6CLrOGSLA8+H6SEnH5DsdG4JsyuTDFnbBCSLuR6FvRZUmD/+MGzRRklXVrMpa71U8LKM4iWafNRIzznYzcZN+qUULG/cfwuJ2eyq67PjRQGAbxdx0/5vE6IK/MlZwl+AyLAo9wtXLvFEBPeefmiVoCgxW4Qi3k7yYaQ8M7Dq+CiWgDotiEnpV3v/GFTUC7V/I5997LZbjC+BhEoNJnfZ0VboornL0UfTW90PgjxWFlsMjfpBKx2ms59db5GhKQU5wblMxhui7XcT2vDyIp0yjuH/6Z8JRC6nEbj7yEGdoN5NxFF3r6Cu+hw7lZdw/SkHLGaRzeCqGVaijt4FNK+/ULgpYO/y3bQ6aCznxCfLjWmYrlMio3md7irb9XJiLT5KYhON/ddS5AQGXChNp3k7eUm/AnEgGfHNdw4TwTJcZvDOToB8kSkN3zIA8BYBxHJmO7Jqmf90jmhpXcFRjaID8Kj0MPsJ9vuKyBhiNtESnceaCBP/h1UgS3NdfxW7ffNX6L2+cPBo9htYeTQ8z6hSSYBZqTZcJgnfDVIgrch4QT/ZHrGacKfQxUUG/HH7KANXAgNKJQQ2v1aiSL6KDsIFN69/okwWRnc/b/i1eGp4M8up8l4BKphKe4V5B+0wK/lSRABVuptpSZ8MZSswBP886F/4T78JVSB/UtQ7i7twMGLFZztX0DaAgkgAJ+MIs+rxOP90+DViq1XLQg/0iWXIH3swOdWpmafMtG2ylq5SiS4/nvcRJPhHhoHIeEVcLgXYw2ZXPLpfskCcTNUyxqUbAf/u05ErZr9vfAevQYJIDGa4vAI6uvE5Qol46d0TG2ZBIiBuyboHzggkKqaNQ7Cnolf6dKz+8PvmJA0Q9l09e/vVXn6mkGeFNshAaQkbd1ExQTwAw0HkoANqCji/Ib6jy4IHiQA1oMb6QdS004SItXPD/RA7wEwI+x4SDdU/aSoN2AUYNNCq0uCAHs1P54MSRSC8KhIPwDSGe3HQhLsc3VBq1x3Y6DPDcXbfrK8qxArzGrIMw393rUloRcSZn/Jf4GkQCwfCUmAOEWXzItmucsvkVeWyezxCjwlkjyQr3HML8eMoG24MEgD5+rSyYSBTnKSvoY/D9/zIQkknqFJLbzJCxpImuXILxy+4ZeMYPwxJAAPeCfq0tkWNfzD4RHmqbMgUA0tveww1zbwUAxTZ5ekC+idCVXCq+DzfBMJHKdnYAAE+0Of8RPn+XJGppviRLAPzujj/on0PIwY0r+9CRbygkZAFVhdNI3b6I90Muzbpaf9Ennb57BCVl4N1RTCrGtUf6L8Bh3QrrBQwF9CFWTuKs5gM3Sh/wNo65CQfYF6D4ILseMHFVaHu4j/+K5eed/2WRghSMV6+j/46OwU71KoAt6WuSFIRqRlWbvRzT6m6ypbQDFQYUU8bV0PEenbO9ZFzOCZmby3EuLA9oZIpf9JK4MwJCbRwTe3cF187UQkuUwdnIIYqHbiWUO7Fca7G9+FEQIN85916YS4Xp3agxjUddJJvPhsCpITvU0Wux7oP/0E1KjIQ/fj2c7CVIiA1U3HgUmb2aYbDzGQhNqG56H+tnS7++cQkUyH10YIWnsF3NK/Dvmu4chenp8e1f4OxP1Wno6HCBzWxe2bpqDjBj12Y+o/YISwl+BmtuF0TuvJZNAj9d0UaVFmtRene1KGKnff3C/hm1ophONJZrao8HgIQaqdToOyt4W1OHbwaKksWrlxdMZ5ig3cTaxYfwEhqL+Pjsjk3Yd5igs4vEX32kvrdgybQkLfTgDHsNTWrIocCoE6DmtL+RRnPFMnxy9d0HOrhv2zrbp0NmVOLpfl01ZLcRqEINNWPgcw/VTY4wZ9fik2RjNT5E9CbSMh3M3eyW47Z24eEHTQSilLuPP5O7YJ8TKA0D4v/3jEvKfCycJEb7zAfrOvpTLpx3qvGLgHPb6F5rhp+Wm2g1axEuodwsSWnluapTuzksnL73GnfRIC4OnsQbYr25xe2ALX4Ls+NG5r61j3DBZYxA7sy3jGqQso0yt2lrc8L8cBZkFQPKIKXHaWpR6DsHRQvUXeS9xtQe4x5cft9ki0l3n7dkCKOvtcD7ww9VYh4vkQlv7nmit2Skr+DEIuLNQGPXesWsnZ/OF9hJWVUZwjnv6n+LhiVj2xwtOHISyIL3F5dvNLlSVSB9wp/LkSFcYVEEFTOWIp5csRTI4yd8rzXBZuL5gggY6PcqbEtN05+1ZkhriOAgNjDfExe3Fw1Et/xrWVP+EJEVp5WQf2ct1U2cr835EkWQfiHDUZfMA+2+6tYvvpDhhdtMdJMx00A8l7Ju7Z3LCoEcxpEBeHkbXy7he4PCokfmTPdpC8mV+M5uGSgkdEMdtZhv8HEeFZSM1AzTCaDD5OWlRxh1WGMyWNCnBwhXER/7MAI8eLWaFMinCwWXIvv/nXwcjS7qeEIXg1jhIqeFC4kUfvNhhlhjgq7aUpdVjpJjiEcJfgVhelipv8NSSNpCdTKfGRqBcuFRuM9expvLYSv54w6jin3SCWQkxIQlXxg06jWEYgbodRZFiPuXorUNDnlA0ACaDOOwgQy7mJX4aYmAVXKWHoYwFhIQNf7l0EeyAGxSZjPYHxyaROLnI7OazW1xVzxtVxos8P0OOURQdUBZKTQ+WNuBEgmSATrtxvePNEbSIM65Hw3bpRB82xLD7EjRx44Fz/fHxIkJhbyOFdbLYvgDDHKAdh3VmcDtnME5D0rQpQifRpsDrl3crRDTFQp+t4P/YM/nV10xnREzwKneI0Ro9o78cOdtef3bd/XD08IN3Ei56Z/FL8AOLC7gpWgg6zKOaUGs2HyBZ/CsMoY6jGT+edOQLNVZyhCikPjn7BSjRuOyvgJlbAbf2Tsp00lXcJ1nIBv3jgOEDg3TcV53hJ+SUxkrM4Cjz83O7kjFA+ST8qO0kpdzlvRy3kms0M8cy3BMG3IS3uKV6Bz0AE+i9WqG+m2GA6Yt3+5Zi8+QKVfptHnsc7JLxHjxQmmup11oM8mqKV6zYggKTioUl7t/AK8bOVL8JcSzeYVHvpNBCpEwzyVMSJcnBnePPXYd/a81LScwbIZws7U9uCrldTOxDkwQIwvHOdnPnpoOdmumgWuPK/gtwm3Ckuy+zl9zAd3Z1Ei9RCBBJAvbzsQzmRXVynsIvjD9kYHc8bB8rN8RIPvs+ZKe/H++emXoRTsAwxOKCIt3I9N2dNcX+1l4qGhgtvtbnzKWXORSlP5c8zWJHUFTUFJPxvHv5ekBlja2kR/iwoK+XQBwMuQPQ+OLLuh4QxO8pnGKSmwiGOabbxZCsK+H5xn/nsQQeyutm2ZHhnCoHKNrk8KH91/006LWYNdorXGHneU4qosDrKH+OpTzld0+qzulXBkmJ1UKxeppU+gEbl4iRtAAePqLfyFP1XUGNUec8pooIdyldw0bukgNWlJerm2JDk906ycNxjbJvodmr2WYaYOmpTXY0KVQU8/r5gO66bR8WPR1JCRW7iHts0VECs7uDUeNv1Am3WGsnynlREhd0YbR/1HZT9R7JBJ8IukDi3KtSogvesIlYDb509ob8Gg+bF9SvWiMeYVEQF+1l9o7RZAw/P5mEK1Bg1xqwiCkNoL+90oZzMsdUaoRi7I2IZtPe5iITOKtcIx5hVRPZC6uvuUjKb/TVCMWYVET1XawOiYWpvQKiRLGNWEaUU+vM5aO+AGqPG2J2aBehO871h57LJ3DpWIxQmjEEOHIT3PdtMI3irQo3hGXMj4sRvq/u5RbdOBqX7HagxqowpRVRHQZ0iPQX6SO89dsmu7hriGpEZM1OzivKWZakuUgqK8v5HWDGhFo84yoyZEdHOwk5CDLjaDd+AcilaRE+NRBg7U/Ml6DmA6mTiNj8RBLwm7v+0u0Z1jK3FSgP2OoZQkTVbh6QRrC3m8AGo8Xth7PkR1f8yLW1c1v8sMrtrvmfvFIfUpQJjjTEbc1e5U0d6TwLio7YnlsHSeKfpatSomsolmTUOCX4Hn8i63ZAyQc4AAAAASUVORK5CYII=", - "ICON": "logoSquare.png", - "APP_ID": "a569f8fb0309417780b793786b534a86", - "PROJECT_ID": "49c705c1c9efb71000d7", - "FRONTEND_ENDPOINT": "https://app-builder-core-voice-chat-git-preprod-agoraio.vercel.app", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "MIX", - "SCREEN_SHARING": false, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform", + "APP_ID": "a32ad7a7333e40dbaccdeff5d543ef56", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, + "ENABLE_APPLE_OAUTH": false, "ENABLE_SLACK_OAUTH": false, "ENABLE_MICROSOFT_OAUTH": false, - "ENABLE_APPLE_OAUTH": false, "GOOGLE_CLIENT_ID": "", "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROFILE": "720p_3", - "SCREEN_SHARE_PROFILE": "1080p_2", + "PROJECT_ID": "2b8279fa91bda33fcf84", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", + "BUCKET_NAME": "", + "BUCKET_ACCESS_KEY": "", + "BUCKET_ACCESS_SECRET": "", + "GOOGLE_CLIENT_SECRET": "", + "MICROSOFT_CLIENT_SECRET": "", + "SLACK_CLIENT_SECRET": "", + "APPLE_PRIVATE_KEY": "", + "APPLE_KEY_ID": "", + "APPLE_TEAM_ID": "", + "PSTN_EMAIL": "", + "PSTN_ACCOUNT": "", + "PSTN_PASSWORD": "", + "RECORDING_REGION": 3, + "GEO_FENCING": true, + "LOG_ENABLED": true, "EVENT_MODE": false, "RAISE_HAND": false, - "LOG_ENABLED": true, - "GEO_FENCING": true, - "GEO_FENCING_INCLUDE_AREA": "GLOBAL", - "GEO_FENCING_EXCLUDE_AREA": "CHINA", "AUDIO_ROOM": true, + "PRODUCT_ID": "breakoutroomfeaturetesting", + "APP_NAME": "BreakoutRoomFeatureTesting", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "2b8279fa91bda33fcf84-7y25qrqsf-agoraio.vercel.app", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1610292", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "BG": "", + "PRIMARY_FONT_COLOR": "#363636", + "SECONDARY_FONT_COLOR": "#FFFFFF", + "SENTRY_DSN": "", + "PROFILE": "480p_8", + "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, + "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", "SECONDARY_ACTION_COLOR": "#FFFFFF", "FONT_COLOR": "#FFFFFF", - "BG": "", "BACKGROUND_COLOR": "#111111", "VIDEO_AUDIO_TILE_COLOR": "#222222", "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#00000040", @@ -54,30 +82,24 @@ "CARD_LAYER_4_COLOR": "#333333", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_TEXT": true, "ICON_BG_COLOR": "#242529", "TOOLBAR_COLOR": "#111111", "ACTIVE_SPEAKER": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_IDP_AUTH": false, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, + "WHITEBOARD_APPIDENTIFIER": "WUjVACgwEe2QlOX96Oc4TA/DXlhL5JAksoOSQ", + "WHITEBOARD_REGION": "us-sv", "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, "ENABLE_WHITEBOARD": true, - "ENABLE_WHITEBOARD_FILE_UPLOAD": false, - "ENABLE_WAITING_ROOM": true, - "WHITEBOARD_APPIDENTIFIER": "EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "41754367", - "CHAT_APP_NAME": "1042822", - "CHAT_URL": "https://a41.chat.agora.io", - "ENABLE_NOISE_CANCELLATION_BY_DEFAULT": true, - "DISABLE_LANDSCAPE_MODE": false, + "ENABLE_WHITEBOARD_FILE_UPLOAD": true, + "ENABLE_CHAT_NOTIFICATION": true, + "ENABLE_CHAT_OPTION": true, + "ENABLE_WAITING_ROOM": false, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false, - "ENABLE_TEXT_TRACKS": false + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, + "SDK_CODEC": "vp8", + "ENABLE_BREAKOUT_ROOM": false } From 32ea1d17596405ac864f16a37c90ee314d6500e8 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 13 Oct 2025 12:30:22 +0530 Subject: [PATCH 50/56] add light theme --- audio-livecast.config.light.json | 104 +++++++++++++++++++------------ config.light.json | 81 +++++++++++++----------- live-streaming.config.light.json | 104 +++++++++++++++++++------------ voice-chat.config.light.json | 104 +++++++++++++++++++------------ 4 files changed, 232 insertions(+), 161 deletions(-) diff --git a/audio-livecast.config.light.json b/audio-livecast.config.light.json index a7221d32a..4a73960ec 100644 --- a/audio-livecast.config.light.json +++ b/audio-livecast.config.light.json @@ -1,42 +1,70 @@ { - "PRODUCT_ID": "helloworld", - "APP_NAME": "HelloWorld", - "LOGO": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKIAAAA5CAYAAAC1U/CbAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABFwSURBVHgB7Z0NnFTVdcDPue/NzJuFBSV+EBEFAvxsCjZVUxXUfGgNajWkiRoTg4GdXVBQSLStpmUFTWt+KthE4rK7s7uuJmqoaaOm1aZNFaI0mlhav1JTMVsMMcSA4sLMezPv3ZNzZ0H36933MW83+Nv5/37+lpl75r77cd6955577hUhBuNb6CgvC7PA9Y4hgJQAKEgydgsLXil8AV8Pm8+4LppMrndqodF8CKpk0jdogpMuT5cpMZMI6oAAoQos03j4rcX4FlTBuPZ9R0svMwNSOJXLlOEC2QS0B8rmdvtK7IFqaN0zMYP1p6Ew/9fO4f+H/t0assZPLs3wTOPEYs58AGJS10rvl+jORsTJXDdTGNBb4rq5wnwBIrSb1UrHUcr9g9CdZbWXPwooPsv/XMj/He0vib8ApM0A8h67IfX4cBLpDvdTQoqVLPcR/mjbOSMLMchspFloykWscxci0Inq4ZAQHooPlRvwfyAiVr58FkpYSEJ8hj9O1Yju4pd4iyB3U7Ex8yBEfQ53IBjygALi04TY7DTgD3zlO7mtPe8qErgACSao77jdw7fXespmJsJ8lN4V/LwF3NJH+MoS/JzTt3jotZYb0j8dtjx5N8f1X4aAJ7NsT2BBrHZ3MXf09YgwG6LTA5LW2k3m3ZVPm8jI7PU28luU6ycTWRHNtvI8fgX/lmusFDkx5XsXfM3OieOi/CKTLy8Awq9y3U6G6PSw4t7h7IANsIbVOAQDFbEP7tgNzgSxCi5B7+B36TzNEUDf5NSzBucRShEfJzPzilyJSDdwu7wPIsIzwLNCwLriEvN+9XnSt2jC/gLdh4IueEeIFdH0y2BCniaVyNsAiJdV0dPTQGCX1eGdbpbEavdtuRwGKmFkMp30JZByLVexHkYM+cOwkqqdHHS/giS+zA0at6mmoZRft6bQn0FLscm+MtsDMeCHr7B6YbfNE7D6nMkTzxReVxwFUtR10DHeduriQejcuO+7GvFIwn2ZvHeSUxB/s7/grkEhLhgqNwzZPM0nkN8F7RQcmV0++YUaEVWjSKD7gCqj4EjyWyiXPhxGGbijZyLIfwP1wiUFwmus1NcWc/gPOrHhRsR3kdxGJo+A8mZdHroRMdvhXiIJ21hgIiQEj9hvcX6HDUngEVEM/q4u714gyVMjQpJKCNXkpxpdEv1oFJSQeMq4JpQSttFsVsInIUklrJQApvIgsKky8sfOQjwUpIQ6ePS6iggfSFIJFcMq4QEGKKLVQtM8wk1s52TgkIEQDI9HB5oBIwiv/N5EFBcftGV0ZFtoCorKSJj0y/oOPFWvt/LFs2L8VNvhQaS7nBP49xtgRGxvf95VxFZKQYp+yPZAHRxCWHn1ZuOfwAiDAtNc918EChIJSknlDYi0mIlXqPS36lvfPgJGCTUQCc98FEZZCRXvKKJleq0hR53dRPI2cuV5WBbH1lmCh28xjb87l4u/kTsqvE8rAF6xX85//jpIjkezAo/iD/L0/TkwStPrCmJipiQmeeSdznbJ1SzxUwjOZJz05COHt5J2OrI6YTX/mQVB2fXZQ3fwqvEi1U7jUUww9hUn81B3JiffAsqjEJzJ1LIx7uuQHHuAJHsb5DkiI44ZkpqmjRDC1OC6qT6+CYQ8W6CYonSAbZoPsElwHrdjN6fthohUND/dRXOFJ58LFCa57rCjzebXL8KCr1DrTi7X5Os559UQDt/FCq+2t3OtZwQUaiuWjEuKV+JOnRi7MS5mm6490O4haLYbjeHtq06aannyZa5bVl8kuDNjiuYgh3g2763gTr0TAuAOvsjJ4SP9v9MvVoYpE8j14503b3xj+VH7hkvvW2HLhwMzknCT/StxM7uZXD8R5ch30WrmFfNVEK5wfYsVIeWXg2TZz7Wy2Ji6TquEiqVTCtyRzfyvr0AVpDvdzwQqIUC3LYyzg5RQUVKrUBQnsftI33kIq6CLrOGSLA8+H6SEnH5DsdG4JsyuTDFnbBCSLuR6FvRZUmD/+MGzRRklXVrMpa71U8LKM4iWafNRIzznYzcZN+qUULG/cfwuJ2eyq67PjRQGAbxdx0/5vE6IK/MlZwl+AyLAo9wtXLvFEBPeefmiVoCgxW4Qi3k7yYaQ8M7Dq+CiWgDotiEnpV3v/GFTUC7V/I5997LZbjC+BhEoNJnfZ0VboornL0UfTW90PgjxWFlsMjfpBKx2ms59db5GhKQU5wblMxhui7XcT2vDyIp0yjuH/6Z8JRC6nEbj7yEGdoN5NxFF3r6Cu+hw7lZdw/SkHLGaRzeCqGVaijt4FNK+/ULgpYO/y3bQ6aCznxCfLjWmYrlMio3md7irb9XJiLT5KYhON/ddS5AQGXChNp3k7eUm/AnEgGfHNdw4TwTJcZvDOToB8kSkN3zIA8BYBxHJmO7Jqmf90jmhpXcFRjaID8Kj0MPsJ9vuKyBhiNtESnceaCBP/h1UgS3NdfxW7ffNX6L2+cPBo9htYeTQ8z6hSSYBZqTZcJgnfDVIgrch4QT/ZHrGacKfQxUUG/HH7KANXAgNKJQQ2v1aiSL6KDsIFN69/okwWRnc/b/i1eGp4M8up8l4BKphKe4V5B+0wK/lSRABVuptpSZ8MZSswBP886F/4T78JVSB/UtQ7i7twMGLFZztX0DaAgkgAJ+MIs+rxOP90+DViq1XLQg/0iWXIH3swOdWpmafMtG2ylq5SiS4/nvcRJPhHhoHIeEVcLgXYw2ZXPLpfskCcTNUyxqUbAf/u05ErZr9vfAevQYJIDGa4vAI6uvE5Qol46d0TG2ZBIiBuyboHzggkKqaNQ7Cnolf6dKz+8PvmJA0Q9l09e/vVXn6mkGeFNshAaQkbd1ExQTwAw0HkoANqCji/Ib6jy4IHiQA1oMb6QdS004SItXPD/RA7wEwI+x4SDdU/aSoN2AUYNNCq0uCAHs1P54MSRSC8KhIPwDSGe3HQhLsc3VBq1x3Y6DPDcXbfrK8qxArzGrIMw393rUloRcSZn/Jf4GkQCwfCUmAOEWXzItmucsvkVeWyezxCjwlkjyQr3HML8eMoG24MEgD5+rSyYSBTnKSvoY/D9/zIQkknqFJLbzJCxpImuXILxy+4ZeMYPwxJAAPeCfq0tkWNfzD4RHmqbMgUA0tveww1zbwUAxTZ5ekC+idCVXCq+DzfBMJHKdnYAAE+0Of8RPn+XJGppviRLAPzujj/on0PIwY0r+9CRbygkZAFVhdNI3b6I90Muzbpaf9Ennb57BCVl4N1RTCrGtUf6L8Bh3QrrBQwF9CFWTuKs5gM3Sh/wNo65CQfYF6D4ILseMHFVaHu4j/+K5eed/2WRghSMV6+j/46OwU71KoAt6WuSFIRqRlWbvRzT6m6ypbQDFQYUU8bV0PEenbO9ZFzOCZmby3EuLA9oZIpf9JK4MwJCbRwTe3cF187UQkuUwdnIIYqHbiWUO7Fca7G9+FEQIN85916YS4Xp3agxjUddJJvPhsCpITvU0Wux7oP/0E1KjIQ/fj2c7CVIiA1U3HgUmb2aYbDzGQhNqG56H+tnS7++cQkUyH10YIWnsF3NK/Dvmu4chenp8e1f4OxP1Wno6HCBzWxe2bpqDjBj12Y+o/YISwl+BmtuF0TuvJZNAj9d0UaVFmtRene1KGKnff3C/hm1ophONJZrao8HgIQaqdToOyt4W1OHbwaKksWrlxdMZ5ig3cTaxYfwEhqL+Pjsjk3Yd5igs4vEX32kvrdgybQkLfTgDHsNTWrIocCoE6DmtL+RRnPFMnxy9d0HOrhv2zrbp0NmVOLpfl01ZLcRqEINNWPgcw/VTY4wZ9fik2RjNT5E9CbSMh3M3eyW47Z24eEHTQSilLuPP5O7YJ8TKA0D4v/3jEvKfCycJEb7zAfrOvpTLpx3qvGLgHPb6F5rhp+Wm2g1axEuodwsSWnluapTuzksnL73GnfRIC4OnsQbYr25xe2ALX4Ls+NG5r61j3DBZYxA7sy3jGqQso0yt2lrc8L8cBZkFQPKIKXHaWpR6DsHRQvUXeS9xtQe4x5cft9ki0l3n7dkCKOvtcD7ww9VYh4vkQlv7nmit2Skr+DEIuLNQGPXesWsnZ/OF9hJWVUZwjnv6n+LhiVj2xwtOHISyIL3F5dvNLlSVSB9wp/LkSFcYVEEFTOWIp5csRTI4yd8rzXBZuL5gggY6PcqbEtN05+1ZkhriOAgNjDfExe3Fw1Et/xrWVP+EJEVp5WQf2ct1U2cr835EkWQfiHDUZfMA+2+6tYvvpDhhdtMdJMx00A8l7Ju7Z3LCoEcxpEBeHkbXy7he4PCokfmTPdpC8mV+M5uGSgkdEMdtZhv8HEeFZSM1AzTCaDD5OWlRxh1WGMyWNCnBwhXER/7MAI8eLWaFMinCwWXIvv/nXwcjS7qeEIXg1jhIqeFC4kUfvNhhlhjgq7aUpdVjpJjiEcJfgVhelipv8NSSNpCdTKfGRqBcuFRuM9expvLYSv54w6jin3SCWQkxIQlXxg06jWEYgbodRZFiPuXorUNDnlA0ACaDOOwgQy7mJX4aYmAVXKWHoYwFhIQNf7l0EeyAGxSZjPYHxyaROLnI7OazW1xVzxtVxos8P0OOURQdUBZKTQ+WNuBEgmSATrtxvePNEbSIM65Hw3bpRB82xLD7EjRx44Fz/fHxIkJhbyOFdbLYvgDDHKAdh3VmcDtnME5D0rQpQifRpsDrl3crRDTFQp+t4P/YM/nV10xnREzwKneI0Ro9o78cOdtef3bd/XD08IN3Ei56Z/FL8AOLC7gpWgg6zKOaUGs2HyBZ/CsMoY6jGT+edOQLNVZyhCikPjn7BSjRuOyvgJlbAbf2Tsp00lXcJ1nIBv3jgOEDg3TcV53hJ+SUxkrM4Cjz83O7kjFA+ST8qO0kpdzlvRy3kms0M8cy3BMG3IS3uKV6Bz0AE+i9WqG+m2GA6Yt3+5Zi8+QKVfptHnsc7JLxHjxQmmup11oM8mqKV6zYggKTioUl7t/AK8bOVL8JcSzeYVHvpNBCpEwzyVMSJcnBnePPXYd/a81LScwbIZws7U9uCrldTOxDkwQIwvHOdnPnpoOdmumgWuPK/gtwm3Ckuy+zl9zAd3Z1Ei9RCBBJAvbzsQzmRXVynsIvjD9kYHc8bB8rN8RIPvs+ZKe/H++emXoRTsAwxOKCIt3I9N2dNcX+1l4qGhgtvtbnzKWXORSlP5c8zWJHUFTUFJPxvHv5ekBlja2kR/iwoK+XQBwMuQPQ+OLLuh4QxO8pnGKSmwiGOabbxZCsK+H5xn/nsQQeyutm2ZHhnCoHKNrk8KH91/006LWYNdorXGHneU4qosDrKH+OpTzld0+qzulXBkmJ1UKxeppU+gEbl4iRtAAePqLfyFP1XUGNUec8pooIdyldw0bukgNWlJerm2JDk906ycNxjbJvodmr2WYaYOmpTXY0KVQU8/r5gO66bR8WPR1JCRW7iHts0VECs7uDUeNv1Am3WGsnynlREhd0YbR/1HZT9R7JBJ8IukDi3KtSogvesIlYDb509ob8Gg+bF9SvWiMeYVEQF+1l9o7RZAw/P5mEK1Bg1xqwiCkNoL+90oZzMsdUaoRi7I2IZtPe5iITOKtcIx5hVRPZC6uvuUjKb/TVCMWYVET1XawOiYWpvQKiRLGNWEaUU+vM5aO+AGqPG2J2aBehO871h57LJ3DpWIxQmjEEOHIT3PdtMI3irQo3hGXMj4sRvq/u5RbdOBqX7HagxqowpRVRHQZ0iPQX6SO89dsmu7hriGpEZM1OzivKWZakuUgqK8v5HWDGhFo84yoyZEdHOwk5CDLjaDd+AcilaRE+NRBg7U/Ml6DmA6mTiNj8RBLwm7v+0u0Z1jK3FSgP2OoZQkTVbh6QRrC3m8AGo8Xth7PkR1f8yLW1c1v8sMrtrvmfvFIfUpQJjjTEbc1e5U0d6TwLio7YnlsHSeKfpatSomsolmTUOCX4Hn8i63ZAyQc4AAAAASUVORK5CYII=", - "ICON": "logoSquare.png", - "APP_ID": "a569f8fb0309417780b793786b534a86", - "PROJECT_ID": "49c705c1c9efb71000d7", - "FRONTEND_ENDPOINT": "https://app-builder-core-audio-livecast-light-git-preprod-agoraio.vercel.app", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "MIX", - "SCREEN_SHARING": false, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform", + "APP_ID": "a32ad7a7333e40dbaccdeff5d543ef56", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, + "ENABLE_APPLE_OAUTH": false, "ENABLE_SLACK_OAUTH": false, "ENABLE_MICROSOFT_OAUTH": false, - "ENABLE_APPLE_OAUTH": false, "GOOGLE_CLIENT_ID": "", "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROFILE": "720p_3", - "SCREEN_SHARE_PROFILE": "1080p_2", + "PROJECT_ID": "2b8279fa91bda33fcf84", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", + "BUCKET_NAME": "appbuilder-dev-qa-test-recording", + "BUCKET_ACCESS_KEY": "", + "BUCKET_ACCESS_SECRET": "", + "GOOGLE_CLIENT_SECRET": "", + "MICROSOFT_CLIENT_SECRET": "", + "SLACK_CLIENT_SECRET": "", + "APPLE_PRIVATE_KEY": "", + "APPLE_KEY_ID": "", + "APPLE_TEAM_ID": "", + "PSTN_EMAIL": "", + "PSTN_ACCOUNT": "", + "PSTN_PASSWORD": "", + "RECORDING_REGION": 3, + "GEO_FENCING": true, + "LOG_ENABLED": true, "EVENT_MODE": true, "RAISE_HAND": true, - "LOG_ENABLED": true, - "GEO_FENCING": true, - "GEO_FENCING_INCLUDE_AREA": "GLOBAL", - "GEO_FENCING_EXCLUDE_AREA": "CHINA", "AUDIO_ROOM": true, + "PRODUCT_ID": "breakoutroomfeaturetesting", + "APP_NAME": "BreakoutRoomFeatureTesting", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "2b8279fa91bda33fcf84-7y25qrqsf-agoraio.vercel.app", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1610292", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "BG": "https://dbudicf5k4as1.cloudfront.net/10/Artboard.png", + "PRIMARY_FONT_COLOR": "#363636", + "SECONDARY_FONT_COLOR": "#FFFFFF", + "SENTRY_DSN": "", + "PROFILE": "480p_8", + "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, + "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", "SECONDARY_ACTION_COLOR": "#19394D", "FONT_COLOR": "#333333", - "BG": "https://dbudicf5k4as1.cloudfront.net/10/Artboard.png", "BACKGROUND_COLOR": "#FFFFFF", "VIDEO_AUDIO_TILE_COLOR": "#222222", "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#80808080", @@ -54,30 +82,24 @@ "CARD_LAYER_4_COLOR": "#FFFFFF", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_TEXT": true, "ICON_BG_COLOR": "#EBF1F5", "TOOLBAR_COLOR": "#FFFFFF00", "ACTIVE_SPEAKER": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_IDP_AUTH": false, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, + "WHITEBOARD_APPIDENTIFIER": "WUjVACgwEe2QlOX96Oc4TA/DXlhL5JAksoOSQ", + "WHITEBOARD_REGION": "us-sv", "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, "ENABLE_WHITEBOARD": true, - "ENABLE_WHITEBOARD_FILE_UPLOAD": false, - "ENABLE_WAITING_ROOM": true, - "WHITEBOARD_APPIDENTIFIER": "EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "41754367", - "CHAT_APP_NAME": "1042822", - "CHAT_URL": "https://a41.chat.agora.io", - "ENABLE_NOISE_CANCELLATION_BY_DEFAULT": true, - "DISABLE_LANDSCAPE_MODE": false, + "ENABLE_WHITEBOARD_FILE_UPLOAD": true, + "ENABLE_CHAT_NOTIFICATION": true, + "ENABLE_CHAT_OPTION": true, + "ENABLE_WAITING_ROOM": false, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false, - "ENABLE_TEXT_TRACKS": false + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, + "SDK_CODEC": "vp8", + "ENABLE_BREAKOUT_ROOM": false } diff --git a/config.light.json b/config.light.json index 76ac37dda..ac7ea488f 100644 --- a/config.light.json +++ b/config.light.json @@ -1,22 +1,5 @@ { - "PROJECT_ID": "8e5a647465fb0a5e9006", - "APP_ID": "aae40f7b5ab348f2a27e992c9f3e13a7", - "APP_CERTIFICATE": "", - "CUSTOMER_ID": "", - "CUSTOMER_CERTIFICATE": "", - "PRODUCT_ID": "breakoutroomfeature", - "APP_NAME": "BreakoutRoomFeature", - "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", - "ICON": "", - "FRONTEND_ENDPOINT": "", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "MIX", - "SCREEN_SHARING": true, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "APP_ID": "a32ad7a7333e40dbaccdeff5d543ef56", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, "ENABLE_APPLE_OAUTH": false, @@ -26,7 +9,12 @@ "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "BUCKET_NAME": "", + "PROJECT_ID": "2b8279fa91bda33fcf84", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", + "BUCKET_NAME": "appbuilder-dev-qa-test-recording", "BUCKET_ACCESS_KEY": "", "BUCKET_ACCESS_SECRET": "", "GOOGLE_CLIENT_SECRET": "", @@ -44,9 +32,35 @@ "EVENT_MODE": false, "RAISE_HAND": false, "AUDIO_ROOM": false, - "BG": "", + "PRODUCT_ID": "breakoutroomfeaturetesting", + "APP_NAME": "BreakoutRoomFeatureTesting", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "2b8279fa91bda33fcf84-7y25qrqsf-agoraio.vercel.app", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1610292", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "BG": "https://dbudicf5k4as1.cloudfront.net/10/Artboard.png", + "PRIMARY_FONT_COLOR": "#363636", + "SECONDARY_FONT_COLOR": "#FFFFFF", + "SENTRY_DSN": "", "PROFILE": "480p_8", "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, + "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", "SECONDARY_ACTION_COLOR": "#19394D", @@ -68,33 +82,24 @@ "CARD_LAYER_4_COLOR": "#FFFFFF", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_TEXT": true, "ICON_BG_COLOR": "#EBF1F5", "TOOLBAR_COLOR": "#FFFFFF00", "ACTIVE_SPEAKER": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_IDP_AUTH": true, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, + "WHITEBOARD_APPIDENTIFIER": "WUjVACgwEe2QlOX96Oc4TA/DXlhL5JAksoOSQ", + "WHITEBOARD_REGION": "us-sv", "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, - "ENABLE_WHITEBOARD": false, - "ENABLE_WHITEBOARD_FILE_UPLOAD": false, + "ENABLE_WHITEBOARD": true, + "ENABLE_WHITEBOARD_FILE_UPLOAD": true, "ENABLE_CHAT_NOTIFICATION": true, "ENABLE_CHAT_OPTION": true, "ENABLE_WAITING_ROOM": false, - "WHITEBOARD_APPIDENTIFIER": "", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "61394961", - "CHAT_APP_NAME": "1573238", - "CHAT_URL": "https://a61.chat.agora.io", - "DISABLE_LANDSCAPE_MODE": false, - "STT_AUTO_START": false, - "CLOUD_RECORDING_AUTO_START": false, "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, + "STT_AUTO_START": false, + "CLOUD_RECORDING_AUTO_START": false, + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, "SDK_CODEC": "vp8", - "ENABLE_BREAKOUT_ROOM": true, - "ENABLE_TEXT_TRACKS": false + "ENABLE_BREAKOUT_ROOM": true } diff --git a/live-streaming.config.light.json b/live-streaming.config.light.json index 7ba7a652b..e0ec6edae 100644 --- a/live-streaming.config.light.json +++ b/live-streaming.config.light.json @@ -1,42 +1,70 @@ { - "PRODUCT_ID": "helloworld", - "APP_NAME": "HelloWorld", - "LOGO": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAR0AAABiCAYAAABtaQfbAAAACXBIWXMAAAsSAAALEgHS3X78AAAPH0lEQVR4nO2d3VFbSROGZ1V7xQ3KABwBcgSICCxHsEIJrByBIQKbBDBEsCICiwgsRbCQgXWjS7w1/t7DN4gjnXP6Z35EP1WUt2pBM+f0zDs9o+nuP379+uUMwzBi8ad2OwfXT0PnXPXTd86d7Pj1e+fcT+fcwjk3X096c2bbx865EX4GzrnD9aT3B+czCX0Y4Nn9v74/pzHbr+FyPeldxGjo4PqpHzx7G/svnXMPUvbv2Nc5bLNE+7P1pDcT+uxR8B4q+6vaAWO/arP6Odzy6ys88yJ49w/M9n17Y/TB2/x+Pen5/3bing4GWjXRPzA/zr+MWdcBgBfuDfrX5v+LITrBC/fv4Ei7vY58lJpMdSjY34vBjWaf3UvRCXn042g96d0QPq8ag6Mtk11cdNBmNe52iXsbHjH3vnYRIDgZX2valxed4IGnOxSVg38JN3gJP3f0Y4yHru2Dpuig7amAwTV5x13F6oD9pxgDyexPZYvoVHgPfNSm3V0L3gZiooOJfqHoRd9DfLd6nlhsbnYsNM+i0+P2xjd2cP3kH/hf59xnpQHn4DH4z384uH6abumLf+hvin2oxRv94PrpAW3nLDiP0oKzYf+/I9k/yvYw4BTtDnb9UvAemgRHBN8fiOV35W27/+zvvi0I3AsgOPO2ni1LdLBXfcBgiIUf1F8Orp8W4SCA4EQxdtCmn3AzGD23bVQdnbcJu0ho/89e5OsmgHK7c3gyL8DkX8R6Dxh33pv/EfmMsBKfrxCaUHBaL7akg2Q09DX2JN/AP+SPg+unSwz82IIzwiSO6lUxWMFmbDKx/xEmQLSDcdh6hkPZ38QeB1hoZ4kXOe/RjvDsdec3O+ns6UDp54kHXMhnbGuiATf6n4IEx0mdheRof7j9/UjtnVTbO5zhRRsHaO9HJl71EdXT6iQ6UNlF5ucWqmAbF3M7IcFSwsvJ2P6n2PrEEp4pzhWjLXax29OktehgwM0LW91FSXFuJIDfVo25Xk4B9j+JKDy/zxUjtPMbnN9Ea0+bVqJjglO04AzXk96C8yEF2T+m8EQBW6q/9+V5XBvRCb5/f8uCMy1QcPyWaiAgOKXZ/wQHrcWDg9q92FKFtPF0bt74Gc6wUNe2j5ASLiXa/zTBXR5RcGAvesUhF3aKDlZ47lX2YglW+RI54va9cPt/brrMlzl7u7vYek8HE05ytVjC7fVnA4vwYBOqXgUF5hSvdCHYl+c4Mn+vaHPbA4+qegdSE/2D/1xK4GRk+/fx7CNh+9+Ed2oSc4fnXzRteSH2Upf+qvi1aty9GAsYd8fB3FMXuq2xV4IHp7eI2+gaNKYSS9I29gqr5A+BJjsHDWISToXi2Hzow6tbtC36sC/2/7Se9BqvCzTEXlGpLmS2viMF2z8I2H2F8TPr2PZIeLGt2B3wCc/jX2YjS3xVSz7IxOCbSapvB9HhDsIVJhv5fkyLILq2nHcUvZzsf8OcAN4Ox00TT0F0rmD/TudqOIvi3gMjtR30oVr0JO+jNQZ8ct3q2/Wkx/7mBK7gMaJcowEvhzMAH/FVNetCnh8060nPrzznzGfvas+c7D/A1oTKISZQLFYQ+SlBcPrMvvq2zyhth2Dc+TFwhs8U5ZXoYJXjuNX+hY+lOokXMISbHguO4UW+qg6Bl/KeMQCOcN+jkUztP2LaX6w/DVT3oqgH+JztdNW2WOIzfNZQWnjqPJ0R4/OuGC98JxjI6h4PVhvqpKsML57vBSLGmTxt/5Zj/1tl+1M9niPcedGGexGTal+RS6B14DNFhadOdKirvN+zabuxIw13r6YNKiqCU4HseZfEPz+tS8tQA3Xg30t6OFsYY+tKQVt0PjHPrwaMs6uphuBU4LPF5vYL0cGgpDz4KoYLiwmt3Q51cF5qGr4Ce+0l8c935p+B/akXAXO3v6bo3HPP75hir36XDG1wztae2fR0qIbplEeVA1Z7zW0WJTGUWK6allBXnSb7UpNiXUa0/5xo/0PFpF8S95mofYt1XuWkvJ1N0aE+eMwJ54QvrT0DF5dykKeSt3cbmHgUb6fJvvtufw3RuRc6vKV4mLexxN79b9w9SHyhsyk6nS+R4cGjTTj3/0lH3dvvgnp7NUWoBGWiHzZEYFOe/64g+2vcTpbIU0QVwxSBrew2N0WHorbRahNtoPHCKaK7jLnaBFDf+66JR7F/qohuSrviKS+ESuNQxt1KuyxPHWiT9WXOs+i0/GajjlSio9EuZcVJ8vwQOspqX2tnRg6akuwvHeYgdbZImXvqX1potR16OiTRSbTKO8Sn5EDKflDa3mZn0tYjof1TTroc+pBK7Nltc+teRQ1PCInx9XRL3urAc4ntn8OiI3WWFbOUTnLYxfYMwzC6YKJjGEZUuKKTLEES4+BbmpT9SJ2gKmZ1yRfsU/L1TM6nohGKDmV/2nTvQxMTHVrb2+xMOiNJaP+SU5FuQpl7KZ+f1faz6DAOZlMdgmm0SzmYjRG9/ApMdsq9mlo7Mw5mS7I/NWZNG4ropDx8lhEdQLn3kWTSKbVLMf5JotWe+vy7xGXf7Z/LNYtNKAv+YYrE88xo+N9sig7l4UexJx0eXKMsCtXbixl0x2lz1eDR7Lv9cz07ofYrZkZEsTY3RYeyvYidDtJptccI3Iv6/IjVoRziNj3fvts/9b2mWhC7RvIyYwp+kLidhYToOBSUj/LwmHCa1TYpOUOOIhd3o7alITousv0HVPtLpvJUgBJHdRg5wv9CokjCC9HBYTJFcQ8jRlprv2TOxFPfYzNrIu0c2IXYn9qOSAIqRajj7i/FPEHPoA2Rmup193Sokasf2ib/poJaTNolbqmD+vfE01zxIWpUL6dtNDzH/qrbLKb9s65vzozenmneW8OYFnt/daLD8SS+aQmPYPG3nWB/TV0V/YSYawgPBGfOcG/b2pVj/y+Z2n8VI6WnAJwFb6Y07vrMcfeKV6KD1ZATyCcuPLEEJ4Az8cSFB64tx/CrtisV7M/ZinyT9ngE7B87syEViXEntsUPFjrR3YVWsT0/8NjK613Gg+unRWTB4eThrfBGepAoe4ID6u/MlaZrOlXuJP2Skf1j568mI5AOtBIetujjM8QFx20THYFJ51AK10+8i66DD4PtK0rbap/hbIMrvF4k/vHlaikHfd5bPLh+ehAo7dp50sH+3INXrv1vhOwfNX+1ABLj7gtj3A1RYvmL5JYqpLaWuZOrZ11Ruff+Z143CNDeEPcAuLW7t9K2ljn6NBPsyxJ79kXdV7eYmAM8/0iwgH2nOuZBfzTsP4f9Xx1oK9n/EdVWG0WHWMv8EiWBRBGqZ17xiHE333ZlAOI0xIVTqXG3yXMt862i4+Qf/lUn8C81hohER9Hp4+q8huKvgpuoWtHaz4amoGz/JcJOqLXW2vCxbR7hzESnj7Gh8V4eg3AQzXe/yfNY3JnaglnYrYlT/KTaPjWiXNzvMHgHGqy4t0dhf63sgCd4dq1BfxUhcbnKZUOMO62YtqNg3MUSnBe0yacTo5RvtmDgXhXY9ZHQWUaJ9l9q1UbbQLOEtPd0zrU+PyWNooP9t2gB9dJAjfbcb7SGnEtd+YdwlWT/lXZN+aod7TzdOItjF7fLjVaZA6ULqBfKOON8LCFX0hfhYP9UKSy6EEtwXKwbzutJb7xvwtM6XSkG8vlb9XiCFT9ZBYQWfIJXJg48p7OM7V8JTqz0FdFuOO+b8HTKkQzhebNbLS88OIHPcQD4LZXqJTgIT4729x7ocUTBkapf3hoIT4lni6/onJgdhh1ktNW4jWkMXA3PsU5RlNwqsP9xRh7fbcQtlYPgpkjaVp0tfsxE9H0fPlF0gFQNwh8uryc9P/kuKX8vxAr3MMaxDnqDWJQkXzU28EEr2HSTwOO7TDgBQvvHvHE8TlnoD9+mDhKL/j0uXX6FAHcaA6wSNLjH8S7BC7iCOx0e5o01hUcgyjsGalHudcD+gwTf7NXZPwbnCdp8BRb9Ic5YKfmPqDziHQwr4YXn22nLzS62F7yAswji413pd96z2VzdsPqOlLdaOQtOxUnkQ84HvPezCOKz1f7K+Mn2Prf0GL4/60nvOIL4PGIrNah7B4HwtNpq7QyDoIAYmqlg/FAVs3TTdqAhluSi7rZvlzCIms/13tQ36t9H5krrm6xdwP5jwTiezvYn9rsuDKIKli0iaBRZDcaCsWt3eO+tvTuEzkxrFuh2sVdcgkPXIdzwNoNwibiTrcGBHV5AFcQ2rNrmiI4rT3jOUuYFhgCNYPu2FRzuERvEtn8XNkTnrgpQLixC/RkI0CCYe01eehULuMB7J28jgwTuYfBuHNHZ0qHjLZUpF8orWR/uIXsSMgMhHwPjVvSDiSm5hWsdZR2LIJr+BamTpmOB/JnykFgbPOOr8z7td18lFquuNEQXnX2hYza7R7jps6ZBDQNV21MJAUqyzTKMbZjoMGghPN5lnRLz2fQhPhKpJd7t8wpulIWJDpMdeVjucKeDtbWB58OtgnGLG62GkRwTHSZBtvxQFESTOwUlQDi5d8zbMbKAfU/nrRMEglZ3FD5JZ5MLbgBzQk/sXMfIAhMdAYJMb1fKQZcc4SkhNYXxBrDtVWHgjOcHsdfvI0ZiG0Yt5ukUBkSDGuphh8lGckx0yuSCGN0tVv3RMKiY6BQIzpAo19S1Kk8YRmtMdMqFFBsjWevaMCiY6BQKIyAvSq4dw9iGiU7ZUPIX5Zhq1XhDmOiUjd0wNorDRKdsTHSM4jDRMQwjKiY6ZWOHwkZxmOiUjX39bRSHiU7ZUETHYq+MpJjoFAou+VHSmRaZaNzYH0x0yoUUvJk6AbphmOiUC0V0cqk/b7xhTHQKBCVwKFsr83KM5JjoFEZQQZWCiY6RHBOd8rghejmrHIr/G4aJTkGgzhY1J44JjpEFJjqF0LGiaB2aCeMNozV/2qvKG9S8ugkK0VO4t4TsRi6Y6OSPF4sjZi9F63AZBgfbXuXPiJiEveLOLgQaOWGikznYFg2JwrOyyp5GbpjoFACEhyIeF1a/3MgNq/BZEAfXTz704VvLHvttlZUSNrLDPJ2CWE96/lus8xY9Xlo1TyNXzNMpkIY7O/4cZ2DbKiNXzNMpkPWk572Y25qee8EZmuAYOWOiUygQnrug90sIjl0CNLLGLgeWzRiR4z4b4Ag1zg0ja+xMxzCMeDjn/gOfmMxfrwK3DwAAAABJRU5ErkJggg==", - "ICON": "logoSquare.png", - "APP_ID": "a569f8fb0309417780b793786b534a86", - "PROJECT_ID": "49c705c1c9efb71000d7", - "FRONTEND_ENDPOINT": "https://app-builder-core-live-streaming-light-git-preprod-agoraio.vercel.app", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "MIX", - "SCREEN_SHARING": true, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform", + "APP_ID": "a32ad7a7333e40dbaccdeff5d543ef56", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, + "ENABLE_APPLE_OAUTH": false, "ENABLE_SLACK_OAUTH": false, "ENABLE_MICROSOFT_OAUTH": false, - "ENABLE_APPLE_OAUTH": false, "GOOGLE_CLIENT_ID": "", "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROFILE": "720p_3", - "SCREEN_SHARE_PROFILE": "1080p_2", + "PROJECT_ID": "2b8279fa91bda33fcf84", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", + "BUCKET_NAME": "appbuilder-dev-qa-test-recording", + "BUCKET_ACCESS_KEY": "", + "BUCKET_ACCESS_SECRET": "", + "GOOGLE_CLIENT_SECRET": "", + "MICROSOFT_CLIENT_SECRET": "", + "SLACK_CLIENT_SECRET": "", + "APPLE_PRIVATE_KEY": "", + "APPLE_KEY_ID": "", + "APPLE_TEAM_ID": "", + "PSTN_EMAIL": "", + "PSTN_ACCOUNT": "", + "PSTN_PASSWORD": "", + "RECORDING_REGION": 3, + "GEO_FENCING": true, + "LOG_ENABLED": true, "EVENT_MODE": true, "RAISE_HAND": true, - "LOG_ENABLED": true, - "GEO_FENCING": true, - "GEO_FENCING_INCLUDE_AREA": "GLOBAL", - "GEO_FENCING_EXCLUDE_AREA": "CHINA", "AUDIO_ROOM": false, + "PRODUCT_ID": "breakoutroomfeaturetesting", + "APP_NAME": "BreakoutRoomFeatureTesting", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "2b8279fa91bda33fcf84-7y25qrqsf-agoraio.vercel.app", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1610292", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "BG": "https://dbudicf5k4as1.cloudfront.net/10/Artboard.png", + "PRIMARY_FONT_COLOR": "#363636", + "SECONDARY_FONT_COLOR": "#FFFFFF", + "SENTRY_DSN": "", + "PROFILE": "480p_8", + "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, + "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", "SECONDARY_ACTION_COLOR": "#19394D", "FONT_COLOR": "#333333", - "BG": "https://dbudicf5k4as1.cloudfront.net/10/Artboard.png", "BACKGROUND_COLOR": "#FFFFFF", "VIDEO_AUDIO_TILE_COLOR": "#222222", "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#80808080", @@ -54,30 +82,24 @@ "CARD_LAYER_4_COLOR": "#FFFFFF", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_TEXT": true, "ICON_BG_COLOR": "#EBF1F5", "TOOLBAR_COLOR": "#FFFFFF00", "ACTIVE_SPEAKER": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_IDP_AUTH": false, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, + "WHITEBOARD_APPIDENTIFIER": "WUjVACgwEe2QlOX96Oc4TA/DXlhL5JAksoOSQ", + "WHITEBOARD_REGION": "us-sv", "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, "ENABLE_WHITEBOARD": true, - "ENABLE_WHITEBOARD_FILE_UPLOAD": false, - "ENABLE_WAITING_ROOM": true, - "WHITEBOARD_APPIDENTIFIER": "EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "41754367", - "CHAT_APP_NAME": "1042822", - "CHAT_URL": "https://a41.chat.agora.io", - "ENABLE_NOISE_CANCELLATION_BY_DEFAULT": true, - "DISABLE_LANDSCAPE_MODE": false, + "ENABLE_WHITEBOARD_FILE_UPLOAD": true, + "ENABLE_CHAT_NOTIFICATION": true, + "ENABLE_CHAT_OPTION": true, + "ENABLE_WAITING_ROOM": false, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false, - "ENABLE_TEXT_TRACKS": false + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, + "SDK_CODEC": "vp8", + "ENABLE_BREAKOUT_ROOM": false } diff --git a/voice-chat.config.light.json b/voice-chat.config.light.json index 6c0fe679e..73223b116 100644 --- a/voice-chat.config.light.json +++ b/voice-chat.config.light.json @@ -1,42 +1,70 @@ { - "PRODUCT_ID": "helloworld", - "APP_NAME": "HelloWorld", - "LOGO": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKIAAAA5CAYAAAC1U/CbAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAABFwSURBVHgB7Z0NnFTVdcDPue/NzJuFBSV+EBEFAvxsCjZVUxXUfGgNajWkiRoTg4GdXVBQSLStpmUFTWt+KthE4rK7s7uuJmqoaaOm1aZNFaI0mlhav1JTMVsMMcSA4sLMezPv3ZNzZ0H36933MW83+Nv5/37+lpl75r77cd6955577hUhBuNb6CgvC7PA9Y4hgJQAKEgydgsLXil8AV8Pm8+4LppMrndqodF8CKpk0jdogpMuT5cpMZMI6oAAoQos03j4rcX4FlTBuPZ9R0svMwNSOJXLlOEC2QS0B8rmdvtK7IFqaN0zMYP1p6Ew/9fO4f+H/t0assZPLs3wTOPEYs58AGJS10rvl+jORsTJXDdTGNBb4rq5wnwBIrSb1UrHUcr9g9CdZbWXPwooPsv/XMj/He0vib8ApM0A8h67IfX4cBLpDvdTQoqVLPcR/mjbOSMLMchspFloykWscxci0Inq4ZAQHooPlRvwfyAiVr58FkpYSEJ8hj9O1Yju4pd4iyB3U7Ex8yBEfQ53IBjygALi04TY7DTgD3zlO7mtPe8qErgACSao77jdw7fXespmJsJ8lN4V/LwF3NJH+MoS/JzTt3jotZYb0j8dtjx5N8f1X4aAJ7NsT2BBrHZ3MXf09YgwG6LTA5LW2k3m3ZVPm8jI7PU28luU6ycTWRHNtvI8fgX/lmusFDkx5XsXfM3OieOi/CKTLy8Awq9y3U6G6PSw4t7h7IANsIbVOAQDFbEP7tgNzgSxCi5B7+B36TzNEUDf5NSzBucRShEfJzPzilyJSDdwu7wPIsIzwLNCwLriEvN+9XnSt2jC/gLdh4IueEeIFdH0y2BCniaVyNsAiJdV0dPTQGCX1eGdbpbEavdtuRwGKmFkMp30JZByLVexHkYM+cOwkqqdHHS/giS+zA0at6mmoZRft6bQn0FLscm+MtsDMeCHr7B6YbfNE7D6nMkTzxReVxwFUtR10DHeduriQejcuO+7GvFIwn2ZvHeSUxB/s7/grkEhLhgqNwzZPM0nkN8F7RQcmV0++YUaEVWjSKD7gCqj4EjyWyiXPhxGGbijZyLIfwP1wiUFwmus1NcWc/gPOrHhRsR3kdxGJo+A8mZdHroRMdvhXiIJ21hgIiQEj9hvcX6HDUngEVEM/q4u714gyVMjQpJKCNXkpxpdEv1oFJSQeMq4JpQSttFsVsInIUklrJQApvIgsKky8sfOQjwUpIQ6ePS6iggfSFIJFcMq4QEGKKLVQtM8wk1s52TgkIEQDI9HB5oBIwiv/N5EFBcftGV0ZFtoCorKSJj0y/oOPFWvt/LFs2L8VNvhQaS7nBP49xtgRGxvf95VxFZKQYp+yPZAHRxCWHn1ZuOfwAiDAtNc918EChIJSknlDYi0mIlXqPS36lvfPgJGCTUQCc98FEZZCRXvKKJleq0hR53dRPI2cuV5WBbH1lmCh28xjb87l4u/kTsqvE8rAF6xX85//jpIjkezAo/iD/L0/TkwStPrCmJipiQmeeSdznbJ1SzxUwjOZJz05COHt5J2OrI6YTX/mQVB2fXZQ3fwqvEi1U7jUUww9hUn81B3JiffAsqjEJzJ1LIx7uuQHHuAJHsb5DkiI44ZkpqmjRDC1OC6qT6+CYQ8W6CYonSAbZoPsElwHrdjN6fthohUND/dRXOFJ58LFCa57rCjzebXL8KCr1DrTi7X5Os559UQDt/FCq+2t3OtZwQUaiuWjEuKV+JOnRi7MS5mm6490O4haLYbjeHtq06aannyZa5bVl8kuDNjiuYgh3g2763gTr0TAuAOvsjJ4SP9v9MvVoYpE8j14503b3xj+VH7hkvvW2HLhwMzknCT/StxM7uZXD8R5ch30WrmFfNVEK5wfYsVIeWXg2TZz7Wy2Ji6TquEiqVTCtyRzfyvr0AVpDvdzwQqIUC3LYyzg5RQUVKrUBQnsftI33kIq6CLrOGSLA8+H6SEnH5DsdG4JsyuTDFnbBCSLuR6FvRZUmD/+MGzRRklXVrMpa71U8LKM4iWafNRIzznYzcZN+qUULG/cfwuJ2eyq67PjRQGAbxdx0/5vE6IK/MlZwl+AyLAo9wtXLvFEBPeefmiVoCgxW4Qi3k7yYaQ8M7Dq+CiWgDotiEnpV3v/GFTUC7V/I5997LZbjC+BhEoNJnfZ0VboornL0UfTW90PgjxWFlsMjfpBKx2ms59db5GhKQU5wblMxhui7XcT2vDyIp0yjuH/6Z8JRC6nEbj7yEGdoN5NxFF3r6Cu+hw7lZdw/SkHLGaRzeCqGVaijt4FNK+/ULgpYO/y3bQ6aCznxCfLjWmYrlMio3md7irb9XJiLT5KYhON/ddS5AQGXChNp3k7eUm/AnEgGfHNdw4TwTJcZvDOToB8kSkN3zIA8BYBxHJmO7Jqmf90jmhpXcFRjaID8Kj0MPsJ9vuKyBhiNtESnceaCBP/h1UgS3NdfxW7ffNX6L2+cPBo9htYeTQ8z6hSSYBZqTZcJgnfDVIgrch4QT/ZHrGacKfQxUUG/HH7KANXAgNKJQQ2v1aiSL6KDsIFN69/okwWRnc/b/i1eGp4M8up8l4BKphKe4V5B+0wK/lSRABVuptpSZ8MZSswBP886F/4T78JVSB/UtQ7i7twMGLFZztX0DaAgkgAJ+MIs+rxOP90+DViq1XLQg/0iWXIH3swOdWpmafMtG2ylq5SiS4/nvcRJPhHhoHIeEVcLgXYw2ZXPLpfskCcTNUyxqUbAf/u05ErZr9vfAevQYJIDGa4vAI6uvE5Qol46d0TG2ZBIiBuyboHzggkKqaNQ7Cnolf6dKz+8PvmJA0Q9l09e/vVXn6mkGeFNshAaQkbd1ExQTwAw0HkoANqCji/Ib6jy4IHiQA1oMb6QdS004SItXPD/RA7wEwI+x4SDdU/aSoN2AUYNNCq0uCAHs1P54MSRSC8KhIPwDSGe3HQhLsc3VBq1x3Y6DPDcXbfrK8qxArzGrIMw393rUloRcSZn/Jf4GkQCwfCUmAOEWXzItmucsvkVeWyezxCjwlkjyQr3HML8eMoG24MEgD5+rSyYSBTnKSvoY/D9/zIQkknqFJLbzJCxpImuXILxy+4ZeMYPwxJAAPeCfq0tkWNfzD4RHmqbMgUA0tveww1zbwUAxTZ5ekC+idCVXCq+DzfBMJHKdnYAAE+0Of8RPn+XJGppviRLAPzujj/on0PIwY0r+9CRbygkZAFVhdNI3b6I90Muzbpaf9Ennb57BCVl4N1RTCrGtUf6L8Bh3QrrBQwF9CFWTuKs5gM3Sh/wNo65CQfYF6D4ILseMHFVaHu4j/+K5eed/2WRghSMV6+j/46OwU71KoAt6WuSFIRqRlWbvRzT6m6ypbQDFQYUU8bV0PEenbO9ZFzOCZmby3EuLA9oZIpf9JK4MwJCbRwTe3cF187UQkuUwdnIIYqHbiWUO7Fca7G9+FEQIN85916YS4Xp3agxjUddJJvPhsCpITvU0Wux7oP/0E1KjIQ/fj2c7CVIiA1U3HgUmb2aYbDzGQhNqG56H+tnS7++cQkUyH10YIWnsF3NK/Dvmu4chenp8e1f4OxP1Wno6HCBzWxe2bpqDjBj12Y+o/YISwl+BmtuF0TuvJZNAj9d0UaVFmtRene1KGKnff3C/hm1ophONJZrao8HgIQaqdToOyt4W1OHbwaKksWrlxdMZ5ig3cTaxYfwEhqL+Pjsjk3Yd5igs4vEX32kvrdgybQkLfTgDHsNTWrIocCoE6DmtL+RRnPFMnxy9d0HOrhv2zrbp0NmVOLpfl01ZLcRqEINNWPgcw/VTY4wZ9fik2RjNT5E9CbSMh3M3eyW47Z24eEHTQSilLuPP5O7YJ8TKA0D4v/3jEvKfCycJEb7zAfrOvpTLpx3qvGLgHPb6F5rhp+Wm2g1axEuodwsSWnluapTuzksnL73GnfRIC4OnsQbYr25xe2ALX4Ls+NG5r61j3DBZYxA7sy3jGqQso0yt2lrc8L8cBZkFQPKIKXHaWpR6DsHRQvUXeS9xtQe4x5cft9ki0l3n7dkCKOvtcD7ww9VYh4vkQlv7nmit2Skr+DEIuLNQGPXesWsnZ/OF9hJWVUZwjnv6n+LhiVj2xwtOHISyIL3F5dvNLlSVSB9wp/LkSFcYVEEFTOWIp5csRTI4yd8rzXBZuL5gggY6PcqbEtN05+1ZkhriOAgNjDfExe3Fw1Et/xrWVP+EJEVp5WQf2ct1U2cr835EkWQfiHDUZfMA+2+6tYvvpDhhdtMdJMx00A8l7Ju7Z3LCoEcxpEBeHkbXy7he4PCokfmTPdpC8mV+M5uGSgkdEMdtZhv8HEeFZSM1AzTCaDD5OWlRxh1WGMyWNCnBwhXER/7MAI8eLWaFMinCwWXIvv/nXwcjS7qeEIXg1jhIqeFC4kUfvNhhlhjgq7aUpdVjpJjiEcJfgVhelipv8NSSNpCdTKfGRqBcuFRuM9expvLYSv54w6jin3SCWQkxIQlXxg06jWEYgbodRZFiPuXorUNDnlA0ACaDOOwgQy7mJX4aYmAVXKWHoYwFhIQNf7l0EeyAGxSZjPYHxyaROLnI7OazW1xVzxtVxos8P0OOURQdUBZKTQ+WNuBEgmSATrtxvePNEbSIM65Hw3bpRB82xLD7EjRx44Fz/fHxIkJhbyOFdbLYvgDDHKAdh3VmcDtnME5D0rQpQifRpsDrl3crRDTFQp+t4P/YM/nV10xnREzwKneI0Ro9o78cOdtef3bd/XD08IN3Ei56Z/FL8AOLC7gpWgg6zKOaUGs2HyBZ/CsMoY6jGT+edOQLNVZyhCikPjn7BSjRuOyvgJlbAbf2Tsp00lXcJ1nIBv3jgOEDg3TcV53hJ+SUxkrM4Cjz83O7kjFA+ST8qO0kpdzlvRy3kms0M8cy3BMG3IS3uKV6Bz0AE+i9WqG+m2GA6Yt3+5Zi8+QKVfptHnsc7JLxHjxQmmup11oM8mqKV6zYggKTioUl7t/AK8bOVL8JcSzeYVHvpNBCpEwzyVMSJcnBnePPXYd/a81LScwbIZws7U9uCrldTOxDkwQIwvHOdnPnpoOdmumgWuPK/gtwm3Ckuy+zl9zAd3Z1Ei9RCBBJAvbzsQzmRXVynsIvjD9kYHc8bB8rN8RIPvs+ZKe/H++emXoRTsAwxOKCIt3I9N2dNcX+1l4qGhgtvtbnzKWXORSlP5c8zWJHUFTUFJPxvHv5ekBlja2kR/iwoK+XQBwMuQPQ+OLLuh4QxO8pnGKSmwiGOabbxZCsK+H5xn/nsQQeyutm2ZHhnCoHKNrk8KH91/006LWYNdorXGHneU4qosDrKH+OpTzld0+qzulXBkmJ1UKxeppU+gEbl4iRtAAePqLfyFP1XUGNUec8pooIdyldw0bukgNWlJerm2JDk906ycNxjbJvodmr2WYaYOmpTXY0KVQU8/r5gO66bR8WPR1JCRW7iHts0VECs7uDUeNv1Am3WGsnynlREhd0YbR/1HZT9R7JBJ8IukDi3KtSogvesIlYDb509ob8Gg+bF9SvWiMeYVEQF+1l9o7RZAw/P5mEK1Bg1xqwiCkNoL+90oZzMsdUaoRi7I2IZtPe5iITOKtcIx5hVRPZC6uvuUjKb/TVCMWYVET1XawOiYWpvQKiRLGNWEaUU+vM5aO+AGqPG2J2aBehO871h57LJ3DpWIxQmjEEOHIT3PdtMI3irQo3hGXMj4sRvq/u5RbdOBqX7HagxqowpRVRHQZ0iPQX6SO89dsmu7hriGpEZM1OzivKWZakuUgqK8v5HWDGhFo84yoyZEdHOwk5CDLjaDd+AcilaRE+NRBg7U/Ml6DmA6mTiNj8RBLwm7v+0u0Z1jK3FSgP2OoZQkTVbh6QRrC3m8AGo8Xth7PkR1f8yLW1c1v8sMrtrvmfvFIfUpQJjjTEbc1e5U0d6TwLio7YnlsHSeKfpatSomsolmTUOCX4Hn8i63ZAyQc4AAAAASUVORK5CYII=", - "ICON": "logoSquare.png", - "APP_ID": "a569f8fb0309417780b793786b534a86", - "PROJECT_ID": "49c705c1c9efb71000d7", - "FRONTEND_ENDPOINT": "https://app-builder-core-voice-chat-light-git-preprod-agoraio.vercel.app", - "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", - "PSTN": true, - "PRECALL": true, - "CHAT": true, - "CLOUD_RECORDING": true, - "RECORDING_MODE": "MIX", - "SCREEN_SHARING": false, - "LANDING_SUB_HEADING": "The Real-Time Engagement Platform", + "APP_ID": "a32ad7a7333e40dbaccdeff5d543ef56", "ENCRYPTION_ENABLED": true, "ENABLE_GOOGLE_OAUTH": false, + "ENABLE_APPLE_OAUTH": false, "ENABLE_SLACK_OAUTH": false, "ENABLE_MICROSOFT_OAUTH": false, - "ENABLE_APPLE_OAUTH": false, "GOOGLE_CLIENT_ID": "", "MICROSOFT_CLIENT_ID": "", "SLACK_CLIENT_ID": "", "APPLE_CLIENT_ID": "", - "PROFILE": "720p_3", - "SCREEN_SHARE_PROFILE": "1080p_2", + "PROJECT_ID": "2b8279fa91bda33fcf84", + "RECORDING_MODE": "MIX", + "APP_CERTIFICATE": "", + "CUSTOMER_ID": "", + "CUSTOMER_CERTIFICATE": "", + "BUCKET_NAME": "appbuilder-dev-qa-test-recording", + "BUCKET_ACCESS_KEY": "", + "BUCKET_ACCESS_SECRET": "", + "GOOGLE_CLIENT_SECRET": "", + "MICROSOFT_CLIENT_SECRET": "", + "SLACK_CLIENT_SECRET": "", + "APPLE_PRIVATE_KEY": "", + "APPLE_KEY_ID": "", + "APPLE_TEAM_ID": "", + "PSTN_EMAIL": "", + "PSTN_ACCOUNT": "", + "PSTN_PASSWORD": "", + "RECORDING_REGION": 3, + "GEO_FENCING": true, + "LOG_ENABLED": true, "EVENT_MODE": false, "RAISE_HAND": false, - "LOG_ENABLED": true, - "GEO_FENCING": true, - "GEO_FENCING_INCLUDE_AREA": "GLOBAL", - "GEO_FENCING_EXCLUDE_AREA": "CHINA", "AUDIO_ROOM": true, + "PRODUCT_ID": "breakoutroomfeaturetesting", + "APP_NAME": "BreakoutRoomFeatureTesting", + "LOGO": "https://dl-prod.rteappbuilder.com/tluafed/5TWpUqsdC0BH6lPJ.jpeg", + "ICON": "", + "PRIMARY_COLOR": "#00AEFC", + "FRONTEND_ENDPOINT": "2b8279fa91bda33fcf84-7y25qrqsf-agoraio.vercel.app", + "BACKEND_ENDPOINT": "https://managedservices-preprod.rteappbuilder.com", + "PSTN": false, + "PRECALL": true, + "CHAT": true, + "CHAT_ORG_NAME": "61394961", + "CHAT_APP_NAME": "1610292", + "CHAT_URL": "https://a61.chat.agora.io", + "CLOUD_RECORDING": true, + "SCREEN_SHARING": true, + "LANDING_SUB_HEADING": "The Real-Time Engagement Platform for meaningful human connections.", + "BG": "https://dbudicf5k4as1.cloudfront.net/10/Artboard.png", + "PRIMARY_FONT_COLOR": "#363636", + "SECONDARY_FONT_COLOR": "#FFFFFF", + "SENTRY_DSN": "", + "PROFILE": "480p_8", + "SCREEN_SHARE_PROFILE": "1080p_2", + "DISABLE_LANDSCAPE_MODE": true, + "ENABLE_IDP_AUTH": true, + "ENABLE_TOKEN_AUTH": false, + "ENABLE_STT": true, + "ENABLE_TEXT_TRACKS": false, + "ENABLE_CONVERSATIONAL_AI": false, + "ICON_TEXT": true, "PRIMARY_ACTION_BRAND_COLOR": "#099DFD", "PRIMARY_ACTION_TEXT_COLOR": "#FFFFFF", "SECONDARY_ACTION_COLOR": "#19394D", "FONT_COLOR": "#333333", - "BG": "https://dbudicf5k4as1.cloudfront.net/10/Artboard.png", "BACKGROUND_COLOR": "#FFFFFF", "VIDEO_AUDIO_TILE_COLOR": "#222222", "VIDEO_AUDIO_TILE_OVERLAY_COLOR": "#80808080", @@ -54,30 +82,24 @@ "CARD_LAYER_4_COLOR": "#FFFFFF", "CARD_LAYER_5_COLOR": "#808080", "HARD_CODED_BLACK_COLOR": "#000000", - "ICON_TEXT": true, "ICON_BG_COLOR": "#EBF1F5", "TOOLBAR_COLOR": "#FFFFFF00", "ACTIVE_SPEAKER": true, - "ENABLE_TOKEN_AUTH": false, - "ENABLE_IDP_AUTH": false, - "ENABLE_STT": true, - "ENABLE_CAPTION": true, - "ENABLE_MEETING_TRANSCRIPT": true, + "WHITEBOARD_APPIDENTIFIER": "WUjVACgwEe2QlOX96Oc4TA/DXlhL5JAksoOSQ", + "WHITEBOARD_REGION": "us-sv", "ENABLE_NOISE_CANCELLATION": true, "ENABLE_VIRTUAL_BACKGROUND": true, "ENABLE_WHITEBOARD": true, - "ENABLE_WHITEBOARD_FILE_UPLOAD": false, - "ENABLE_WAITING_ROOM": true, - "WHITEBOARD_APPIDENTIFIER": "EEJBQPVbEe2Bao8ZShuoHQ/hgB5eo0qcDbVig", - "WHITEBOARD_REGION": "us-sv", - "CHAT_ORG_NAME": "41754367", - "CHAT_APP_NAME": "1042822", - "CHAT_URL": "https://a41.chat.agora.io", - "ENABLE_NOISE_CANCELLATION_BY_DEFAULT": true, - "DISABLE_LANDSCAPE_MODE": false, + "ENABLE_WHITEBOARD_FILE_UPLOAD": true, + "ENABLE_CHAT_NOTIFICATION": true, + "ENABLE_CHAT_OPTION": true, + "ENABLE_WAITING_ROOM": false, + "ENABLE_WAITING_ROOM_AUTO_APPROVAL": false, + "ENABLE_WAITING_ROOM_AUTO_REQUEST": false, "STT_AUTO_START": false, "CLOUD_RECORDING_AUTO_START": false, - "ENABLE_SPOTLIGHT": false, - "AUTO_CONNECT_RTM": false, - "ENABLE_TEXT_TRACKS": false + "AI_LAYOUT": "LAYOUT_TYPE_1", + "AI_AGENTS": null, + "SDK_CODEC": "vp8", + "ENABLE_BREAKOUT_ROOM": false } From 271faeedf40315af72fb6382cea5d5f0fb9754b6 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 13 Oct 2025 17:54:32 +0530 Subject: [PATCH 51/56] waiting room change --- .../context/BreakoutRoomContext.tsx | 2 +- .../src/rtm/RTMConfigureMainRoomProvider.tsx | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 8bbb49bf7..1ba43bf6e 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -753,7 +753,7 @@ const BreakoutRoomProvider = ({ console.log('supriya-api-get response', data.sts, data); if (data?.session_id) { - logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Session exits', { + logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Session exists', { sessionId: data.session_id, roomCount: data?.breakout_room?.length || 0, assignmentType: data?.assignment_type, diff --git a/template/src/rtm/RTMConfigureMainRoomProvider.tsx b/template/src/rtm/RTMConfigureMainRoomProvider.tsx index e8649ec06..f83d1a41d 100644 --- a/template/src/rtm/RTMConfigureMainRoomProvider.tsx +++ b/template/src/rtm/RTMConfigureMainRoomProvider.tsx @@ -698,10 +698,18 @@ const RTMConfigureMainRoomProvider: React.FC< useAsyncEffect(async () => { try { - if (client && isLoggedIn && callActive && currentChannel) { - registerMainChannelMessageHandler(handleMainChannelMessageEvent); - registerMainChannelStorageHandler(handleMainChannelStorageEvent); - await init(); + if (client && isLoggedIn && currentChannel) { + // RTM initialization logic: + // - Waiting room attendees: Connect immediately + // - Waiting room hosts: Wait for callActive + // - Non-waiting room: Everyone waits for callActive + const shouldInit = + callActive || ($config.ENABLE_WAITING_ROOM && !isHost); + if (shouldInit) { + registerMainChannelMessageHandler(handleMainChannelMessageEvent); + registerMainChannelStorageHandler(handleMainChannelStorageEvent); + await init(); + } } } catch (error) { logger.error(LogSource.AgoraSDK, 'Log', 'RTM init failed', {error}); @@ -728,7 +736,7 @@ const RTMConfigureMainRoomProvider: React.FC< setIsInitialQueueCompleted(false); logger.debug(LogSource.AgoraSDK, 'Log', 'RTM cleanup done'); }; - }, [client, isLoggedIn, callActive, currentChannel]); + }, [client, isLoggedIn, callActive, currentChannel, isHost]); // Provide context data to children const contextValue: RTMMainRoomData = { From 1cf9290801241b9aeb58d8f79fde6559445ea373 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Mon, 13 Oct 2025 18:10:39 +0530 Subject: [PATCH 52/56] update ui kit package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbf87e75b..6ce78b2b7 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ ], "scripts": { "vercel-build": "npm run dev-setup && cd template && npm run web:build && cd .. && npm run copy-vercel", - "uikit": "rm -rf template/agora-rn-uikit && git clone https://github.com/AgoraIO-Community/appbuilder-ui-kit.git template/agora-rn-uikit && cd template/agora-rn-uikit && git checkout appbuilder-uikit-4.0.0-beta", + "uikit": "rm -rf template/agora-rn-uikit && git clone https://github.com/AgoraIO-Community/appbuilder-ui-kit.git template/agora-rn-uikit && cd template/agora-rn-uikit && git checkout appbuilder-uikit-3.1.10", "deps": "cd template && npm i --force", "dev-setup": "npm run uikit && npm run deps && node devSetup.js", "web-build": "cd template && npm run web:build && cd .. && npm run copy-vercel", From 3090eb1c1b9bd7e92148d3eb23c022bc666beb16 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 14 Oct 2025 12:29:34 +0530 Subject: [PATCH 53/56] fix logs in rtm engine --- template/src/rtm/RTMEngine.ts | 116 +++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 29 deletions(-) diff --git a/template/src/rtm/RTMEngine.ts b/template/src/rtm/RTMEngine.ts index a63d641b2..91e683498 100644 --- a/template/src/rtm/RTMEngine.ts +++ b/template/src/rtm/RTMEngine.ts @@ -47,39 +47,56 @@ class RTMEngine { /** Sets UID and initializes the client if needed */ setLocalUID(localUID: string | number) { if (localUID == null) { - throw new Error('setLocalUID: localUID cannot be null or undefined'); + throw new Error( + '[RTMEngine] setLocalUID: localUID cannot be null or undefined', + ); } const newUID = String(localUID).trim(); if (!newUID) { - throw new Error('setLocalUID: localUID cannot be empty'); + throw new Error('[RTMEngine] setLocalUID: localUID cannot be empty'); } if (this._engine && this.localUID !== newUID) { throw new Error( - `Cannot change UID from '${this.localUID}' to '${newUID}'. Call destroy() first.`, + `[RTMEngine] setLocalUID: Cannot change UID from '${this.localUID}' to '${newUID}'. Call destroy() first.`, ); } this.localUID = newUID; if (!this._engine) { + console.info( + `[RTMEngine] setLocalUID: Initializing client for UID: ${this.localUID}`, + ); this.createClientInstance(); + } else { + console.info( + `[RTMEngine] setLocalUID: UID already initialized: ${this.localUID}`, + ); } } addChannel(name: string, channelID: string) { if (!name || typeof name !== 'string' || name.trim() === '') { - throw new Error('addChannel: name must be a non-empty string'); + throw new Error( + '[RTMEngine] addChannel: name must be a non-empty string', + ); } if ( !channelID || typeof channelID !== 'string' || channelID.trim() === '' ) { - throw new Error('addChannel: channelID must be a non-empty string'); + throw new Error( + '[RTMEngine] addChannel: channelID must be a non-empty string', + ); } + console.info( + `[RTMEngine] addChannel: Added channel '${name}' → ${channelID}`, + ); this.channelMap.set(name, channelID); this.setActiveChannelName(name); } removeChannel(name: string) { + console.info(`[RTMEngine] removeChannel: Removing channel '${name}'`); this.channelMap.delete(name); } @@ -90,7 +107,6 @@ class RTMEngine { getChannelId(name?: string): string { // Default to active channel if no name provided const channelName = name || this.activeChannelName; - console.log('supriya channelName: ', this.channelMap.get(channelName)); return this.channelMap.get(channelName) || ''; } @@ -111,16 +127,20 @@ class RTMEngine { /** Set the active channel for default operations */ setActiveChannelName(name: string): void { if (!name || typeof name !== 'string' || name.trim() === '') { - throw new Error('setActiveChannel: name must be a non-empty string'); + throw new Error( + '[RTMEngine] setActiveChannelName: name must be a non-empty string', + ); } if (!this.hasChannel(name)) { throw new Error( - `setActiveChannel: Channel '${name}' not found. Add it first with addChannel().`, + `[RTMEngine] setActiveChannelName: Channel '${name}' not found. Add it first with addChannel().`, ); } this.activeChannelName = name; - console.log( - `RTMEngine: Active channel set to '${name}' (${this.getChannelId(name)})`, + console.info( + `[RTMEngine] setActiveChannelName: Active channel set → '${name}' (${this.getChannelId( + name, + )})`, ); } @@ -152,7 +172,9 @@ class RTMEngine { private ensureEngineReady() { if (!this.isEngineReady) { - throw new Error('RTM Engine not ready. Call setLocalUID() first.'); + throw new Error( + '[RTMEngine] ensureEngineReady: not ready. Call setLocalUID() first.', + ); } } @@ -160,23 +182,30 @@ class RTMEngine { private createClientInstance() { try { if (!this.localUID || this.localUID.trim() === '') { - throw new Error('Cannot create RTM client: localUID is not set'); + throw new Error( + '[RTMEngine] createClientInstance: Cannot create RTM client: localUID is not set', + ); } if (!$config.APP_ID) { - throw new Error('Cannot create RTM client: APP_ID is not configured'); + throw new Error( + '[RTMEngine] createClientInstance: Cannot create RTM client: APP_ID is not configured', + ); } const rtmConfig = new RtmConfig({ appId: $config.APP_ID, userId: this.localUID, }); this._engine = createAgoraRtmClient(rtmConfig); + console.info( + `[RTMEngine] createClientInstance: RTM client created for UID: ${this.localUID}`, + ); } catch (error) { const contextError = new Error( - `Failed to create RTM client instance for userId: ${ + `[RTMEngine] createClientInstance: Failed to create RTM client instance for userId: ${ this.localUID }, appId: ${$config.APP_ID}. Error: ${error.message || error}`, ); - console.error('RTMEngine createClientInstance error:', contextError); + console.error('[RTMEngine] createClientInstance: error:', contextError); throw contextError; } } @@ -185,68 +214,97 @@ class RTMEngine { if (!this._engine) { return; } - console.log('supriya-rtm-lifecycle unsubscribing from all channel'); + console.group( + `[RTMEngine] destroyClientInstance: Destroying client for UID: ${this.localUID}`, + ); + console.info( + '[RTMEngine] destroyClientInstance: Unsubscribing from channels:', + this.allChannelIds, + ); // 1. Unsubscribe from all tracked channels for (const channel of this.allChannelIds) { - console.log('supriya-stt channel: ', channel); try { await this._engine.unsubscribe(channel); + console.info( + `[RTMEngine] destroyClientInstance: Unsubscribed from ${channel}`, + ); } catch (err) { - console.warn(`Failed to unsubscribe from '${channel}':`, err); + console.warn( + `[RTMEngine] destroyClientInstance: Unsubscribe failed: ${channel}`, + err, + ); } } // 2. Remove user metadata try { - console.log('supriya-rtm-lifecycle removing user metadata'); await this._engine?.storage.removeUserMetadata(); + console.info('[RTMEngine] destroyClientInstance: User metadata removed'); } catch (err) { - console.warn('Failed to remove user metadata:', err); + console.warn( + '[RTMEngine] destroyClientInstance: Failed to remove user metadata', + err, + ); } // 3. Remove all listeners try { - console.log('supriya-rtm-lifecycle remove all listeners '); this._engine.removeAllListeners?.(); - } catch { - console.warn('Failed to remove listeners:'); + console.info('[RTMEngine] destroyClientInstance: All listeners removed'); + } catch (err) { + console.warn( + '[RTMEngine] destroyClientInstance: Failed to remove listeners', + err, + ); } // 4. Logout and release resources try { await this._engine.logout(); - console.log('supriya-rtm-lifecycle logged out '); + console.info( + '[RTMEngine] destroyClientInstance: Logged out successfully', + ); if (isAndroid() || isIOS()) { this._engine.release(); + console.info( + '[RTMEngine] destroyClientInstance: Released native resources', + ); } } catch (logoutErrorState) { // Logout of Signaling const {operation, reason, errorCode} = logoutErrorState; console.log( - `${operation} supriya-rtm-lifecycle logged out failed, the error code is ${errorCode}, because of: ${reason}.`, + `[RTMEngine] destroyClientInstance: ${operation} logged out failed, the error code is ${errorCode}, because of: ${reason}.`, + ); + console.warn( + '[RTMEngine] destroyClientInstance: Logout/release failed', + logoutErrorState, ); - console.warn('RTM logout/release failed:', logoutErrorState); } } /** Fully destroy the singleton and cleanup */ public async destroy() { try { + console.info( + `[RTMEngine] destroy: Destroy called for UID: ${this.localUID}`, + ); if (!this._engine) { return; } await this.destroyClientInstance(); - console.log('supriya-rtm-lifecycle destruction completed '); + console.info('[RTMEngine] destroy: Cleanup complete'); + console.info('[RTMEngine] destroy: clearing channels', this.channelMap); this.channelMap.clear(); // Reset state this.localUID = ''; this.activeChannelName = RTM_ROOMS.MAIN; this._engine = undefined; RTMEngine._instance = null; - console.log('supriya-rtm-lifecycle setting engine instance as null'); + console.info('[RTMEngine] destroy: Singleton reset'); } catch (error) { - console.error('Error destroying RTM instance:', error); + console.error('[RTMEngine] destroy: Error during destroy:', error); // Don't re-throw - destruction should be a best-effort cleanup // Re-throwing could prevent proper cleanup in calling code } From 482fc3a9852bcffbab5b1fcae108995ebde40297 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 14 Oct 2025 12:35:52 +0530 Subject: [PATCH 54/56] supriya remove logs --- template/src/components/Controls.tsx | 1 - .../beauty-effect/useBeautyEffects.tsx | 8 -- .../breakout-room/ui/BreakoutRoomSettings.tsx | 3 - .../breakout-room/ui/BreakoutRoomView.tsx | 1 - .../raise-hand/RaiseHandProvider.tsx | 5 +- .../src/pages/video-call/VideoCallContent.tsx | 2 +- template/src/rtm-events-api/Events.ts | 4 - .../rtm/hooks/useMainRoomUserDisplayName.ts | 10 ++- template/src/rtm/rtm-presence-utils.ts | 4 +- template/src/rtm/utils.ts | 77 ++++++++----------- 10 files changed, 39 insertions(+), 76 deletions(-) diff --git a/template/src/components/Controls.tsx b/template/src/components/Controls.tsx index 3ab10d0ab..d6631c1a0 100644 --- a/template/src/components/Controls.tsx +++ b/template/src/components/Controls.tsx @@ -733,7 +733,6 @@ const MoreButton = (props: {fields: ToolbarMoreButtonDefaultFields}) => { // 9. Recording const canAccessRecording = useControlPermissionMatrix('recordingControl'); - console.log('supriya-canAccessRecording: ', canAccessRecording); if (canAccessRecording) { actionMenuitems.push({ hide: w => { diff --git a/template/src/components/beauty-effect/useBeautyEffects.tsx b/template/src/components/beauty-effect/useBeautyEffects.tsx index 5b3adbe79..48a584235 100644 --- a/template/src/components/beauty-effect/useBeautyEffects.tsx +++ b/template/src/components/beauty-effect/useBeautyEffects.tsx @@ -88,11 +88,6 @@ const BeautyEffectProvider: React.FC = ({children}) => { } if (!roomPreference?.disableVideoProcessors) { - console.log( - 'supriya-trackstatus', - localVideoTrack?.getMediaStreamTrack()?.readyState, - ); - /** * Small delay to ensure the new track is stable * when we move from main room to breakout room the track changes @@ -102,7 +97,6 @@ const BeautyEffectProvider: React.FC = ({children}) => { timeoutRef.current = setTimeout(() => { const trackStatus = localVideoTrack?.getMediaStreamTrack()?.readyState; if (trackStatus === 'live') { - console.log('supriya-trackstatus applying'); try { if ($config.ENABLE_VIRTUAL_BACKGROUND) { localVideoTrack @@ -147,13 +141,11 @@ const BeautyEffectProvider: React.FC = ({children}) => { // Proper cleanup for both processor and timeout useEffect(() => { return () => { - console.log('supriya-trackstatus cleanup'); beautyProcessor?.disable(); beautyProcessor?.unpipe?.(); if (timeoutRef.current) { clearTimeout(timeoutRef.current); timeoutRef.current = null; - console.log('supriya-trackstatus timeout cleared'); } }; }, []); diff --git a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx index 6a2a77975..e10181e69 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomSettings.tsx @@ -40,9 +40,6 @@ export default function BreakoutRoomSettings() { // Immediately call API for NO_ASSIGN strategy if (newStrategy === RoomAssignmentStrategy.NO_ASSIGN) { - console.log( - 'supriya-state-sync calling handleAssignParticipants on strategy change', - ); handleAssignParticipants(newStrategy); } }; diff --git a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx index 44a004424..0a96da579 100644 --- a/template/src/components/breakout-room/ui/BreakoutRoomView.tsx +++ b/template/src/components/breakout-room/ui/BreakoutRoomView.tsx @@ -36,7 +36,6 @@ export default function BreakoutRoomView({closeSidePanel}: Props) { permissions, isBreakoutUILocked, } = useBreakoutRoom(); - console.log('supriya-isBreakoutUILocked: ', isBreakoutUILocked); useEffect(() => { const init = async () => { diff --git a/template/src/components/raise-hand/RaiseHandProvider.tsx b/template/src/components/raise-hand/RaiseHandProvider.tsx index 2fd4cbc89..da8a66747 100644 --- a/template/src/components/raise-hand/RaiseHandProvider.tsx +++ b/template/src/components/raise-hand/RaiseHandProvider.tsx @@ -121,11 +121,10 @@ export const RaiseHandProvider: React.FC = ({ }), PersistanceLevel.Sender, ); - console.log('supriya-here outside', isInBreakoutRoute); + // 2. Send cross-room notification to main room (if in breakout room) if (isInBreakoutRoute) { try { - console.log('supriya-here inside', isInBreakoutRoute); // Get current active channel to restore later events.send( EventNames.CROSS_ROOM_RAISE_HAND_NOTIFICATION, @@ -211,7 +210,6 @@ export const RaiseHandProvider: React.FC = ({ const {payload} = data; const eventData = JSON.parse(payload); const {uid, raised, timestamp} = eventData; - console.log('supriya-here same room'); // Update raised hands state setRaisedHands(prev => ({ ...prev, @@ -240,7 +238,6 @@ export const RaiseHandProvider: React.FC = ({ const {payload} = data; const eventData = JSON.parse(payload); const {type, uid, userName, roomName} = eventData; - console.log('supriya-here cross room'); // Only show notifications for other users and only in main room if (uid !== localUid && !isInBreakoutRoute) { diff --git a/template/src/pages/video-call/VideoCallContent.tsx b/template/src/pages/video-call/VideoCallContent.tsx index 837779e56..cf336fcd0 100644 --- a/template/src/pages/video-call/VideoCallContent.tsx +++ b/template/src/pages/video-call/VideoCallContent.tsx @@ -74,7 +74,7 @@ const VideoCallContent: React.FC = props => { // Process the event payload const {payload} = evtData; const data: BreakoutChannelJoinEventPayload = JSON.parse(payload); - console.log('supriya-event Breakout room join event received', data); + console.log('Breakout room join event received', data); if (data?.data?.act === 'CHAN_JOIN') { const {channel_name, mainUser, screenShare, chat, room_name} = data.data.data; diff --git a/template/src/rtm-events-api/Events.ts b/template/src/rtm-events-api/Events.ts index e5065158e..3cb63e305 100644 --- a/template/src/rtm-events-api/Events.ts +++ b/template/src/rtm-events-api/Events.ts @@ -273,10 +273,6 @@ class Events { } const rtmAttribute = [{key: rtmPayload.evt, value: rtmPayload.value}]; - console.log( - 'supriya-channel-attrbiutes setting channel attrbiytes: ', - rtmAttribute, - ); await rtmEngine.storage.setChannelMetadata( toChannelId, nativeChannelTypeMapping.MESSAGE, diff --git a/template/src/rtm/hooks/useMainRoomUserDisplayName.ts b/template/src/rtm/hooks/useMainRoomUserDisplayName.ts index a8753bd5d..d353a299a 100644 --- a/template/src/rtm/hooks/useMainRoomUserDisplayName.ts +++ b/template/src/rtm/hooks/useMainRoomUserDisplayName.ts @@ -9,12 +9,14 @@ information visit https://appbuilder.agora.io. ********************************************* */ + import {useCallback} from 'react'; import {videoRoomUserFallbackText} from '../../language/default-labels/videoCallScreenLabels'; import {useString} from '../../utils/useString'; import {UidType} from '../../../agora-rn-uikit'; import {useRTMGlobalState} from '../RTMGlobalStateProvider'; import {useContent} from 'customization-api'; + /** * Hook to get user display names with fallback to main room RTM users * This ensures users in breakout rooms can see names of users in other rooms @@ -26,12 +28,12 @@ export const useMainRoomUserDisplayName = () => { const sanitize = (name?: string) => name?.trim() || undefined; - // 👇 useCallback ensures the returned function updates whenever - // defaultContent or mainRoomRTMUsers change + /** + * useCallback ensures the returned function updates whenever + * defaultContent or mainRoomRTMUsers change + */ return useCallback( (uid: UidType): string => { - console.log('supriya-name defaultContent', defaultContent); - console.log('supriya-name mainRoomRTMUsers', mainRoomRTMUsers); return ( sanitize(defaultContent?.[uid]?.name) || sanitize(mainRoomRTMUsers?.[uid]?.name) || diff --git a/template/src/rtm/rtm-presence-utils.ts b/template/src/rtm/rtm-presence-utils.ts index c9a35df96..487d63af0 100644 --- a/template/src/rtm/rtm-presence-utils.ts +++ b/template/src/rtm/rtm-presence-utils.ts @@ -11,9 +11,8 @@ import { } from 'agora-react-native-rtm'; import {RTMUserData} from './RTMGlobalStateProvider'; import {RECORDING_BOT_UID} from '../utils/constants'; -import {hasJsonStructure, stripRoomPrefixFromEventKey} from '../rtm/utils'; +import {hasJsonStructure} from '../rtm/utils'; import {nativeChannelTypeMapping} from '../../bridge/rtm/web/Types'; -import {PersistanceLevel} from '../rtm-events-api'; export const fetchOnlineMembersWithRetries = async ( client: RTMClient, @@ -253,7 +252,6 @@ export const fetchChannelAttributesWithRetries = async ( await client.storage .getChannelMetadata(channelName, nativeChannelTypeMapping.MESSAGE) .then(async (data: GetChannelMetadataResponse) => { - console.log('supriya-channel-attributes: ', data); for (const item of data.items) { try { const {key, value, authorUserId, updateTs} = item; diff --git a/template/src/rtm/utils.ts b/template/src/rtm/utils.ts index b8492c020..07645696a 100644 --- a/template/src/rtm/utils.ts +++ b/template/src/rtm/utils.ts @@ -1,5 +1,4 @@ import {RTM_EVENT_SCOPE} from '../rtm-events'; -import {RTM_ROOMS} from './constants'; export const hasJsonStructure = (str: string) => { if (typeof str !== 'string') { @@ -80,49 +79,33 @@ export function isEventForActiveChannel( return true; } -export function stripRoomPrefixFromEventKey( - eventKey: string, - currentRoomKey: string, -): string | null { - // Event key - if (!eventKey) { - return eventKey; - } - - // Only handle room-aware keys - if (!eventKey.startsWith(`${currentRoomKey}__`)) { - return eventKey; - } - - // Format: room____ - const parts = eventKey.split('__'); - console.log('supriya-session-attribute parts: ', parts); - const [roomKey, ...evtParts] = parts; - - console.log('supriya-session-attribute parts:', roomKey, evtParts); - - // If the roomKey matches current room, strip and return event name - if (roomKey === currentRoomKey) { - console.log( - 'supriya-session-attribute Matched current room, stripping prefix:', - roomKey, - ); - return evtParts.join('__'); - } - - // If the roomKey is "MAIN" or "BREAKOUT" but doesn't match current room → skip - if (roomKey === RTM_ROOMS.MAIN || roomKey === RTM_ROOMS.BREAKOUT) { - console.log( - 'supriya-session-attribute Prefix is MAIN/BREAKOUT but does not match, skipping', - ); - return null; - } - - // Different room → skip - console.log( - 'supriya-session-attribute Different room, skipping event:', - roomKey, - currentRoomKey, - ); - return null; -} +// export function stripRoomPrefixFromEventKey( +// eventKey: string, +// currentRoomKey: string, +// ): string | null { +// // Event key +// if (!eventKey) { +// return eventKey; +// } + +// // Only handle room-aware keys +// if (!eventKey.startsWith(`${currentRoomKey}__`)) { +// return eventKey; +// } + +// // Format: room____ +// const parts = eventKey.split('__'); +// const [roomKey, ...evtParts] = parts; + +// // If the roomKey matches current room, strip and return event name +// if (roomKey === currentRoomKey) { +// return evtParts.join('__'); +// } + +// // If the roomKey is "MAIN" or "BREAKOUT" but doesn't match current room → skip +// if (roomKey === RTM_ROOMS.MAIN || roomKey === RTM_ROOMS.BREAKOUT) { +// return null; +// } + +// return null; +// } From c1ac4a9ffb01bf8a211ff7eaac9b654f66a50da3 Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 14 Oct 2025 21:56:16 +0530 Subject: [PATCH 55/56] add logs in breakout context --- .../context/BreakoutRoomContext.tsx | 1290 ++++++++--------- 1 file changed, 579 insertions(+), 711 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index 1ba43bf6e..f5df9336b 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useRef, } from 'react'; -import {ContentInterface, UidType} from '../../../../agora-rn-uikit'; +import {UidType} from '../../../../agora-rn-uikit'; import {createHook} from 'customization-implementation'; import {randomNameGenerator} from '../../../utils'; import StorageContext from '../../StorageContext'; @@ -25,10 +25,9 @@ import { } from '../state/reducer'; import {useLocalUid} from '../../../../agora-rn-uikit'; import {useContent} from '../../../../customization-api'; -import events, {PersistanceLevel} from '../../../rtm-events-api'; +import events from '../../../rtm-events-api'; import {BreakoutRoomAction, initialBreakoutGroups} from '../state/reducer'; import {BreakoutRoomEventNames} from '../events/constants'; -import {EventNames} from '../../../rtm-events'; import {BreakoutRoomSyncStateEventPayload} from '../state/types'; import {IconsInterface} from '../../../atoms/CustomIcon'; import Toast from '../../../../react-native-toast-message'; @@ -40,7 +39,6 @@ import { RTMUserData, useRTMGlobalState, } from '../../../rtm/RTMGlobalStateProvider'; -import {useScreenshare} from '../../../subComponents/screenshare/useScreenshare'; const HOST_BROADCASTED_OPERATIONS = [ BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, @@ -130,14 +128,15 @@ const needsDeepCloning = (action: BreakoutRoomAction): boolean => { BreakoutGroupActionTypes.EXIT_GROUP, BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, - BreakoutGroupActionTypes.CLOSE_GROUP, // Safe to include - BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, // Safe to include + BreakoutGroupActionTypes.CLOSE_GROUP, + BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, BreakoutGroupActionTypes.SYNC_STATE, ]; return CLONING_REQUIRED_ACTIONS.includes(action.type as any); }; + export interface MemberDropdownOption { type: 'move-to-main' | 'move-to-room' | 'make-presenter'; icon: keyof IconsInterface; @@ -166,10 +165,9 @@ interface BreakoutRoomPermissions { const defaulBreakoutRoomPermission: BreakoutRoomPermissions = { canJoinRoom: false, canExitRoom: false, - canSwitchBetweenRooms: false, // Media controls + canSwitchBetweenRooms: false, canScreenshare: true, canRaiseHands: false, - // Room management (host only) canHostManageMainRoom: false, canAssignParticipants: false, canCreateRooms: false, @@ -177,6 +175,7 @@ const defaulBreakoutRoomPermission: BreakoutRoomPermissions = { canCloseRooms: false, canMakePresenter: false, }; + interface BreakoutRoomContextValue { mainChannelId: string; isBreakoutUILocked: boolean; @@ -264,7 +263,6 @@ const BreakoutRoomContext = React.createContext({ // Multi-host coordination handlers handleHostOperationStart: () => {}, handleHostOperationEnd: () => {}, - // Provide a safe non-null default object permissions: {...defaulBreakoutRoomPermission}, // Loading states isBreakoutUpdateInFlight: false, @@ -285,8 +283,7 @@ const BreakoutRoomProvider = ({ }) => { const {store} = useContext(StorageContext); const {defaultContent, activeUids} = useContent(); - const {mainRoomRTMUsers, customRTMMainRoomData, setCustomRTMMainRoomData} = - useRTMGlobalState(); + const {mainRoomRTMUsers} = useRTMGlobalState(); const localUid = useLocalUid(); const { data: {isHost, roomId: joinRoomId}, @@ -296,8 +293,9 @@ const BreakoutRoomProvider = ({ breakoutRoomReducer, initialBreakoutRoomState, ); - console.log('supriya-event state', state); + const [isBreakoutUpdateInFlight, setBreakoutUpdateInFlight] = useState(false); + // Parse URL to determine current mode const location = useLocation(); const searchParams = new URLSearchParams(location.search); @@ -315,9 +313,9 @@ const BreakoutRoomProvider = ({ string | undefined >(undefined); - // Timestamp Server (authoritative ordering) + // Timestamp Server const lastProcessedServerTsRef = useRef(0); - // 2Self join guard (prevent stale reverts) (when self join happens) + // Self join guard (prevent stale reverts) (when self join happens) const lastSelfJoinRef = useRef<{roomId: string; ts: number} | null>(null); // Timestamp client tracking for event ordering client side const lastSyncedTimestampRef = useRef(0); @@ -346,7 +344,7 @@ const BreakoutRoomProvider = ({ const [selfJoinRoomId, setSelfJoinRoomId] = useState(null); // Presenter - const {isScreenshareActive, stopScreenshare} = useScreenshare(); + // const {isScreenshareActive, stopScreenshare} = useScreenshare(); // const [canIPresent, setICanPresent] = useState(false); // Get presenters from custom RTM main room data (memoized to maintain stable reference) @@ -358,16 +356,28 @@ const BreakoutRoomProvider = ({ // State version tracker to force dependent hooks to re-compute const [breakoutRoomVersion, setBreakoutRoomVersion] = useState(0); - // Refs to avoid stale closures in async callbacks + // Refs to avoid stale closures in async callbacks const stateRef = useRef(state); const prevStateRef = useRef(state); const isHostRef = useRef(isHost); const defaultContentRef = useRef(defaultContent); const isMountedRef = useRef(true); + // Enhanced dispatch that tracks user actions const [lastAction, setLastAction] = useState(null); const dispatch = useCallback((action: BreakoutRoomAction) => { + // Minimal action summary for Datadog + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + `[Base DISPATCH] Action -> ${action.type}`, + { + type: action.type, + payloadKeys: Object.keys(action?.payload || {}), + }, + ); + if (needsDeepCloning(action)) { // Only deep clone when necessary prevStateRef.current = { @@ -380,7 +390,7 @@ const BreakoutRoomProvider = ({ // Shallow copy for non-participant actions prevStateRef.current = { ...stateRef.current, - breakoutGroups: [...stateRef.current.breakoutGroups], // Shallow copy + breakoutGroups: [...stateRef.current.breakoutGroups], }; } baseDispatch(action); @@ -442,6 +452,11 @@ const BreakoutRoomProvider = ({ return () => { snapshot.forEach(timeoutId => clearTimeout(timeoutId)); snapshot.clear(); + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[CLEANUP] Cleared all pending timeouts', + ); }; }, []); @@ -473,7 +488,7 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Broadcasting host operation start', + `[HOST] Broadcasting start for operation -> ${operationName}`, {operation: operationName, hostName, hostUid: localUid}, ); @@ -501,8 +516,12 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Broadcasting host operation end', - {operation: operationName, hostName, hostUid: localUid}, + `[HOST] Broadcast end for operation -> ${operationName}`, + { + operation: operationName, + hostName, + hostUid: localUid, + }, ); events.send( @@ -521,36 +540,35 @@ const BreakoutRoomProvider = ({ // Common operation lock for API-triggering actions with multi-host coordination const acquireOperationLock = useCallback( (operationName: string): boolean => { - // Check if another host is operating - console.log('supriya-state-sync acquiring lock step 1'); + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + `[LOCK] Attempt acquire for operation -> ${operationName}`, + {operationName, inFlight: isBreakoutUpdateInFlight}, + ); - // Check if API call is in progress if (isBreakoutUpdateInFlight) { - logger.log( + logger.warn( LogSource.Internals, 'BREAKOUT_ROOM', - 'Operation blocked - API call in progress', - { - blockedOperation: operationName, - currentlyInFlight: isBreakoutUpdateInFlight, - }, + `[LOCK] Blocked as (Update BreakoutRoom API in flight) for operation -> ${operationName}`, + {blockedOperation: operationName}, ); return false; } // Broadcast that this host is starting an operation - console.log( - 'supriya-state-sync broadcasting host operation start', - operationName, - ); + setBreakoutUpdateInFlight(true); broadcastHostOperationStart(operationName); logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - `Operation lock acquired for ${operationName}`, - {operation: operationName}, + `[LOCK] Acquired for operation -> ${operationName}`, + { + operationName, + }, ); return true; }, @@ -561,7 +579,7 @@ const BreakoutRoomProvider = ({ ], ); - // Update unassigned participants and remove offline users from breakout rooms + // Update unassigned participants useEffect(() => { if (!stateRef.current?.breakoutSessionId) { return; @@ -596,9 +614,6 @@ const BreakoutRoomProvider = ({ // Get additional RTM data if available for cross-room scenarios const rtmUser = mainRoomRTMUsers[uid]; const user = v || rtmUser; - - console.log('supriya-breakoutSessionId user: ', user); - // Create BreakoutRoomUser object with proper fallback const breakoutRoomUser: BreakoutRoomUser = { name: user?.name || rtmUser?.name || '', @@ -619,6 +634,16 @@ const BreakoutRoomProvider = ({ return 0; }); + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[STATE] Update unassigned participants', + { + count: filteredParticipants.length, + filteredParticipants: filteredParticipants, + }, + ); + // // Find offline users who are currently assigned to breakout rooms // const currentlyAssignedUids = new Set(); // stateRef.current.breakoutGroups.forEach(group => { @@ -645,9 +670,7 @@ const BreakoutRoomProvider = ({ // Update unassigned participants dispatch({ type: BreakoutGroupActionTypes.UPDATE_UNASSIGNED_PARTICIPANTS, - payload: { - unassignedParticipants: filteredParticipants, - }, + payload: {unassignedParticipants: filteredParticipants}, }); }, [ defaultContent, @@ -658,7 +681,7 @@ const BreakoutRoomProvider = ({ mainRoomRTMUsers, ]); - // Increment version when breakout group assignments change + // Increment Version when breakout data changes useEffect(() => { setBreakoutRoomVersion(prev => prev + 1); }, [state.breakoutGroups]); @@ -669,19 +692,23 @@ const BreakoutRoomProvider = ({ useCallback(async (): Promise => { // Skip API call if roomId is not available or if API update is in progress if (!joinRoomId?.host && !joinRoomId?.attendee) { - console.log('supriya-sync-queue: Skipping GET no roomId available'); + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[API: checkIfBreakoutRoomSessionExistsAPI] Skipped (no roomId available yet)', + ); return false; } if (isBreakoutUpdateInFlight) { - console.log('supriya-sync-queue upsert in progress: Skipping GET'); + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[API checkIfBreakoutRoomSessionExistsAPI] Skipped (upsert in progress)', + ); return false; } - console.log( - 'supriya-sync-queue calling checkIfBreakoutRoomSessionExistsAPI', - joinRoomId, - isHostRef.current, - ); + const startTime = Date.now(); const requestId = getUniqueID(); const url = `${ @@ -690,11 +717,10 @@ const BreakoutRoomProvider = ({ isHostRef.current ? joinRoomId.host : joinRoomId.attendee }`; - // Log internals for breakout room lifecycle logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Checking active session', + '[API checkIfBreakoutRoomSessionExistsAPI] current sessionId and role', { isHost: isHostRef.current, sessionId: stateRef.current.breakoutSessionId, @@ -716,7 +742,7 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Check session API cancelled - component unmounted', + '[API checkIfBreakoutRoomSessionExistsAPI] cancelled (unmounted)', {requestId}, ); return false; @@ -729,13 +755,7 @@ const BreakoutRoomProvider = ({ LogSource.NetworkRest, 'breakout-room', 'GET breakout-room session', - { - url, - method: 'GET', - status: response.status, - latency, - requestId, - }, + {url, method: 'GET', status: response.status, latency, requestId}, ); if (!response.ok) { throw new Error(`Failed with status ${response.status}`); @@ -744,44 +764,57 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'No active session found', + '[API checkIfBreakoutRoomSessionExistsAPI] No active session', ); return false; } const data = await response.json(); - console.log('supriya-api-get response', data.sts, data); if (data?.session_id) { - logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Session exists', { - sessionId: data.session_id, - roomCount: data?.breakout_room?.length || 0, - assignmentType: data?.assignment_type, - switchRoom: data?.switch_room, - }); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[API checkIfBreakoutRoomSessionExistsAPI] session exists got breakout data', + { + sessionId: data.session_id, + rooms: data?.breakout_room || 0, + roomCount: data?.breakout_room?.length || 0, + assignmentType: data?.assignment_type, + switchRoom: data?.switch_room, + }, + ); return true; } return false; - } catch (error) { + } catch (error: any) { const latency = Date.now() - startTime; - logger.log(LogSource.NetworkRest, 'breakout-room', 'API call failed', { - url, - method: 'GET', - error: error.message, - latency, - requestId, - }); + logger.error( + LogSource.NetworkRest, + 'breakout-room', + 'GET breakout-room session failed', + { + url, + method: 'GET', + error: error?.message, + latency, + requestId, + }, + ); return false; } }, [isBreakoutUpdateInFlight, joinRoomId, store.token]); + // Initial session check with delayed start useEffect(() => { if (!joinRoomId?.host && !joinRoomId?.attendee) { return; } const loadInitialData = async () => { - console.log( - 'supriya-sync-queue checkIfBreakoutRoomSessionExistsAPI called', + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[API checkIfBreakoutRoomSessionExistsAPI] will be called , inside loadInitial data', ); await checkIfBreakoutRoomSessionExistsAPI(); }; @@ -794,8 +827,13 @@ const BreakoutRoomProvider = ({ const delay = justEnteredBreakout ? 3000 : 1200; if (justEnteredBreakout) { - sessionStorage.removeItem('breakout_room_transition'); // Clear flag - console.log('Using extended delay for breakout transition'); + sessionStorage.removeItem('breakout_room_transition'); + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[INIT] Using a bit of delay after breakout transition so it gives time for user to join the call', + {delay}, + ); } const timeoutId = setTimeout(() => { @@ -807,6 +845,7 @@ const BreakoutRoomProvider = ({ }; }, [joinRoomId, checkIfBreakoutRoomSessionExistsAPI]); + // Upsert API const upsertBreakoutRoomAPI = useCallback( async (type: 'START' | 'UPDATE' = 'START', retryCount = 0) => { type UpsertPayload = { @@ -822,11 +861,10 @@ const BreakoutRoomProvider = ({ const requestId = getUniqueID(); const url = `${$config.BACKEND_ENDPOINT}/v1/channel/breakout-room`; - // Log internals for lifecycle logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - `Upsert API called - ${type}`, + `[API upsertBreakoutRoomAPI] Upsert start called with intent ->(${type})`, { type, isHost: isHostRef.current, @@ -877,21 +915,18 @@ const BreakoutRoomProvider = ({ body: JSON.stringify(payload), }); - // Guard against component unmount after fetch if (!isMountedRef.current) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Upsert API cancelled - component unmounted after fetch', + '[API upsertBreakoutRoomAPI] Upsert cancelled (unmounted)', {type, requestId}, ); return; } - const endRequestTs = Date.now(); - const latency = endRequestTs - startReqTs; + const latency = Date.now() - startReqTs; - // Log network request logger.log( LogSource.NetworkRest, 'breakout-room', @@ -910,12 +945,11 @@ const BreakoutRoomProvider = ({ if (!response.ok) { const msg = await response.text(); - // 🛡️ Guard against component unmount after error text parsing if (!isMountedRef.current) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Error text parsing cancelled - component unmounted', + '[API upsertBreakoutRoomAPI] Error text parsing cancelled (unmounted)', {type, status: response.status, requestId}, ); return; @@ -924,14 +958,12 @@ const BreakoutRoomProvider = ({ throw new Error(`Breakout room creation failed: ${msg}`); } else { const data = await response.json(); - console.log('supriya-api-upsert response', data.sts, data); - // 🛡️ Guard against component unmount after JSON parsing if (!isMountedRef.current) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Upsert API success cancelled - component unmounted after parsing', + '[API upsertBreakoutRoomAPI] Upsert success cancelled (unmounted)', {type, requestId}, ); return; @@ -940,7 +972,7 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - `Upsert API success - ${type}`, + `[API upsertBreakoutRoomAPI] Upsert success called with intent -> (${type})`, { type, newSessionId: data?.session_id, @@ -956,23 +988,23 @@ const BreakoutRoomProvider = ({ }); } } - } catch (err) { + } catch (err: any) { const latency = Date.now() - startReqTs; const maxRetries = 3; const isRetriableError = - err.name === 'TypeError' || // Network errors - err.message.includes('fetch') || - err.message.includes('timeout') || - err.response?.status >= 500; // Server errors + err?.name === 'TypeError' || // Network errors + err?.message?.includes('fetch') || + err?.message?.includes('timeout') || + err?.response?.status >= 500; // Server errors logger.log( LogSource.NetworkRest, 'breakout-room', - 'Upsert API failed', + 'POST breakout-room upsert failed', { url, method: 'POST', - error: err.message, + error: err?.message, latency, requestId, type, @@ -982,39 +1014,33 @@ const BreakoutRoomProvider = ({ }, ); - // 🛡️ Retry logic for network/server errors + // Retry logic for network/server errors if (retryCount < maxRetries && isRetriableError) { - const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 5000); // Exponential backoff, max 5s - + const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 5000); logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - `Retrying upsert API in ${retryDelay}ms`, + `[API upsertBreakoutRoomAPI] Retrying upsert in ${retryDelay}ms`, {retryCount: retryCount + 1, maxRetries, type}, ); - // Don't clear polling/selfJoinRoomId on retry safeSetTimeout(() => { - // 🛡️ Guard against component unmount during retry delay if (!isMountedRef.current) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'API retry cancelled - component unmounted', + '[API upsertBreakoutRoomAPI] Retry cancelled (unmounted)', {type, retryCount: retryCount + 1}, ); return; } - console.log('supriya-state-sync calling upsertBreakoutRoomAPI 941'); upsertBreakoutRoomAPI(type, retryCount + 1); }, retryDelay); return; // Don't execute finally block on retry } - // 🛡️ Only clear state if we're not retrying setSelfJoinRoomId(null); } finally { - // 🛡️ Only clear state on successful completion (not on retry) if (retryCount === 0) { setSelfJoinRoomId(null); } @@ -1032,6 +1058,12 @@ const BreakoutRoomProvider = ({ const setManualAssignments = useCallback( (assignments: ManualParticipantAssignment[]) => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[ACTION] Set manual assignments', + {count: assignments.length}, + ); dispatch({ type: BreakoutGroupActionTypes.SET_MANUAL_ASSIGNMENTS, payload: {assignments}, @@ -1041,41 +1073,35 @@ const BreakoutRoomProvider = ({ ); const clearManualAssignments = useCallback(() => { + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[ACTION] Clear manual assignments', + ); dispatch({ type: BreakoutGroupActionTypes.CLEAR_MANUAL_ASSIGNMENTS, }); }, [dispatch]); const toggleRoomSwitchingAllowed = (value: boolean) => { - console.log( - 'supriya-state-sync toggleRoomSwitchingAllowed value is', - value, - ); if (!acquireOperationLock('SET_ALLOW_PEOPLE_TO_SWITCH_ROOM')) { - console.log('supriya-state-sync lock acquired'); return; } logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Switch rooms permission changed', + `[ACTION] Toggle room switching with value ${value}`, { previousValue: stateRef.current.canUserSwitchRoom, newValue: value, isHost: isHostRef.current, - roomCount: stateRef.current.breakoutGroups.length, }, ); - console.log( - 'supriya-state-sync dispatching SET_ALLOW_PEOPLE_TO_SWITCH_ROOM', - ); dispatch({ type: BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, - payload: { - canUserSwitchRoom: value, - }, + payload: {canUserSwitchRoom: value}, }); }; @@ -1087,7 +1113,7 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Creating new breakout room', + '[ACTION] Create new breakout room', { currentRoomCount: stateRef.current.breakoutGroups.length, isHost: isHostRef.current, @@ -1095,19 +1121,22 @@ const BreakoutRoomProvider = ({ }, ); - dispatch({ - type: BreakoutGroupActionTypes.CREATE_GROUP, - }); + dispatch({type: BreakoutGroupActionTypes.CREATE_GROUP}); }; const handleAssignParticipants = (strategy: RoomAssignmentStrategy) => { - console.log('supriya-assign', stateRef.current); if (stateRef.current.breakoutGroups.length === 0) { Toast.show({ type: 'info', text1: 'No breakout rooms found.', visibilityTime: 3000, }); + logger.warn( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[ACTION] Assign participants blocked (no rooms)', + {strategy}, + ); return; } @@ -1126,41 +1155,43 @@ const BreakoutRoomProvider = ({ ? 'No other participants to assign. (Host is excluded from auto-assignment)' : 'No participants left to assign.'; - Toast.show({ - type: 'info', - text1: message, - visibilityTime: 3000, - }); + Toast.show({type: 'info', text1: message, visibilityTime: 3000}); + logger.warn( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[ACTION] Assign participants blocked (none available)', + {strategy}, + ); return; } + if (!acquireOperationLock(`ASSIGN_${strategy}`)) { return; } - logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Assigning participants', { - strategy, - unassignedCount: stateRef.current.unassignedParticipants.length, - roomCount: stateRef.current.breakoutGroups.length, - isHost: isHostRef.current, - }); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `[ACTION] Assign participants with strategy ${strategy}`, + { + strategy, + unassignedCount: stateRef.current.unassignedParticipants.length, + roomCount: stateRef.current.breakoutGroups.length, + isHost: isHostRef.current, + }, + ); if (strategy === RoomAssignmentStrategy.AUTO_ASSIGN) { dispatch({ type: BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, - payload: { - localUid, - }, + payload: {localUid}, }); } if (strategy === RoomAssignmentStrategy.MANUAL_ASSIGN) { - dispatch({ - type: BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, - }); + dispatch({type: BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS}); } if (strategy === RoomAssignmentStrategy.NO_ASSIGN) { - dispatch({ - type: BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, - }); + dispatch({type: BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS}); } }; @@ -1170,17 +1201,17 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Move to main room failed - no uid provided', + '[ACTION] Move to main blocked (no uid)', ); return; } - // 🛡️ Check for API operation conflicts first + // Check for API operation conflicts first if (!acquireOperationLock('MOVE_PARTICIPANT_TO_MAIN')) { return; } - // 🛡️ Use fresh state to avoid race conditions + // Use fresh state to avoid race conditions const currentState = stateRef.current; const currentGroup = currentState.breakoutGroups.find( group => @@ -1191,7 +1222,7 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Moving user to main room', + '[ACTION] Move user to main', { userId: uid, fromGroupId: currentGroup?.id, @@ -1202,21 +1233,15 @@ const BreakoutRoomProvider = ({ if (currentGroup) { dispatch({ type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, - payload: { - uid, - fromGroupId: currentGroup.id, - }, + payload: {uid, fromGroupId: currentGroup.id}, }); } - } catch (error) { - logger.log( + } catch (error: any) { + logger.error( LogSource.Internals, 'BREAKOUT_ROOM', - 'Error moving user to main room', - { - userId: uid, - error: error.message, - }, + '[ERROR] Move to main failed', + {userId: uid, error: error?.message}, ); } }; @@ -1227,18 +1252,18 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Move to group failed - no uid provided', + '[ACTION] Move to group blocked (no uid)', {toGroupId}, ); return; } - // 🛡️ Check for API operation conflicts first + // Check for API operation conflicts first if (!acquireOperationLock('MOVE_PARTICIPANT_TO_GROUP')) { return; } - // 🛡️ Use fresh state to avoid race conditions + // Use fresh state to avoid race conditions const currentState = stateRef.current; const currentGroup = currentState.breakoutGroups.find( group => @@ -1253,29 +1278,38 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Target group not found', - { - userId: uid, - toGroupId, - }, + '[ACTION] Move to group blocked (target not found)', + {userId: uid, toGroupId}, ); - return; } + // Determine if user is host + let isUserHost: boolean | undefined; + if (currentGroup) { + // User is moving from another breakout room + isUserHost = currentGroup.participants.hosts.includes(uid); + } else { + // User is moving from main room - check mainRoomRTMUsers + const rtmUser = mainRoomRTMUsers[uid]; + if (rtmUser) { + isUserHost = rtmUser.isHost === 'true'; + } + } + logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Moving user between groups', + '[ACTION] Move user between groups', { userId: uid, fromGroupId: currentGroup?.id, fromGroupName: currentGroup?.name, toGroupId, toGroupName: targetGroup.name, + isUserHost, }, ); - // // Clean up presenter status if user is switching rooms // const isPresenting = presenters.some(p => p.uid === uid); // if (isPresenting) { @@ -1308,19 +1342,6 @@ const BreakoutRoomProvider = ({ // } // } - // Check if user is a host - let isUserHost: boolean | undefined; - if (currentGroup) { - // User is moving from another breakout room - isUserHost = currentGroup.participants.hosts.includes(uid); - } else { - // User is moving from main room - check mainRoomRTMUsers - const rtmUser = mainRoomRTMUsers[uid]; - if (rtmUser) { - isUserHost = rtmUser.isHost === 'true'; - } - } - dispatch({ type: BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, payload: { @@ -1330,16 +1351,12 @@ const BreakoutRoomProvider = ({ isHost: isUserHost, }, }); - } catch (error) { - logger.log( + } catch (error: any) { + logger.error( LogSource.Internals, 'BREAKOUT_ROOM', - 'Error moving user to breakout room', - { - userId: uid, - toGroupId, - error: error.message, - }, + '[ERROR] Move to group failed', + {userId: uid, toGroupId, error: error?.message}, ); } }; @@ -1375,7 +1392,7 @@ const BreakoutRoomProvider = ({ return hosts.includes(uid) || attendees.includes(uid); })?.id ?? null; - // Permissions + // Permissions recompute useEffect(() => { if (lastSyncedSnapshotRef.current) { const current = lastSyncedSnapshotRef.current; @@ -1383,12 +1400,7 @@ const BreakoutRoomProvider = ({ const currentlyInRoom = !!findUserRoomId(localUid, current.breakout_room); const hasAvailableRooms = current.breakout_room?.length > 0; const allowAttendeeSwitch = current.switch_room; - console.log( - 'supriya-canraisehands', - !isHostRef.current && !!current.session_id && currentlyInRoom, - localUid, - current.breakout_room, - ); + const nextPermissions: BreakoutRoomPermissions = { canJoinRoom: hasAvailableRooms && (isHostRef.current || allowAttendeeSwitch), @@ -1398,11 +1410,6 @@ const BreakoutRoomProvider = ({ hasAvailableRooms && (isHostRef.current || allowAttendeeSwitch), canScreenshare: true, - // isHostRef.current - // ? true - // : currentlyInRoom - // ? canIPresent - // : true, canRaiseHands: !isHostRef.current && !!current.session_id, canAssignParticipants: isHostRef.current && !currentlyInRoom, canHostManageMainRoom: isHostRef.current, @@ -1412,6 +1419,19 @@ const BreakoutRoomProvider = ({ isHostRef.current && hasAvailableRooms && !!current.session_id, canMakePresenter: isHostRef.current, }; + + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[PERMISSIONS] Recomputed', + { + currentlyInRoom, + hasAvailableRooms, + allowAttendeeSwitch, + isHost: isHostRef.current, + }, + ); + setPermissions(nextPermissions); } }, [breakoutRoomVersion, isBreakoutMode, localUid]); @@ -1424,12 +1444,8 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Join room blocked - no permission at call time', - { - toRoomId, - permissionAtCallTime, - currentPermission: permissions.canJoinRoom, - }, + '[ACTION] Join blocked (no permission)', + {toRoomId, currentPermission: permissions.canJoinRoom}, ); return; } @@ -1437,18 +1453,26 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Join room failed - user not found', - {localUid, toRoomId}, + '[ACTION] Join blocked (no local user)', + {toRoomId}, ); return; } - logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'User joining room', { - userId: localUid, - toRoomId, - toRoomName: stateRef.current.breakoutGroups.find(r => r.id === toRoomId) - ?.name, - }); + const toRoomName = + stateRef.current.breakoutGroups.find(r => r.id === toRoomId)?.name || ''; + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `[ACTION] User joining room ${toRoomName}`, + { + userId: localUid, + toRoomId, + toRoomName: toRoomName, + }, + ); + lastSelfJoinRef.current = {roomId: toRoomId, ts: Date.now()}; moveUserIntoGroup(localUid, toRoomId); if (!isHostRef.current) { @@ -1458,16 +1482,13 @@ const BreakoutRoomProvider = ({ const exitRoom = useCallback( async (permissionAtCallTime = permissions.canExitRoom) => { - // 🛡️ Use permission passed at call time to avoid race conditions + // Use permission passed at call time to avoid race conditions if (!permissionAtCallTime) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Exit room blocked - no permission at call time', - { - permissionAtCallTime, - currentPermission: permissions.canExitRoom, - }, + '[ACTION] Exit blocked (no permission)', + {currentPermission: permissions.canExitRoom}, ); return; } @@ -1494,34 +1515,34 @@ const BreakoutRoomProvider = ({ // Use breakout-specific exit (doesn't destroy main RTM) await breakoutRoomExit(); - // 🛡️ Guard against component unmount + // Guard against component unmount if (!isMountedRef.current) { logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Exit room cancelled - component unmounted', + '[ACTION] Exit cancelled (unmounted)', {userId: localUid}, ); return; } + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[ACTION] Exit success', + {userId: localUid}, + ); } - } catch (error) { - logger.log( + } catch (error: any) { + logger.error( LogSource.Internals, 'BREAKOUT_ROOM', - 'Exit room error - fallback dispatch', - { - userId: localUid, - error: error.message, - }, + '[ERROR] Exit room failed', + {userId: localUid, error: error?.message}, ); } }, - [ - localUid, - permissions.canExitRoom, // TODO:SUP move to the method call - breakoutRoomExit, - ], + [localUid, permissions.canExitRoom, breakoutRoomExit], ); const closeRoom = (roomIdToClose: string) => { @@ -1533,14 +1554,19 @@ const BreakoutRoomProvider = ({ r => r.id === roomIdToClose, ); - logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Closing breakout room', { - roomId: roomIdToClose, - roomName: roomToClose?.name, - participantCount: - (roomToClose?.participants.hosts.length || 0) + - (roomToClose?.participants.attendees.length || 0), - isHost: isHostRef.current, - }); + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `[ACTION] Close room -> ${roomToClose?.name}`, + { + roomId: roomIdToClose, + roomName: roomToClose?.name, + participantCount: + (roomToClose?.participants.hosts.length || 0) + + (roomToClose?.participants.attendees.length || 0), + isHost: isHostRef.current, + }, + ); dispatch({ type: BreakoutGroupActionTypes.CLOSE_GROUP, @@ -1556,7 +1582,7 @@ const BreakoutRoomProvider = ({ logger.log( LogSource.Internals, 'BREAKOUT_ROOM', - 'Closing all breakout rooms', + '[ACTION] Close all rooms', { roomCount: stateRef.current.breakoutGroups.length, totalParticipants: stateRef.current.breakoutGroups.reduce( @@ -1586,7 +1612,7 @@ const BreakoutRoomProvider = ({ r => r.id === roomIdToEdit, ); - logger.log(LogSource.Internals, 'BREAKOUT_ROOM', 'Renaming breakout room', { + logger.log(LogSource.Internals, 'BREAKOUT_ROOM', '[ACTION] Rename room', { roomId: roomIdToEdit, oldName: roomToRename?.name, newName: newRoomName, @@ -1742,8 +1768,6 @@ const BreakoutRoomProvider = ({ const getRoomMemberDropdownOptions = useCallback( (memberUid: UidType) => { const options: MemberDropdownOption[] = []; - // Find which room the user is currently in - if (!memberUid) { return options; } @@ -1753,13 +1777,18 @@ const BreakoutRoomProvider = ({ group.participants.hosts.includes(memberUid) || group.participants.attendees.includes(memberUid), ); - console.log( - 'supriya-currentRoom', - currentRoom, - memberUid, - JSON.stringify(stateRef.current.breakoutGroups), + + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[UI] Member dropdown options computed', + { + memberUid, + currentRoomId: currentRoom?.id, + roomCount: stateRef.current.breakoutGroups.length, + }, ); - // Move to Main Room option + options.push({ icon: 'double-up-arrow', type: 'move-to-main', @@ -1805,409 +1834,69 @@ const BreakoutRoomProvider = ({ [breakoutRoomVersion], ); - // const handleBreakoutRoomSyncState = useCallback( - // (payload: BreakoutRoomSyncStateEventPayload['data'], timestamp) => { - // console.log( - // 'supriya-api-sync response', - // timestamp, - // JSON.stringify(payload), - // ); - - // // Skip events older than the last processed timestamp - // if (timestamp && timestamp <= lastProcessedTimestampRef.current) { - // console.log('supriya-api-sync Skipping old breakout room sync event', { - // timestamp, - // lastProcessed: lastProcessedTimestampRef.current, - // }); - // return; - // } + // ---- SYNC (EVENTS) ---- - // const {srcuid, data} = payload; - // console.log('supriya-event flow step 2', srcuid); - // console.log('supriya-event uids', srcuid, localUid); + const _handleBreakoutRoomSyncState = useCallback( + async ( + payload: BreakoutRoomSyncStateEventPayload['data'], + timestamp: number, + ) => { + const {srcuid, data} = payload; + const { + session_id, + switch_room, + breakout_room, + assignment_type, + sts = 0, + } = data; - // // if (srcuid === localUid) { - // // console.log('supriya-event flow skipping'); + logger.debug( + LogSource.Events, + 'RTM_EVENTS', + '[SYNC] Breakout sync event received', + { + srcuid, + session_id, + timestamp, + sts, + newRooms: breakout_room?.length || 0, + currentRooms: stateRef.current.breakoutGroups.length, + }, + ); - // // return; - // // } - // const {session_id, switch_room, breakout_room, assignment_type} = data; - // console.log('supriya-event-sync new data: ', data); - // console.log('supriya-event-sync old data: ', stateRef.current); + // Global server ordering + if (sts <= lastProcessedServerTsRef.current) { + logger.warn( + LogSource.Events, + 'RTM_EVENTS', + '[SYNC] Ignored out-of-order state', + {sts, lastProcessedServerTs: lastProcessedServerTsRef.current}, + ); + return; + } + lastProcessedServerTsRef.current = sts; - // logger.log( - // LogSource.Internals, - // 'BREAKOUT_ROOM', - // 'Sync state event received', - // { - // sessionId: session_id, - // incomingRoomCount: breakout_room?.length || 0, - // currentRoomCount: stateRef.current.breakoutGroups.length, - // switchRoom: switch_room, - // assignmentType: assignment_type, - // }, - // ); - - // if (isAnotherHostOperating) { - // setIsAnotherHostOperating(false); - // setCurrentOperatingHostName(undefined); - // } - // // 🛡️ BEFORE snapshot - using stateRef to avoid stale closure - // const prevGroups = stateRef.current.breakoutGroups; - // console.log('supriya-event-sync prevGroups: ', prevGroups); - // const prevSwitchRoom = stateRef.current.canUserSwitchRoom; - - // // Helpers to find membership - // const findUserRoomId = (uid: UidType, groups: BreakoutGroup[] = []) => - // groups.find(g => { - // const hosts = Array.isArray(g?.participants?.hosts) - // ? g.participants.hosts - // : []; - // const attendees = Array.isArray(g?.participants?.attendees) - // ? g.participants.attendees - // : []; - // return hosts.includes(uid) || attendees.includes(uid); - // })?.id ?? null; - - // const prevRoomId = findUserRoomId(localUid, prevGroups); - // const nextRoomId = findUserRoomId(localUid, breakout_room); - - // console.log( - // 'supriya-event-sync prevRoomId and nextRoomId: ', - // prevRoomId, - // nextRoomId, - // ); - - // console.log('supriya-event-sync 1: '); - // // Show notifications based on changes - // // 1. Switch room enabled notification - // const senderName = getDisplayName(srcuid); - // if (switch_room && !prevSwitchRoom) { - // console.log('supriya-toast 1'); - // showDeduplicatedToast('switch-room-toggle', { - // leadingIconName: 'open-room', - // type: 'info', - // text1: `Host:${senderName} has opened breakout rooms.`, - // text2: 'Please choose a room to join.', - // visibilityTime: 3000, - // }); - // } - // console.log('supriya-event-sync 2: '); - - // // 2. User joined a room (compare previous and current state) - // // The notification for this comes from the main room channel_join event - // if (prevRoomId === nextRoomId) { - // // No logic - // } - - // console.log('supriya-event-sync 3: '); - - // // 3. User was moved to main room - // if (prevRoomId && !nextRoomId) { - // const prevRoom = prevGroups.find(r => r.id === prevRoomId); - // // Distinguish "room closed" vs "moved to main" - // const roomStillExists = breakout_room.some(r => r.id === prevRoomId); - - // if (!roomStillExists) { - // showDeduplicatedToast(`current-room-closed-${prevRoomId}`, { - // leadingIconName: 'close-room', - // type: 'error', - // text1: `Host: ${senderName} has closed "${ - // prevRoom?.name || '' - // }" room. `, - // text2: 'Returning to main room...', - // visibilityTime: 3000, - // }); - // } else { - // showDeduplicatedToast(`moved-to-main-${prevRoomId}`, { - // leadingIconName: 'arrow-up', - // type: 'info', - // text1: `Host: ${senderName} has moved you to main room.`, - // visibilityTime: 3000, - // }); - // } - // // Exit breakout room and return to main room - // return exitRoom(true); - // } - - // console.log('supriya-event-sync 5: '); - - // // 5. All breakout rooms closed - // if (breakout_room.length === 0 && prevGroups.length > 0) { - // console.log('supriya-toast 5', prevRoomId, nextRoomId); - - // // Show different messages based on user's current location - // if (prevRoomId) { - // // User was in a breakout room - returning to main - // showDeduplicatedToast('all-rooms-closed', { - // leadingIconName: 'close-room', - // type: 'info', - // text1: `Host: ${senderName} has closed all breakout rooms.`, - // text2: 'Returning to the main room...', - // visibilityTime: 3000, - // }); - // return exitRoom(true); - // } else { - // // User was already in main room - just notify about closure - // showDeduplicatedToast('all-rooms-closed', { - // leadingIconName: 'close-room', - // type: 'info', - // text1: `Host: ${senderName} has closed all breakout rooms`, - // visibilityTime: 4000, - // }); - // } - // } - - // console.log('supriya-event-sync 6: '); - - // // 6) Room renamed (compare per-room names) - // prevGroups.forEach(prevRoom => { - // const after = breakout_room.find(r => r.id === prevRoom.id); - // if (after && after.name !== prevRoom.name) { - // showDeduplicatedToast(`room-renamed-${after.id}`, { - // type: 'info', - // text1: `Host: ${senderName} has renamed room "${prevRoom.name}" to "${after.name}".`, - // visibilityTime: 3000, - // }); - // } - // }); - - // console.log('supriya-event-sync 7: '); - - // // The host clicked on the room to close in which he is a part of - // if (!prevRoomId && !nextRoomId) { - // return exitRoom(true); - // } - // // Finally, apply the authoritative state - // dispatch({ - // type: BreakoutGroupActionTypes.SYNC_STATE, - // payload: { - // sessionId: session_id, - // assignmentStrategy: assignment_type, - // switchRoom: switch_room, - // rooms: breakout_room, - // }, - // }); - // // Update the last processed timestamp after successful processing - // lastProcessedTimestampRef.current = timestamp || Date.now(); - // }, - // [ - // dispatch, - // exitRoom, - // localUid, - // showDeduplicatedToast, - // isAnotherHostOperating, - // getDisplayName, - // ], - // ); - - // Multi-host coordination handlers - const handleHostOperationStart = useCallback( - (operationName: string, hostUid: UidType, hostName: string) => { - // Only process if current user is also a host and it's not their own event - console.log('supriya-state-sync host operation started', operationName); - // if (!isHostRef.current || hostUid === localUid) { - if (hostUid === localUid) { - return; - } - - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'Another host started operation - locking UI', - {operationName, hostUid, hostName}, - ); - - setCurrentOperatingHostName(hostName); - }, - [localUid], - ); - - const handleHostOperationEnd = useCallback( - (operationName: string, hostUid: UidType, hostName: string) => { - // Only process if current user is also a host and it's not their own event - console.log('supriya-state-sync host operation ended', operationName); - - // if (!isHostRef.current || hostUid === localUid) { - if (hostUid === localUid) { - return; - } - - setCurrentOperatingHostName(undefined); - }, - [localUid], - ); - - // Debounced API for performance with multi-host coordination - const debouncedUpsertAPI = useDebouncedCallback( - async (type: 'START' | 'UPDATE', operationName?: string) => { - setBreakoutUpdateInFlight(true); - - try { - console.log( - 'supriya-state-sync before calling upsertBreakoutRoomAPI 2007', - ); - - await upsertBreakoutRoomAPI(type); - console.log( - 'supriya-state-sync after calling upsertBreakoutRoomAPI 2007', - ); - console.log('supriya-state-sync operationName', operationName); - - // Broadcast operation end after successful API call - if (operationName) { - console.log( - 'supriya-state-sync broadcasting host operation end', - operationName, - ); - - broadcastHostOperationEnd(operationName); - } - } catch (error) { - logger.log( - LogSource.Internals, - 'BREAKOUT_ROOM', - 'API call failed. Reverting to previous state.', - error, - ); - - // Broadcast operation end even on failure - if (operationName) { - broadcastHostOperationEnd(operationName); - } - - // // 🔁 Rollback to last valid state - // if ( - // prevStateRef.current && - // validateRollbackState(prevStateRef.current) - // ) { - // baseDispatch({ - // type: BreakoutGroupActionTypes.SYNC_STATE, - // payload: { - // sessionId: prevStateRef.current.breakoutSessionId, - // assignmentStrategy: prevStateRef.current.assignmentStrategy, - // switchRoom: prevStateRef.current.canUserSwitchRoom, - // rooms: prevStateRef.current.breakoutGroups, - // }, - // }); - // showDeduplicatedToast('breakout-api-failure', { - // type: 'error', - // text1: 'Sync failed. Reverted to previous state.', - // }); - // } else { - // showDeduplicatedToast('breakout-api-failure-no-rollback', { - // type: 'error', - // text1: 'Sync failed. Could not rollback safely.', - // }); - // } - } finally { - setBreakoutUpdateInFlight(false); - } - }, - 500, - ); - - // Action-based API triggering - useEffect(() => { - if (!lastAction || !lastAction.type) { - return; - } - - // Actions that should trigger API calls - const API_TRIGGERING_ACTIONS = [ - BreakoutGroupActionTypes.CREATE_GROUP, - BreakoutGroupActionTypes.RENAME_GROUP, - BreakoutGroupActionTypes.CLOSE_GROUP, - BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, - BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, - BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, - BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, - BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, - BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, - BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, - BreakoutGroupActionTypes.EXIT_GROUP, - ]; - - // Host can always trigger API calls for any action - // Attendees can only trigger API when they self-join a room and switch_room is enabled - const attendeeSelfJoinAllowed = - stateRef.current.canUserSwitchRoom && - lastAction.type === BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP; - - const shouldCallAPI = - API_TRIGGERING_ACTIONS.includes(lastAction.type as any) && - (isHostRef.current || (!isHostRef.current && attendeeSelfJoinAllowed)); - - // Compute lastOperationName based on lastAction - const lastOperationName = HOST_BROADCASTED_OPERATIONS.includes( - lastAction?.type as any, - ) - ? lastAction?.type - : undefined; - - console.log( - 'supriya-state-sync shouldCallAPI', - shouldCallAPI, - lastAction.type, - lastOperationName, - ); - if (shouldCallAPI) { - debouncedUpsertAPI('UPDATE', lastOperationName); - } - }, [lastAction]); - - const _handleBreakoutRoomSyncState = useCallback( - async ( - payload: BreakoutRoomSyncStateEventPayload['data'], - timestamp: number, - ) => { - console.log( - 'supriya-sync-ordering exact response', - timestamp, - JSON.stringify(payload), - ); - const {srcuid, data} = payload; - const { - session_id, - switch_room, - breakout_room, - assignment_type, - sts = 0, - } = data; - console.log('supriya-sync-ordering Sync state event received', { - sessionId: session_id, - incomingRoom: breakout_room || [], - currentRoom: stateRef.current.breakoutGroups || [], - switchRoom: switch_room, - assignmentType: assignment_type, - }); - - // global server ordering - if (sts <= lastProcessedServerTsRef.current) { - console.log( - `supriya-sync-ordering [BreakoutSync] Ignoring out-of-order state (sts=${sts}, last=${lastProcessedServerTsRef.current})`, - ); - return; - } - lastProcessedServerTsRef.current = sts; - - // Self-join race protection — ignore stale reverts right after joining - if ( - lastSelfJoinRef.current && - Date.now() - lastSelfJoinRef.current.ts < 2000 && // 2s cooldown - !findUserRoomId(localUid, breakout_room) - ) { - console.log( - 'supriya-sync-ordering [SyncGuard] Ignoring stale sync conflicting with recent self-join to', - lastSelfJoinRef.current.roomId, - ); - return; - } + // Self-join race protection + if ( + lastSelfJoinRef.current && + Date.now() - lastSelfJoinRef.current.ts < 2000 && // 2s cooldown + !findUserRoomId(localUid, breakout_room) + ) { + logger.warn( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[SYNC] Ignored stale revert conflicting with recent self-join', + {recentJoinRoomId: lastSelfJoinRef.current.roomId}, + ); + return; + } // Local duplicate protection (client-side ordering) Skip events older than the last processed timestamp if (timestamp && timestamp <= lastSyncedTimestampRef.current) { - console.log( - 'supriya-sync-ordering Skipping old breakout room sync event', + logger.warn( + LogSource.Events, + 'RTM_EVENTS', + '[SYNC] Ignored old sync event', { timestamp, lastProcessed: lastSyncedTimestampRef.current, @@ -2216,7 +1905,6 @@ const BreakoutRoomProvider = ({ return; } - // Snapshot before applying const prevSnapshot = lastSyncedSnapshotRef?.current; const prevGroups = prevSnapshot?.breakout_room || []; const prevSwitchRoom = prevSnapshot?.switch_room ?? true; @@ -2232,15 +1920,7 @@ const BreakoutRoomProvider = ({ prevRoomId && nextRoomId && prevRoomId !== nextRoomId; const userLeftBreakoutRoom = prevRoomId && !nextRoomId; - console.log( - 'supriya-sync-ordering prevRoomId nextRoomId and new breakout_room', - prevRoomId, - nextRoomId, - breakout_room, - ); - const senderName = getDisplayName(srcuid); - console.log('supriya-senderName: ', senderName, srcuid); // ---- SCREEN SHARE CLEANUP ---- // Stop screen share if user is moving between rooms or leaving breakout @@ -2255,10 +1935,15 @@ const BreakoutRoomProvider = ({ // } // ---- PRIORITY ORDER ---- - // 1. Room closed + // 1) All rooms closed if (breakout_room.length === 0 && prevGroups.length > 0) { - console.log('supriya-sync-ordering 1. all room closed: '); - // 1. User is in breakout toom and the exits + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[SYNC][Condition 1] All rooms closed', + {prevRoomCount: prevGroups.length, srcuid, senderName}, + ); + if (prevRoomId && isBreakoutMode) { // Don't show toast if the user is the author if (srcuid !== localUid) { @@ -2290,8 +1975,6 @@ const BreakoutRoomProvider = ({ // 2. User's room deleted (they were in a room → now not) if (userLeftBreakoutRoom && isBreakoutMode) { - console.log('supriya-sync-ordering 2. they were in a room → now not: '); - const prevRoom = prevGroups.find(r => r.id === prevRoomId); const roomStillExists = breakout_room.some(r => r.id === prevRoomId); // Case A: Room deleted @@ -2322,6 +2005,13 @@ const BreakoutRoomProvider = ({ } } + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[SYNC][Condition 2] User leaving breakout (to main)', + {prevRoomId, srcuid, senderName}, + ); + // Set transition flag - user will remount in main room and need fresh data sessionStorage.setItem('breakout_room_transition', 'true'); lastSyncedSnapshotRef.current = null; @@ -2330,27 +2020,16 @@ const BreakoutRoomProvider = ({ // 3. User moved between breakout rooms if (userMovedBetweenRooms) { - console.log( - 'supriya-sync-ordering 3. user moved between breakout rooms', + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[SYNC][Condition 3] User moved between breakout rooms', + {prevRoomId, nextRoomId, srcuid, senderName}, ); - // const prevRoom = prevGroups.find(r => r.id === prevRoomId); - // const nextRoom = breakout_room.find(r => r.id === nextRoomId); - - // showDeduplicatedToast(`user-moved-${prevRoomId}-${nextRoomId}`, { - // leadingIconName: 'arrow-right', - // type: 'info', - // text1: `Host: ${senderName} has moved you to "${ - // nextRoom?.name || nextRoomId - // }".`, - // text2: `From "${prevRoom?.name || prevRoomId}"`, - // visibilityTime: 3000, - // }); } - // 4. Rooms control switched + // 4) Rooms switch control toggled if (switch_room && !prevSwitchRoom) { - console.log('supriya-sync-ordering 4. switch_room changed: '); - // Don't show toast if the user is the author if (srcuid !== localUid) { showDeduplicatedToast('switch-room-toggle', { leadingIconName: 'open-room', @@ -2360,14 +2039,18 @@ const BreakoutRoomProvider = ({ visibilityTime: 3000, }); } + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[SYNC][Condition 4] Switch room enabled', + {srcuid, senderName}, + ); } - // 5. Group renamed + // 5) Room renamed prevGroups.forEach(prevRoom => { const after = breakout_room.find(r => r.id === prevRoom.id); if (after && after.name !== prevRoom.name) { - console.log('supriya-sync-ordering 5. group renamed '); - // Don't show toast if the user is the author if (srcuid !== localUid) { showDeduplicatedToast(`room-renamed-${after.id}`, { type: 'info', @@ -2375,9 +2058,16 @@ const BreakoutRoomProvider = ({ visibilityTime: 3000, }); } + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[SYNC][Condition 5] Room renamed', + {roomId: after.id, from: prevRoom.name, to: after.name}, + ); } }); + // Apply new state dispatch({ type: BreakoutGroupActionTypes.SYNC_STATE, payload: { @@ -2391,8 +2081,26 @@ const BreakoutRoomProvider = ({ // Store the snap of this lastSyncedSnapshotRef.current = payload.data; lastSyncedTimestampRef.current = timestamp || Date.now(); + + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[SYNC] State applied', + { + session_id, + rooms: breakout_room?.length || 0, + timestampApplied: lastSyncedTimestampRef.current, + }, + ); }, - [dispatch, exitRoom, localUid, showDeduplicatedToast, getDisplayName], + [ + dispatch, + exitRoom, + localUid, + showDeduplicatedToast, + getDisplayName, + isBreakoutMode, + ], ); /** @@ -2400,21 +2108,29 @@ const BreakoutRoomProvider = ({ * Event 2 arrives (ts=200) and Event 3 arrives (ts=300). * Both will overwrite latestTask: * Now, queue.latestTask only holds event 3, because event 2 was replaced before it could be picked up. + * Latest-event-wins queue: enqueue only the freshest by timestamp. */ const enqueueBreakoutSyncEvent = useCallback( (payload: BreakoutRoomSyncStateEventPayload['data'], timestamp: number) => { const queue = breakoutSyncQueueRef.current; - // Always keep the freshest event only - console.log('supriya-sync-queue 1', queue); + if ( !queue.latestTask || (timestamp && timestamp > queue.latestTask.timestamp) ) { - console.log('supriya-sync-queue 2', queue); queue.latestTask = {payload, timestamp}; } - console.log('supriya-sync-queue 3', queue.latestTask); + + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[QUEUE] Enqueued sync event', + { + latestTs: queue.latestTask?.timestamp, + currentlyProcessing: queue.isProcessing, + }, + ); processBreakoutSyncQueue(); }, @@ -2423,40 +2139,199 @@ const BreakoutRoomProvider = ({ const processBreakoutSyncQueue = useCallback(async () => { const queue = breakoutSyncQueueRef.current; - console.log('supriya-sync-queue 4', queue.latestTask); // 1. If the queue is already being processed by another call, exit immediately. if (queue.isProcessing) { - console.log('supriya-sync-queue 5 returning '); - + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[QUEUE] Already processing, skipping start', + ); return; } try { // 2. "lock" the queue, so no second process can start. queue.isProcessing = true; - console.log('supriya-sync-queue 6 lcoked '); - // 3. Loop the queue + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[QUEUE] Processing started', + ); + while (queue.latestTask) { const {payload, timestamp} = queue.latestTask; - console.log('supriya-sync-queue 7 ', payload, timestamp); queue.latestTask = null; try { await _handleBreakoutRoomSyncState(payload, timestamp); - } catch (err) { - console.error('[BreakoutSync] Error processing sync event', err); + } catch (err: any) { + logger.error( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[QUEUE] Error processing sync event', + {error: err?.message}, + ); // Continue processing other events even if one fails } } - } catch (err) { - console.error('[BreakoutSync] Critical error in queue processing', err); + } catch (err: any) { + logger.error( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[QUEUE] Critical error', + {error: err?.message}, + ); } finally { // Always unlock the queue, even if there's an error queue.isProcessing = false; + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[QUEUE] Processing finished', + ); } }, []); + // Multi-host coordination handlers + const handleHostOperationStart = useCallback( + (operationName: string, hostUid: UidType, hostName: string) => { + // Only process if current user is also a host and it's not their own event + if (hostUid === localUid) { + return; + } + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `[HOST] Another host has started operation (lock UI) -> ${operationName}`, + {operationName, hostUid, hostName}, + ); + + setCurrentOperatingHostName(hostName); + }, + [localUid], + ); + + const handleHostOperationEnd = useCallback( + (operationName: string, hostUid: UidType, hostName: string) => { + // Only process if current user is also a host and it's not their own event + if (hostUid === localUid) { + return; + } + + logger.log( + LogSource.Internals, + 'BREAKOUT_ROOM', + `[HOST] Another host ended operation (unlock UI) ${operationName}`, + {operationName, hostUid, hostName}, + ); + + setCurrentOperatingHostName(undefined); + }, + [localUid], + ); + + // Debounced API for performance with multi-host coordination + const debouncedUpsertAPI = useDebouncedCallback( + async (type: 'START' | 'UPDATE', operationName?: string) => { + setBreakoutUpdateInFlight(true); + + try { + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[API] Debounced upsert start for', + {type, operationName}, + ); + + await upsertBreakoutRoomAPI(type); + + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[API] Debounced upsert success', + {type, operationName}, + ); + + // Broadcast operation end after successful API call + if (operationName) { + broadcastHostOperationEnd(operationName); + } + } catch (error: any) { + logger.error( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[API] Debounced upsert failed', + {error: error?.message, type, operationName}, + ); + + // Broadcast operation end even on failure + if (operationName) { + broadcastHostOperationEnd(operationName); + } + } finally { + setBreakoutUpdateInFlight(false); + } + }, + 500, + ); + + // Action-based API triggering + useEffect(() => { + if (!lastAction || !lastAction.type) { + return; + } + + // Actions that should trigger API calls + const API_TRIGGERING_ACTIONS = [ + BreakoutGroupActionTypes.CREATE_GROUP, + BreakoutGroupActionTypes.RENAME_GROUP, + BreakoutGroupActionTypes.CLOSE_GROUP, + BreakoutGroupActionTypes.CLOSE_ALL_GROUPS, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_MAIN, + BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP, + BreakoutGroupActionTypes.AUTO_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.MANUAL_ASSIGN_PARTICPANTS, + BreakoutGroupActionTypes.NO_ASSIGN_PARTICIPANTS, + BreakoutGroupActionTypes.SET_ALLOW_PEOPLE_TO_SWITCH_ROOM, + BreakoutGroupActionTypes.EXIT_GROUP, + ]; + + // Host can always trigger API calls for any action + // Attendees can only trigger API when they self-join a room and switch_room is enabled + const attendeeSelfJoinAllowed = + stateRef.current.canUserSwitchRoom && + lastAction.type === BreakoutGroupActionTypes.MOVE_PARTICIPANT_TO_GROUP; + + const shouldCallAPI = + API_TRIGGERING_ACTIONS.includes(lastAction.type as any) && + (isHostRef.current || (!isHostRef.current && attendeeSelfJoinAllowed)); + + // Compute lastOperationName based on lastAction + const lastOperationName = HOST_BROADCASTED_OPERATIONS.includes( + lastAction?.type as any, + ) + ? lastAction?.type + : undefined; + + logger.debug( + LogSource.Internals, + 'BREAKOUT_ROOM', + '[ACTION] Post-dispatch evaluation', + { + actionType: lastAction.type, + shouldCallAPI, + attendeeSelfJoinAllowed, + lastOperationName, + }, + ); + + if (shouldCallAPI) { + debouncedUpsertAPI('UPDATE', lastOperationName); + } + }, [lastAction]); + return ( {children} From e1e97252014acb422d26889d419bcaeb0418255b Mon Sep 17 00:00:00 2001 From: Supriya Adep Date: Tue, 14 Oct 2025 21:57:01 +0530 Subject: [PATCH 56/56] remove logs --- .../components/breakout-room/context/BreakoutRoomContext.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx index f5df9336b..b36200330 100644 --- a/template/src/components/breakout-room/context/BreakoutRoomContext.tsx +++ b/template/src/components/breakout-room/context/BreakoutRoomContext.tsx @@ -1659,7 +1659,6 @@ const BreakoutRoomProvider = ({ // } // try { // const timestamp = Date.now(); - // console.log('supriya-presenter sending make presenter'); // // Host sends BREAKOUT_ROOM_MAKE_PRESENTER event to the attendee // events.send( // BreakoutRoomEventNames.BREAKOUT_ROOM_MAKE_PRESENTER, @@ -1928,9 +1927,6 @@ const BreakoutRoomProvider = ({ // (userMovedBetweenRooms || userLeftBreakoutRoom) && // isScreenshareActive // ) { - // console.log( - // 'supriya-sync-ordering: stopping screenshare due to room change', - // ); // stopScreenshare(); // }