Handling States and Configs
Source Code: https://github.com/dyte-io/react-samples/tree/main/samples/create-your-own-ui
DyteMeeting
component does a lot more than just providing the user interface.
It does the following things internally.
- Keeps a mapping of components and show them according to the preset's view_type such as group_call, webinar, and livestream.
- Provides background color, text colors and other such CSS properties.
- Maintains states of modals, sidebars between web-core & ui-kit
- Shifts the control bar buttons to More menu if the screen size is small.
- Passes config, states, translation, icon packs to all child components.
- It is the target element that gets full screened on click of full screen toggle.
- Joins the meeting automatically if showSetupScreen is false.
Since we are splitting DyteMeeting
component in pieces, we need to do these ourselves now.
import { defaultConfig, generateConfig } from '@dytesdk/react-ui-kit'; import { useDyteMeeting, useDyteSelector } from '@dytesdk/react-web-core'; import { useEffect, useState } from 'react'; export default function Meeting() { const { meeting } = useDyteMeeting(); const [config, setConfig] = useState(defaultConfig); /** * We need setStates method to add custom functionalities, * as well as to ensure that web-core & ui-kit are in Sync. */ const [states, setStates] = useState<CustomStates>({ meeting: 'setup', sidebar: 'chat' }); useEffect(() => { async function setupMeetingConfigs(){ const theme = meeting!.self.config; const { config } = generateConfig(theme, meeting!); /** * Full screen toggle, by default requests dyte-meeting/DyteMeeting element to be in full screen. * Since DyteMeeting element is not here, * we need to pass dyte-fullscreen-toggle, an targetElementId through config. */ setFullScreenToggleTargetElement({config, targetElementId: 'root'}); setConfig({...config}); /** * Add listeners on meeting & self to monitor leave meeting, join meeting and so on. * This work was earlier done by DyteMeeting component internally. */ const stateListenersUtils = new DyteStateListenersUtils(() => meeting, () => states, () => setStates); stateListenersUtils.addDyteEventListeners(); try{ await meeting.join(); } catch(e){ // do nothing } } if(meeting){ setupMeetingConfigs(); } }, [meeting]); return ( /** - Using a ref hack, we are adding "dyteStateUpdate" listener, - so that we can listen to child component's internal state changes. */ <div className="flex w-full h-full bg-black text-white justify-center items-center" ref={(el) => { el?.addEventListener('dyteStateUpdate', (e) => { const { detail: newStateUpdate } = e as unknown as { detail: CustomStates }; setStates((oldState: CustomStates) => { return { ...oldState, ...newStateUpdate, }}); }); }}> <CustomDyteMeetingUI meeting={meeting} config={config} states={states} setStates={setStates} /> </div> ); } function CustomDyteMeetingUI({ meeting, config, states, setStates }: { meeting: DyteClient, config: UIConfig, states: CustomStates, setStates: SetStates}) { return <div>Your Custom UI will come here </div>; } /** - DyteStateListenersUtils is a class that listens to web-core changes and syncs them with ui-kit */ class DyteStateListenersUtils{ getStates: () => CustomStates; getStateSetter: () => (newState: CustomStates) => void; getMeeting: () => DyteClient; get states(){ return this.getStates(); } get setGlobalStates(){ return this.getStateSetter(); }; get meeting(){ return this.getMeeting(); } constructor(getMeeting: () => DyteClient, getGlobalStates: () => CustomStates, getGlobalStateSetter: () => (newState: CustomStates) => void){ this.getMeeting = getMeeting; this.getStates = getGlobalStates; this.getStateSetter = getGlobalStateSetter; } private updateStates(newState: CustomStates){ this.setGlobalStates((oldState: CustomStates) => { return { ...oldState, ...newState, }}); console.log(newState); } private roomJoinedListener = () => { this.updateStates({ meeting: 'joined' }); }; private socketServiceRoomJoinedListener = () => { if (this.meeting.stage.status === 'ON_STAGE' || this.meeting.stage.status === undefined) return; this.updateStates({ meeting: 'joined' }); }; private waitlistedListener = () => { this.updateStates({ meeting: 'waiting' }); }; private roomLeftListener = ({ state }: { state: RoomLeftState }) => { const states = this.states; if (states?.roomLeftState === 'disconnected') { this.updateStates({ meeting: 'ended', roomLeftState: state }); return; } this.updateStates({ meeting: 'ended', roomLeftState: state }); }; private mediaPermissionUpdateListener = ({ kind, message }: { kind: PermissionSettings['kind'], message: string, }) => { if (['audio', 'video'].includes(kind!)) { if (message === 'ACCEPTED' || message === 'NOT_REQUESTED' || this.states.activeDebugger) return; const permissionModalSettings: PermissionSettings = { enabled: true, kind, }; this.updateStates({ activePermissionsMessage: permissionModalSettings }); } }; private joinStateAcceptedListener = () => { this.updateStates({ activeJoinStage: true }); }; private handleChangingMeeting(destinationMeetingId: string) { this.updateStates({ activeBreakoutRoomsManager: { ...this.states.activeBreakoutRoomsManager, active: this.states.activeBreakoutRoomsManager!.active, destinationMeetingId, } }); } addDyteEventListeners(){ if (this.meeting.meta.viewType === 'LIVESTREAM') { this.meeting.self.addListener('socketServiceRoomJoined', this.socketServiceRoomJoinedListener); } this.meeting.self.addListener('roomJoined', this.roomJoinedListener); this.meeting.self.addListener('waitlisted', this.waitlistedListener); this.meeting.self.addListener('roomLeft', this.roomLeftListener); this.meeting.self.addListener('mediaPermissionUpdate', this.mediaPermissionUpdateListener); this.meeting.self.addListener('joinStageRequestAccepted', this.joinStateAcceptedListener); if (this.meeting.connectedMeetings.supportsConnectedMeetings) { this.meeting.connectedMeetings.once('changingMeeting', this.handleChangingMeeting); } } cleanupDyteEventListeners(){ } } /** - setFullScreenToggleTargetElement updates the ui-kit config, - to set targetElement to full screen toggle. */ function setFullScreenToggleTargetElement({config, targetElementId}: { config: UIConfig, targetElementId: string }){ if (config.root && Array.isArray(config.root['div#controlbar-left'])) { const fullScreenToggleIndex = config.root['div#controlbar-left'].indexOf('dyte-fullscreen-toggle'); if(fullScreenToggleIndex > -1){ config.root['div#controlbar-left'][fullScreenToggleIndex] = ['dyte-fullscreen-toggle', { variant: 'vertical', targetElement: document.querySelector("#"+targetElementId), }]; } } ['dyte-more-toggle.activeMoreMenu', 'dyte-more-toggle.activeMoreMenu.md', 'dyte-more-toggle.activeMoreMenu.sm'].forEach((configElemKey) => { const configElem = config?.root?.[configElemKey] as any; configElem?.forEach((dyteElemConfigSet: any) => { if (dyteElemConfigSet[0] === 'dyte-fullscreen-toggle') { dyteElemConfigSet[1].targetElement = document.querySelector("#"+targetElementId); } }); }); }
Let's discuss the bits and pieces one by one.
const theme = meeting!.self.config;
const { config } = generateConfig(theme, meeting!);
In the above code snippets, we are generating configs using the preset configurations & meeting configs.
Post this, We are extending the config to pass the targetElement to full screen toggle and storing this config to be passed to child components.
setFullScreenToggleTargetElement({ config, targetElementId: 'root' });
setConfig({ ...config });
We need to also ensure that web-core & ui-kit states are in sync. Since we are handling states now, we will have to add web-core & ui-kit listeners.
To add web-core listeners, DyteStateListenersUtils
class, is being used.
const stateListenersUtils = new DyteStateListenersUtils(
() => meeting,
() => states,
() => setStates,
);
stateListenersUtils.addDyteEventListeners();
To add ui-kit state listeners, we are using ref based hack to ensure that every propagated dyteStateUpdate
event is listened to.
ref={(el) => {
el?.addEventListener('dyteStateUpdate', (e) => {
const { detail: newStateUpdate } = e as unknown as { detail: CustomStates };
setStates((oldState: CustomStates) => { return {
...oldState,
...newStateUpdate,
}});
});
}}>
To join the meeting, we are using await meeting.join();
.
Now that we know the extra overhead that comes with splitting DyteMeeting
component, let's start with showing custom UIs as per the meeting state.