Files
upstream-jitsi-meet/react/features/whiteboard/components/web/Whiteboard.tsx
upstream-mirror 1d2bfcac0c
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 1s
snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
2026-09-15 15:56:32 +05:00

210 lines
7.8 KiB
TypeScript

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<string | undefined>;
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<any>(null);
const collabAPIRef = useRef<any>(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 (
<div
className = { clsx(
isResizing && 'disable-pointer',
'whiteboard-container'
) }
style = {{
...getDimensions(),
marginTop: `${HEIGHT_OFFSET}px`,
display: `${isInTileView || !isVisible ? 'none' : 'block'}`
}}>
{
isOpen && (
<div className = 'excalidraw-wrapper'>
{/*
* 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.
*/
<span
aria-level = { 1 }
className = 'sr-only'
role = 'heading'>
{ props.t('whiteboard.accessibilityLabel.heading') }
</span>
}
<Suspense fallback = { null }>
<LazyExcalidrawApp
collabDetails = { collabDetails }
collabServerUrl = { collabServerUrl }
excalidraw = {{
isCollaborating: true,
langCode: i18next.language,
theme: 'light',
UIOptions: storageBackendUrl ? WHITEBOARD_UI_OPTIONS_WITH_IMAGES : WHITEBOARD_UI_OPTIONS
}}
getCollabAPI = { getCollabAPI }
getExcalidrawAPI = { getExcalidrawAPI }
meetingDetails = { meetingDetails }
storageBackendUrl = { storageBackendUrl } />
</Suspense>
</div>
)}
</div>
);
};
export default translate(Whiteboard);