This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
/**
|
||||
* The type of the React {@code Component} props of {@link NoWhiteboardError}.
|
||||
*/
|
||||
interface IProps {
|
||||
|
||||
/**
|
||||
* Additional CSS classnames to append to the root of the component.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const NoWhiteboardError = ({ className }: IProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className = { className } >
|
||||
{t('info.noWhiteboard')}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NoWhiteboardError;
|
||||
165
react/features/whiteboard/components/web/Whiteboard.tsx
Normal file
165
react/features/whiteboard/components/web/Whiteboard.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import { ExcalidrawApp } from '@jitsi/excalidraw';
|
||||
import clsx from 'clsx';
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
// @ts-expect-error
|
||||
import Filmstrip from '../../../../../modules/UI/videolayout/Filmstrip';
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
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 } from '../../constants';
|
||||
import {
|
||||
getCollabDetails,
|
||||
getCollabServerUrl,
|
||||
isWhiteboardOpen,
|
||||
isWhiteboardVisible
|
||||
} from '../../functions';
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 excalidrawRef = useRef<any>(null);
|
||||
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 { defaultRemoteDisplayName } = useSelector((state: IReduxState) => state['features/base/config']);
|
||||
const localParticipantName = useSelector(getLocalParticipant)?.name || defaultRemoteDisplayName || 'Fellow Jitster';
|
||||
|
||||
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>
|
||||
}
|
||||
<ExcalidrawApp
|
||||
collabDetails = { collabDetails }
|
||||
collabServerUrl = { collabServerUrl }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
langCode: i18next.language,
|
||||
|
||||
// @ts-ignore
|
||||
ref: excalidrawRef,
|
||||
theme: 'light',
|
||||
UIOptions: WHITEBOARD_UI_OPTIONS
|
||||
}}
|
||||
getCollabAPI = { getCollabAPI }
|
||||
getExcalidrawAPI = { getExcalidrawAPI } />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default translate(Whiteboard);
|
||||
94
react/features/whiteboard/components/web/WhiteboardApp.tsx
Normal file
94
react/features/whiteboard/components/web/WhiteboardApp.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import { generateCollaborationLinkData } from '@jitsi/excalidraw';
|
||||
import React, { ComponentType } from 'react';
|
||||
|
||||
import BaseApp from '../../../base/app/components/BaseApp';
|
||||
import GlobalStyles from '../../../base/ui/components/GlobalStyles.web';
|
||||
import JitsiThemeProvider from '../../../base/ui/components/JitsiThemeProvider.web';
|
||||
import { decodeFromBase64URL } from '../../../base/util/httpUtils';
|
||||
import { parseURLParams } from '../../../base/util/parseURLParams';
|
||||
import { safeDecodeURIComponent } from '../../../base/util/uri';
|
||||
import logger from '../../logger';
|
||||
|
||||
import NoWhiteboardError from './NoWhiteboardError';
|
||||
import WhiteboardWrapper from './WhiteboardWrapper';
|
||||
|
||||
/**
|
||||
* Wrapper application for the whiteboard.
|
||||
*
|
||||
* @augments BaseApp
|
||||
*/
|
||||
export default class WhiteboardApp extends BaseApp<any> {
|
||||
/**
|
||||
* Navigates to {@link Whiteboard} upon mount.
|
||||
*
|
||||
* @returns {void}
|
||||
*/
|
||||
override async componentDidMount() {
|
||||
await super.componentDidMount();
|
||||
|
||||
const { state } = parseURLParams(window.location.href, true);
|
||||
const decodedState = JSON.parse(decodeFromBase64URL(state));
|
||||
const { collabServerUrl, localParticipantName } = decodedState;
|
||||
let { roomId, roomKey } = decodedState;
|
||||
|
||||
if (!roomId && !roomKey) {
|
||||
try {
|
||||
const collabDetails = await generateCollaborationLinkData();
|
||||
|
||||
roomId = collabDetails.roomId;
|
||||
roomKey = collabDetails.roomKey;
|
||||
|
||||
if (window.ReactNativeWebView) {
|
||||
setTimeout(() => {
|
||||
window.ReactNativeWebView.postMessage(JSON.stringify({
|
||||
collabDetails,
|
||||
collabServerUrl
|
||||
}));
|
||||
}, 0);
|
||||
}
|
||||
} catch (e: any) {
|
||||
logger.error('Couldn\'t generate collaboration link data.', e);
|
||||
}
|
||||
}
|
||||
|
||||
super._navigate({
|
||||
component: () => (
|
||||
<>{
|
||||
roomId && roomKey && collabServerUrl
|
||||
? <WhiteboardWrapper
|
||||
className = 'whiteboard'
|
||||
collabDetails = {{
|
||||
roomId,
|
||||
roomKey
|
||||
}}
|
||||
collabServerUrl = { safeDecodeURIComponent(collabServerUrl) }
|
||||
localParticipantName = { localParticipantName } />
|
||||
: <NoWhiteboardError className = 'whiteboard' />
|
||||
}</>
|
||||
) });
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the parent method to inject {@link AtlasKitThemeProvider} as
|
||||
* the top most component.
|
||||
*
|
||||
* @override
|
||||
*/
|
||||
override _createMainElement(component: ComponentType<any>, props: Object) {
|
||||
return (
|
||||
<JitsiThemeProvider>
|
||||
<GlobalStyles />
|
||||
{super._createMainElement(component, props)}
|
||||
</JitsiThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the platform specific dialog container.
|
||||
*
|
||||
* @returns {React$Element}
|
||||
*/
|
||||
override _renderDialogContainer() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { IReduxState } from '../../../app/types';
|
||||
import { translate } from '../../../base/i18n/functions';
|
||||
import { IconWhiteboard, IconWhiteboardHide } from '../../../base/icons/svg';
|
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton';
|
||||
import { setOverflowMenuVisible } from '../../../toolbox/actions.web';
|
||||
import { setWhiteboardOpen } from '../../actions.any';
|
||||
import { isWhiteboardButtonVisible, isWhiteboardVisible } from '../../functions';
|
||||
|
||||
interface IProps extends AbstractButtonProps {
|
||||
|
||||
/**
|
||||
* Whether or not the button is toggled.
|
||||
*/
|
||||
_toggled: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component that renders a toolbar button for the whiteboard.
|
||||
*/
|
||||
class WhiteboardButton extends AbstractButton<IProps> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.showWhiteboard';
|
||||
override toggledAccessibilityLabel = 'toolbar.accessibilityLabel.hideWhiteboard';
|
||||
override icon = IconWhiteboard;
|
||||
override label = 'toolbar.showWhiteboard';
|
||||
override toggledIcon = IconWhiteboardHide;
|
||||
override toggledLabel = 'toolbar.hideWhiteboard';
|
||||
override toggledTooltip = 'toolbar.hideWhiteboard';
|
||||
override tooltip = 'toolbar.showWhiteboard';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens / closes the whiteboard view.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _handleClick() {
|
||||
const { dispatch, _toggled } = this.props;
|
||||
|
||||
dispatch(setWhiteboardOpen(!_toggled));
|
||||
dispatch(setOverflowMenuVisible(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether this button is in toggled state or not.
|
||||
*
|
||||
* @override
|
||||
* @protected
|
||||
* @returns {boolean}
|
||||
*/
|
||||
override _isToggled() {
|
||||
return this.props._toggled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps part of the Redux state to the props of this component.
|
||||
*
|
||||
* @param {Object} state - The Redux state.
|
||||
* @private
|
||||
* @returns {IProps}
|
||||
*/
|
||||
function _mapStateToProps(state: IReduxState) {
|
||||
return {
|
||||
_toggled: isWhiteboardVisible(state),
|
||||
visible: isWhiteboardButtonVisible(state)
|
||||
};
|
||||
}
|
||||
|
||||
export default translate(connect(_mapStateToProps)(WhiteboardButton));
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import Dialog from '../../../base/ui/components/web/Dialog';
|
||||
import { getWhiteboardConfig } from '../../functions';
|
||||
|
||||
/**
|
||||
* Component that renders the whiteboard user limit dialog.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const WhiteboardLimitDialog = () => {
|
||||
const { t } = useTranslation();
|
||||
const { limitUrl } = useSelector(getWhiteboardConfig);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
cancel = {{ hidden: true }}
|
||||
ok = {{ hidden: true }}
|
||||
titleKey = { t('dialog.whiteboardLimitTitle') }>
|
||||
<span>{t('dialog.whiteboardLimitContent')}</span>
|
||||
{limitUrl && (
|
||||
<span>
|
||||
{` ${t('dialog.whiteboardLimitReference')} `}
|
||||
<a
|
||||
href = { limitUrl }
|
||||
rel = 'noopener noreferrer'
|
||||
target = '_blank'>
|
||||
{t('dialog.whiteboardLimitReferenceUrl')}
|
||||
</a>
|
||||
.
|
||||
</span>
|
||||
)}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhiteboardLimitDialog;
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ExcalidrawApp } from '@jitsi/excalidraw';
|
||||
import i18next from 'i18next';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
|
||||
import { WHITEBOARD_UI_OPTIONS } from '../../constants';
|
||||
|
||||
/**
|
||||
* Whiteboard wrapper for mobile.
|
||||
*
|
||||
* @returns {JSX.Element}
|
||||
*/
|
||||
const WhiteboardWrapper = ({
|
||||
className,
|
||||
collabDetails,
|
||||
collabServerUrl,
|
||||
localParticipantName
|
||||
}: {
|
||||
className?: string;
|
||||
collabDetails: {
|
||||
roomId: string;
|
||||
roomKey: string;
|
||||
};
|
||||
collabServerUrl: string;
|
||||
localParticipantName: string;
|
||||
}) => {
|
||||
const excalidrawRef = useRef<any>(null);
|
||||
const excalidrawAPIRef = useRef<any>(null);
|
||||
const collabAPIRef = useRef<any>(null);
|
||||
|
||||
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 = { className }>
|
||||
<div className = 'excalidraw-wrapper'>
|
||||
<ExcalidrawApp
|
||||
collabDetails = { collabDetails }
|
||||
collabServerUrl = { collabServerUrl }
|
||||
detectScroll = { true }
|
||||
excalidraw = {{
|
||||
isCollaborating: true,
|
||||
langCode: i18next.language,
|
||||
|
||||
// @ts-ignore
|
||||
ref: excalidrawRef,
|
||||
theme: 'light',
|
||||
UIOptions: WHITEBOARD_UI_OPTIONS
|
||||
}}
|
||||
getCollabAPI = { getCollabAPI }
|
||||
getExcalidrawAPI = { getExcalidrawAPI } />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhiteboardWrapper;
|
||||
Reference in New Issue
Block a user