Skip to main content

Add custom sidebar

Source Code: https://github.com/dyte-io/react-samples/tree/main/samples/create-your-own-ui

To create a sidebar of your own, you need 2 things.

  1. A custom sidebar UI
  2. An action button to trigger the UI
LIVE EDITOR

import { DyteStage, DyteGrid, DyteNotifications, DyteSidebar, DyteControlbar, DyteParticipantsAudio, DyteDialogManager, DyteControlbarButton, DyteSidebarUi, defaultIconPack, defaultConfig, generateConfig } from '@dytesdk/react-ui-kit';
import { useDyteMeeting, useDyteSelector } from '@dytesdk/react-web-core';
import { useEffect, useState } from 'react';

function SidebarWithCustomUI({
    meeting, states, config, setStates,
}: { meeting: DyteClient, config: UIConfig, states: CustomStates, setStates: SetStates }
){
    const [tabs, setTabs] = useState([
        { id: 'chat', name: 'chat' },
        { id: 'polls', name: 'polls' },
        { id: 'participants', name: 'participants' },
        { id: 'plugins', name: 'plugins' },
        { id: 'guidelines', name: 'Guidelines' }
    ]);
    const [view, setView] = useState<DyteSidebarView>('sidebar');

    if(!states.activeSidebar || (!states.sidebar && !states.customSidebar)){
        return null;
    }

    const currentTab = states.sidebar || states.customSidebar;

    return (
    <DyteSidebarUi
        tabs={tabs}
        currentTab={currentTab}
        view={view}
        onTabChange={(e) => {
            setStates((oldState) => {
                return {
                    ...oldState,
                    activeSidebar: true,
                    customSidebar: e.detail,
                    sidebar: e.detail,
                }
            });
        }}
        className="w-80 "
        onSidebarClose={() => {
            setStates((oldState) => {
                return {
                    ...oldState,
                    activeSidebar: false,
                    sidebar: null,
                    customSidebar: null,
                }
            });
        }}>
        {currentTab === 'chat' && <DyteChat meeting={meeting} config={config} slot="chat" /> }
        {currentTab === 'polls' && <DytePolls meeting={meeting} config={config} slot="polls" /> }
        {currentTab === 'participants' && <DyteParticipants meeting={meeting} config={config} states={states} slot="participants" /> }
        {currentTab === 'plugins' && <DytePlugins meeting={meeting} config={config} slot="plugins" /> }
        {currentTab === 'guidelines' && <div slot="guidelines" className="flex justify-center items-center p-2">
            <div>
              <p>1. Ensure active participation and professionalism by muting your microphone when not speaking.</p>
              <br></br>
              <p>2. Utilize the chat feature for questions or comments during the meeting.</p>
            </div>
        </div> }
    </DyteSidebarUi>);

}

function MeetingStage({ meeting, config, states, setStates }: { meeting: DyteClient, config: UIConfig, states: CustomStates, setStates: SetStates}) {

return (

<div className="flex h-full w-full flex-col">
  <DyteStage className="flex h-full w-full flex-1 p-2">
    <DyteGrid meeting={meeting} config={config} states={states} />
    <DyteNotifications meeting={meeting} config={config} states={states} />
    {states.activeSidebar && (
      <SidebarWithCustomUI
        meeting={meeting}
        config={config}
        states={states}
        setStates={setStates}
      />
    )}
  </DyteStage>
  <DyteParticipantsAudio meeting={meeting} />
  <DyteDialogManager meeting={meeting} config={config} states={states} />
  <div>
    <DyteControlbarButton
      onClick={() => {
          if(states.activeSidebar && !states.sidebar && states.customSidebar === 'guidelines'){
              setStates( (oldState) => { return { ...oldState, activeSidebar: false, sidebar: null, customSidebar: null }});    
          } else {
              setStates( (oldState) => { return { ...oldState, activeSidebar: true, sidebar: null, customSidebar: 'guidelines' }});
          }
      }}
      icon={defaultIconPack.add}
      label={'Open Custom SideBar'}
    />
</div>

</div>
); }

export default function Meeting() {
const { meeting } = useDyteMeeting();
const [config, setConfig] = useState(defaultConfig);
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 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){
    /**
     * During development phase, make sure to expose meeting object to window,
     * for debugging purposes.
     */
      Object.assign(window, {
        meeting,
      })
      setupMeetingConfigs();
    }

}, [meeting]);

return (

  <div className="flex w-full h-full" ref={(el) => {
            el?.addEventListener('dyteStateUpdate', (e) => {
              const { detail: newStateUpdate } = e as unknown as { detail: CustomStates };
              console.log('dyteStateUpdateSetup:: ', newStateUpdate);
              setStates((oldState: CustomStates) => { return {
                ...oldState,
                ...newStateUpdate,
              }});
            });
          }}>
      <MeetingStage meeting={meeting} config={config} states={states} setStates={setStates} />
  </div>
)

}

export 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(){

    }

}

Let's say, we want to show some meeting guidelines to all the participants in a side bar. To do so, in the below code snippet, we have added guidelines sidebar section.

We have added a custom button to trigger the UI as well using DyteControlbarButton component.

    <DyteControlbarButton
onClick={() => {
if(states.activeSidebar && !states.sidebar && states.customSidebar === 'guidelines'){
setStates( (oldState) => { return { ...oldState, activeSidebar: false, sidebar: null, customSidebar: null }});
} else {
setStates( (oldState) => { return { ...oldState, activeSidebar: true, sidebar: null, customSidebar: 'guidelines' }});
}
}}
icon={defaultIconPack.add}
label={'Open Custom SideBar'}
/>

For such a sidebar extension, we will have to update the types as well if in case you are using react with Typescript.

import type { States } from "@dytesdk/ui-kit";
import { DyteSidebarSection } from "@dytesdk/ui-kit/dist/types/components/dyte-sidebar/dyte-sidebar";

export type CustomSideBarTabs = DyteSidebarSection | 'guidelines';

export type CustomStates = States & { activeMediaPreviewModal?: boolean, customSidebar?: CustomSideBarTabs }

export type SetStates = React.Dispatch<React.SetStateAction<CustomStates>>;

Now that we know how we can add a custom sidebar, we can move on to customise the DyteStage component further.