Skip to main content

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.

  1. Keeps a mapping of components and show them according to the preset's view_type such as group_call, webinar, and livestream.
  2. Provides background color, text colors and other such CSS properties.
  3. Maintains states of modals, sidebars between web-core & ui-kit
  4. Shifts the control bar buttons to More menu if the screen size is small.
  5. Passes config, states, translation, icon packs to all child components.
  6. It is the target element that gets full screened on click of full screen toggle.
  7. 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.