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.