snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
209
react/features/whiteboard/components/web/Whiteboard.tsx
Normal file
209
react/features/whiteboard/components/web/Whiteboard.tsx
Normal file
@@ -0,0 +1,209 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user