Handling States and Configs
Source Code: https://github.com/dyte-io/react-native-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 React, {useEffect, useState} from 'react';
import {
DyteProvider,
useDyteClient,
useDyteMeeting,
} from '@dytesdk/react-native-core';
import DyteClient from '@dytesdk/web-core';
import {
DyteUIProvider,
UIConfig,
defaultConfig,
generateConfig,
} from '@dytesdk/react-native-ui-kit';
import {DyteThemePresetV1} from '@dytesdk/web-core';
import {DyteStateListenersUtils} from './dyte-state-listeners';
import {CustomStates} from './types';
import {store} from './utils/store';
import {Provider} from 'react-redux';
function Meeting() {
const {meeting} = useDyteMeeting();
const [config, setConfig] = useState<UIConfig>(defaultConfig);
const [states, setStates] = useState<CustomStates>({
meeting: 'setup',
sidebar: 'chat',
activeMoreMenu: false,
activeLeaveConfirmation: false,
permissionGranted: true,
prefs: {
mirrorVideo: true,
muteNotificationSounds: false,
autoScroll: true,
},
designSystem: {
colors: {
brand: {
300: '#497CFD',
400: '#356EFD',
500: '#2160FD',
600: '#0D51FD',
700: '#2160FD',
},
background: {
1000: '#080808',
900: '#1A1A1A',
800: '#333333',
700: '#4C4C4C',
600: '#666666',
},
text: '#FFFFFF',
textOnBrand: '#FFFFFF',
videoBg: '#333333',
success: '#83D017',
danger: '#FF2D2D',
warning: '#FFCD07',
},
},
});
useEffect(() => {
async function setupMeetingConfigs() {
const theme = meeting!.self.config;
const generatedConfig = generateConfig(theme as DyteThemePresetV1, {});
const newConfig = generatedConfig.config;
setConfig({...newConfig});
const stateListenersUtils = new DyteStateListenersUtils(
() => meeting,
() => states,
() => setStates,
);
stateListenersUtils.addDyteEventListeners();
}
setupMeetingConfigs();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [meeting]);
return <CustomDyteMeetingUI meeting={meeting} config={config} states={states} setStates={setStates} />;
}
function CustomDyteMeetingUI({ meeting, config, states, setStates }: { meeting: DyteClient, config: UIConfig, states: CustomStates, setStates: SetStates}) {
return (
<View>
<DyteText>Your Custom UI will come here </DyteText>
</View>
);
}
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,
};
});
}
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: any; // PermissionSettings['kind'];
message: string;
}) => {
if (['audio', 'video'].includes(kind!)) {
if (
message === 'ACCEPTED' ||
message === 'NOT_REQUESTED' ||
this.states.activeDebugger
) {
return;
}
const permissionModalSettings: any = {
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() {}
}
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.
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 react-native-core listeners, DyteStateListenersUtils
class, is being used.
const stateListenersUtils = new DyteStateListenersUtils(
() => meeting,
() => states,
() => setStates
);
stateListenersUtils.addDyteEventListeners();
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.