import clsx from 'clsx'; import i18next from 'i18next'; import React, { Suspense, useCallback, useEffect, useRef } from 'react'; import { WithTranslation } from 'react-i18next'; import { useSelector, useStore } from 'react-redux'; // @ts-expect-error import Filmstrip from '../../../../../modules/UI/videolayout/Filmstrip'; import { IReduxState } from '../../../app/types'; import { getCurrentConference } from '../../../base/conference/functions'; import { translate } from '../../../base/i18n/functions.web'; import { getLocalParticipant } from '../../../base/participants/functions'; import { getVerticalViewMaxWidth } from '../../../filmstrip/functions.web'; import { getToolboxHeight } from '../../../toolbox/functions.web'; import { shouldDisplayTileView } from '../../../video-layout/functions.any'; import { WHITEBOARD_UI_OPTIONS, WHITEBOARD_UI_OPTIONS_WITH_IMAGES } from '../../constants'; import { getCollabDetails, getCollabServerUrl, getStorageBackendUrl, isWhiteboardOpen, isWhiteboardVisible } from '../../functions'; const LazyExcalidrawApp = React.lazy(async () => { const [ { ExcalidrawApp } ] = await Promise.all([ import(/* webpackChunkName: "excalidraw" */ '@jitsi/excalidraw'), import(/* webpackChunkName: "excalidraw" */ '@jitsi/excalidraw/index.css') ]); return { default: ExcalidrawApp }; }); /** * Space taken by meeting elements like the subject and the watermark. */ const HEIGHT_OFFSET = 80; interface IDimensions { /* The height of the component. */ height: string; /* The width of the component. */ width: string; } interface IMeetingDetails { getStorageToken: () => Promise; jwt: string; roomJid: string; sessionId: string; } /** * The Whiteboard component. * * @param {Props} props - The React props passed to this component. * @returns {JSX.Element} - The React component. */ const Whiteboard = (props: WithTranslation): JSX.Element => { const excalidrawAPIRef = useRef(null); const collabAPIRef = useRef(null); const isOpen = useSelector(isWhiteboardOpen); const isVisible = useSelector(isWhiteboardVisible); const isInTileView = useSelector(shouldDisplayTileView); const { clientHeight, videoSpaceWidth } = useSelector((state: IReduxState) => state['features/base/responsive-ui']); const { visible: filmstripVisible, isResizing: isFilmstripResizing } = useSelector((state: IReduxState) => state['features/filmstrip']); const isChatResizing = useSelector((state: IReduxState) => state['features/chat'].isResizing); const isResizing = isFilmstripResizing || isChatResizing; const filmstripWidth: number = useSelector(getVerticalViewMaxWidth); const collabDetails = useSelector(getCollabDetails); const collabServerUrl = useSelector(getCollabServerUrl); const storageBackendUrl = useSelector(getStorageBackendUrl); const { defaultRemoteDisplayName } = useSelector((state: IReduxState) => state['features/base/config']); const localParticipantName = useSelector(getLocalParticipant)?.name || defaultRemoteDisplayName || 'Fellow Jitster'; const jwt = useSelector((state: IReduxState) => state['features/base/jwt']).jwt || ''; const store = useStore(); const state = store.getState(); const conference = getCurrentConference(state); const sessionId = conference?.getMeetingUniqueId(); const roomJid = conference?.room?.roomjid; // Provides a fresh short-term credential for each whiteboard storage // request, so that image binaries are uploaded/fetched with a token that is // refreshed transparently as the previous one expires. const getStorageToken = useCallback(async () => { const conf = getCurrentConference(store.getState()); if (!conf) { return undefined; } return conf.getShortTermCredentials(conf.getFileSharing()?.getIdentityType()); }, [ store ]); const meetingDetails: IMeetingDetails = { sessionId: sessionId ?? '', roomJid: roomJid ?? '', jwt: jwt, getStorageToken }; useEffect(() => { if (!collabAPIRef.current) { return; } collabAPIRef.current.setUsername(localParticipantName); }, [ localParticipantName ]); /** * Computes the width and the height of the component. * * @returns {IDimensions} - The dimensions of the component. */ const getDimensions = (): IDimensions => { let width: number; let height: number; if (interfaceConfig.VERTICAL_FILMSTRIP) { if (filmstripVisible) { width = videoSpaceWidth - filmstripWidth; } else { width = videoSpaceWidth; } height = clientHeight - getToolboxHeight(); } else { if (filmstripVisible) { height = clientHeight - Filmstrip.getFilmstripHeight(); } else { height = clientHeight; } width = videoSpaceWidth; } return { width: `${width}px`, height: `${height - HEIGHT_OFFSET}px` }; }; const getExcalidrawAPI = useCallback(excalidrawAPI => { if (excalidrawAPIRef.current) { return; } excalidrawAPIRef.current = excalidrawAPI; }, []); const getCollabAPI = useCallback(collabAPI => { if (collabAPIRef.current) { return; } collabAPIRef.current = collabAPI; collabAPIRef.current.setUsername(localParticipantName); }, [ localParticipantName ]); return (
{ isOpen && (
{/* * Excalidraw renders a few lvl 2 headings. This is * quite fortunate, because we actually use lvl 1 * headings to mark the big sections of our app. So make * sure to mark the Excalidraw context with a lvl 1 * heading before showing the whiteboard. */ { props.t('whiteboard.accessibilityLabel.heading') } }
)}
); }; export default translate(Whiteboard);