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.