mirror of https://github.com/jitsi/jitsi-meet
parent
2035cd7e62
commit
3f657c3ded
@ -0,0 +1,7 @@ |
||||
.whiteboard { |
||||
|
||||
.excalidraw-wrapper { |
||||
height: 100vh; |
||||
width: 100vw; |
||||
} |
||||
} |
@ -1,31 +1,68 @@ |
||||
import { IStore } from '../app/types'; |
||||
import { showWarningNotification } from '../notifications/actions'; |
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants'; |
||||
|
||||
import { setWhiteboardOpen } from './actions'; |
||||
import { isWhiteboardAllowed, isWhiteboardOpen, isWhiteboardVisible } from './functions'; |
||||
import { WhiteboardStatus } from './types'; |
||||
import { |
||||
RESET_WHITEBOARD, |
||||
SETUP_WHITEBOARD, |
||||
SET_WHITEBOARD_OPEN |
||||
} from './actionTypes'; |
||||
import { IWhiteboardAction } from './reducer'; |
||||
|
||||
/** |
||||
* Configures the whiteboard collaboration details. |
||||
* |
||||
* @param {Object} payload - The whiteboard settings. |
||||
* @returns {{ |
||||
* type: SETUP_WHITEBOARD, |
||||
* collabDetails: { roomId: string, roomKey: string } |
||||
* }} |
||||
*/ |
||||
export const setupWhiteboard = ({ collabDetails }: { |
||||
collabDetails: { roomId: string; roomKey: string; }; |
||||
}): IWhiteboardAction => { |
||||
return { |
||||
type: SETUP_WHITEBOARD, |
||||
collabDetails |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* API to toggle the whiteboard. |
||||
* Cleans up the whiteboard collaboration settings. |
||||
* To be used only on native for cleanup in between conferences. |
||||
* |
||||
* @returns {Function} |
||||
* @returns {{ |
||||
* type: RESET_WHITEBOARD |
||||
* }} |
||||
*/ |
||||
export function toggleWhiteboard() { |
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => { |
||||
const state = getState(); |
||||
const isAllowed = isWhiteboardAllowed(state); |
||||
const isOpen = isWhiteboardOpen(state); |
||||
export const resetWhiteboard = (): IWhiteboardAction => { |
||||
return { type: RESET_WHITEBOARD }; |
||||
}; |
||||
|
||||
if (isAllowed) { |
||||
if (isOpen && !isWhiteboardVisible(state)) { |
||||
dispatch(setWhiteboardOpen(true)); |
||||
} else if (isOpen && isWhiteboardVisible(state)) { |
||||
dispatch(setWhiteboardOpen(false)); |
||||
} else if (!isOpen) { |
||||
dispatch(setWhiteboardOpen(true)); |
||||
} |
||||
} else if (typeof APP !== 'undefined') { |
||||
APP.API.notifyWhiteboardStatusChanged(WhiteboardStatus.FORBIDDEN); |
||||
} |
||||
/** |
||||
* Sets the whiteboard visibility status. |
||||
* |
||||
* @param {boolean} isOpen - The whiteboard visibility flag. |
||||
* @returns {{ |
||||
* type: SET_WHITEBOARD_OPEN, |
||||
* isOpen |
||||
* }} |
||||
*/ |
||||
export const setWhiteboardOpen = (isOpen: boolean): IWhiteboardAction => { |
||||
return { |
||||
type: SET_WHITEBOARD_OPEN, |
||||
isOpen |
||||
}; |
||||
} |
||||
}; |
||||
|
||||
/** |
||||
* Shows a warning notification about the whiteboard user limit. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export const notifyWhiteboardLimit = () => (dispatch: IStore['dispatch']) => { |
||||
dispatch(showWarningNotification({ |
||||
titleKey: 'notify.whiteboardLimitTitle', |
||||
descriptionKey: 'notify.whiteboardLimitDescription' |
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG)); |
||||
}; |
||||
|
@ -0,0 +1,23 @@ |
||||
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents'; |
||||
import { sendAnalytics } from '../analytics/functions'; |
||||
import { IStore } from '../app/types'; |
||||
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef'; |
||||
import { screen } from '../mobile/navigation/routes'; |
||||
|
||||
import { resetWhiteboard } from './actions.any'; |
||||
|
||||
export * from './actions.any'; |
||||
|
||||
/** |
||||
* Restricts the whiteboard usage. |
||||
* |
||||
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard. |
||||
* @returns {Function} |
||||
*/ |
||||
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => { |
||||
if (shouldCloseWhiteboard) { |
||||
navigateRoot(screen.conference.root); |
||||
} |
||||
dispatch(resetWhiteboard()); |
||||
sendAnalytics(createRestrictWhiteboardEvent()); |
||||
}; |
@ -1,82 +0,0 @@ |
||||
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents'; |
||||
import { sendAnalytics } from '../analytics/functions'; |
||||
import { IStore } from '../app/types'; |
||||
import { showWarningNotification } from '../notifications/actions'; |
||||
import { NOTIFICATION_TIMEOUT_TYPE } from '../notifications/constants'; |
||||
|
||||
import { |
||||
RESET_WHITEBOARD, |
||||
SETUP_WHITEBOARD, |
||||
SET_WHITEBOARD_OPEN |
||||
} from './actionTypes'; |
||||
import { IWhiteboardAction } from './reducer'; |
||||
|
||||
/** |
||||
* Configures the whiteboard collaboration details. |
||||
* |
||||
* @param {Object} payload - The whiteboard settings. |
||||
* @returns {{ |
||||
* type: SETUP_WHITEBOARD, |
||||
* collabDetails: { roomId: string, roomKey: string } |
||||
* }} |
||||
*/ |
||||
export const setupWhiteboard = ({ collabDetails }: { |
||||
collabDetails: { roomId: string; roomKey: string; }; |
||||
}): IWhiteboardAction => { |
||||
return { |
||||
type: SETUP_WHITEBOARD, |
||||
collabDetails |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* Cleans up the whiteboard collaboration settings. |
||||
* To be used only on native for cleanup in between conferences. |
||||
* |
||||
* @returns {{ |
||||
* type: RESET_WHITEBOARD |
||||
* }} |
||||
*/ |
||||
export const resetWhiteboard = (): IWhiteboardAction => { |
||||
return { type: RESET_WHITEBOARD }; |
||||
}; |
||||
|
||||
/** |
||||
* Sets the whiteboard visibility status. |
||||
* |
||||
* @param {boolean} isOpen - The whiteboard visibility flag. |
||||
* @returns {{ |
||||
* type: SET_WHITEBOARD_OPEN, |
||||
* isOpen |
||||
* }} |
||||
*/ |
||||
export const setWhiteboardOpen = (isOpen: boolean): IWhiteboardAction => { |
||||
return { |
||||
type: SET_WHITEBOARD_OPEN, |
||||
isOpen |
||||
}; |
||||
}; |
||||
|
||||
/** |
||||
* Shows a warning notification about the whiteboard user limit. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export const notifyWhiteboardLimit = () => (dispatch: IStore['dispatch']) => { |
||||
dispatch(showWarningNotification({ |
||||
titleKey: 'notify.whiteboardLimitTitle', |
||||
descriptionKey: 'notify.whiteboardLimitDescription' |
||||
}, NOTIFICATION_TIMEOUT_TYPE.LONG)); |
||||
}; |
||||
|
||||
/** |
||||
* Restricts the whiteboard usage. |
||||
* |
||||
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard participant. |
||||
* @returns {Function} |
||||
*/ |
||||
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => { |
||||
shouldCloseWhiteboard && dispatch(setWhiteboardOpen(false)); |
||||
dispatch(resetWhiteboard()); |
||||
sendAnalytics(createRestrictWhiteboardEvent()); |
||||
}; |
@ -0,0 +1,48 @@ |
||||
import { createRestrictWhiteboardEvent } from '../analytics/AnalyticsEvents'; |
||||
import { sendAnalytics } from '../analytics/functions'; |
||||
import { IStore } from '../app/types'; |
||||
|
||||
import { resetWhiteboard, setWhiteboardOpen } from './actions.any'; |
||||
import { isWhiteboardAllowed, isWhiteboardOpen, isWhiteboardVisible } from './functions'; |
||||
import { WhiteboardStatus } from './types'; |
||||
|
||||
export * from './actions.any'; |
||||
|
||||
/** |
||||
* API to toggle the whiteboard. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function toggleWhiteboard() { |
||||
return async (dispatch: IStore['dispatch'], getState: IStore['getState']) => { |
||||
const state = getState(); |
||||
const isAllowed = isWhiteboardAllowed(state); |
||||
const isOpen = isWhiteboardOpen(state); |
||||
|
||||
if (isAllowed) { |
||||
if (isOpen && !isWhiteboardVisible(state)) { |
||||
dispatch(setWhiteboardOpen(true)); |
||||
} else if (isOpen && isWhiteboardVisible(state)) { |
||||
dispatch(setWhiteboardOpen(false)); |
||||
} else if (!isOpen) { |
||||
dispatch(setWhiteboardOpen(true)); |
||||
} |
||||
} else if (typeof APP !== 'undefined') { |
||||
APP.API.notifyWhiteboardStatusChanged(WhiteboardStatus.FORBIDDEN); |
||||
} |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Restricts the whiteboard usage. |
||||
* |
||||
* @param {boolean} shouldCloseWhiteboard - Whether to dismiss the whiteboard. |
||||
* @returns {Function} |
||||
*/ |
||||
export const restrictWhiteboard = (shouldCloseWhiteboard = true) => (dispatch: IStore['dispatch']) => { |
||||
if (shouldCloseWhiteboard) { |
||||
dispatch(setWhiteboardOpen(false)); |
||||
} |
||||
dispatch(resetWhiteboard()); |
||||
sendAnalytics(createRestrictWhiteboardEvent()); |
||||
}; |
@ -0,0 +1,220 @@ |
||||
import { Route } from '@react-navigation/native'; |
||||
import React, { PureComponent } from 'react'; |
||||
import { WithTranslation } from 'react-i18next'; |
||||
import { View, ViewStyle } from 'react-native'; |
||||
import { WebView } from 'react-native-webview'; |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { IReduxState, IStore } from '../../../app/types'; |
||||
import { getCurrentConference } from '../../../base/conference/functions'; |
||||
import { IJitsiConference } from '../../../base/conference/reducer'; |
||||
import { openDialog } from '../../../base/dialog/actions'; |
||||
import { translate } from '../../../base/i18n/functions'; |
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen'; |
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator'; |
||||
import { safeDecodeURIComponent } from '../../../base/util/uri'; |
||||
import { setupWhiteboard } from '../../actions.any'; |
||||
import { WHITEBOARD_ID } from '../../constants'; |
||||
import { getCollabServerUrl, getWhiteboardInfoForURIString } from '../../functions'; |
||||
|
||||
import WhiteboardErrorDialog from './WhiteboardErrorDialog'; |
||||
import styles, { INDICATOR_COLOR } from './styles'; |
||||
|
||||
interface IProps extends WithTranslation { |
||||
|
||||
/** |
||||
* The whiteboard collab server url. |
||||
*/ |
||||
collabServerUrl?: string; |
||||
|
||||
/** |
||||
* The current Jitsi conference. |
||||
*/ |
||||
conference?: IJitsiConference; |
||||
|
||||
/** |
||||
* Redux store dispatch method. |
||||
*/ |
||||
dispatch: IStore['dispatch']; |
||||
|
||||
/** |
||||
* Window location href. |
||||
*/ |
||||
locationHref: string; |
||||
|
||||
/** |
||||
* Default prop for navigating between screen components(React Navigation). |
||||
*/ |
||||
navigation: any; |
||||
|
||||
/** |
||||
* Default prop for navigating between screen components(React Navigation). |
||||
*/ |
||||
route: Route<'', { |
||||
collabDetails: { roomId: string; roomKey: string; }; |
||||
collabServerUrl: string; |
||||
localParticipantName: string; |
||||
}>; |
||||
} |
||||
|
||||
/** |
||||
* Implements a React native component that displays the whiteboard page for a specific room. |
||||
*/ |
||||
class Whiteboard extends PureComponent<IProps> { |
||||
|
||||
/** |
||||
* Initializes a new instance. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
constructor(props: IProps) { |
||||
super(props); |
||||
|
||||
this._onError = this._onError.bind(this); |
||||
this._onNavigate = this._onNavigate.bind(this); |
||||
this._onMessage = this._onMessage.bind(this); |
||||
this._renderLoading = this._renderLoading.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#componentDidMount()}. Invoked |
||||
* immediately after mounting occurs. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
*/ |
||||
componentDidMount() { |
||||
const { navigation, t } = this.props; |
||||
|
||||
navigation.setOptions({ |
||||
headerTitle: t('whiteboard.screenTitle') |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { locationHref, route } = this.props; |
||||
const collabServerUrl = safeDecodeURIComponent(route.params?.collabServerUrl); |
||||
const localParticipantName = safeDecodeURIComponent(route.params?.localParticipantName); |
||||
const collabDetails = route.params?.collabDetails; |
||||
const uri = getWhiteboardInfoForURIString( |
||||
locationHref, |
||||
collabServerUrl, |
||||
collabDetails, |
||||
localParticipantName |
||||
) ?? ''; |
||||
|
||||
return ( |
||||
<JitsiScreen |
||||
safeAreaInsets = { [ 'bottom', 'left', 'right' ] } |
||||
style = { styles.backDrop }> |
||||
<WebView |
||||
incognito = { true } |
||||
javaScriptEnabled = { true } |
||||
nestedScrollEnabled = { true } |
||||
onError = { this._onError } |
||||
onMessage = { this._onMessage } |
||||
onShouldStartLoadWithRequest = { this._onNavigate } |
||||
renderLoading = { this._renderLoading } |
||||
scrollEnabled = { true } |
||||
setSupportMultipleWindows = { false } |
||||
source = {{ uri }} |
||||
startInLoadingState = { true } |
||||
style = { styles.webView } /> |
||||
</JitsiScreen> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Callback to handle the error if the page fails to load. |
||||
* |
||||
* @returns {void} |
||||
*/ |
||||
_onError() { |
||||
this.props.dispatch(openDialog(WhiteboardErrorDialog)); |
||||
} |
||||
|
||||
/** |
||||
* Callback to intercept navigation inside the webview and make the native app handle the whiteboard requests. |
||||
* |
||||
* NOTE: We don't navigate to anywhere else from that view. |
||||
* |
||||
* @param {any} request - The request object. |
||||
* @returns {boolean} |
||||
*/ |
||||
_onNavigate(request: { url: string; }) { |
||||
const { url } = request; |
||||
const { locationHref, route } = this.props; |
||||
const collabServerUrl = route.params?.collabServerUrl; |
||||
const collabDetails = route.params?.collabDetails; |
||||
const localParticipantName = route.params?.localParticipantName; |
||||
|
||||
return url === getWhiteboardInfoForURIString( |
||||
locationHref, |
||||
collabServerUrl, |
||||
collabDetails, |
||||
localParticipantName |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Callback to handle the message events. |
||||
* |
||||
* @param {any} event - The event. |
||||
* @returns {void} |
||||
*/ |
||||
_onMessage(event: any) { |
||||
const { collabServerUrl, conference } = this.props; |
||||
const collabDetails = JSON.parse(event.nativeEvent.data); |
||||
|
||||
if (collabDetails?.roomId && collabDetails?.roomKey) { |
||||
this.props.dispatch(setupWhiteboard({ collabDetails })); |
||||
|
||||
// Broadcast the collab details.
|
||||
conference?.getMetadataHandler().setMetadata(WHITEBOARD_ID, { |
||||
collabServerUrl, |
||||
collabDetails |
||||
}); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Renders the loading indicator. |
||||
* |
||||
* @returns {React$Component<any>} |
||||
*/ |
||||
_renderLoading() { |
||||
return ( |
||||
<View style = { styles.indicatorWrapper as ViewStyle }> |
||||
<LoadingIndicator |
||||
color = { INDICATOR_COLOR } |
||||
size = 'large' /> |
||||
</View> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the associated |
||||
* {@code WaitForOwnerDialog}'s props. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @private |
||||
* @returns {IProps} |
||||
*/ |
||||
function mapStateToProps(state: IReduxState) { |
||||
const { locationURL } = state['features/base/connection']; |
||||
const { href = '' } = locationURL ?? {}; |
||||
|
||||
return { |
||||
conference: getCurrentConference(state), |
||||
collabServerUrl: getCollabServerUrl(state), |
||||
locationHref: href |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(mapStateToProps)(Whiteboard)); |
@ -0,0 +1,43 @@ |
||||
import { connect } from 'react-redux'; |
||||
|
||||
import { IReduxState } from '../../../app/types'; |
||||
import { translate } from '../../../base/i18n/functions'; |
||||
import { IconWhiteboard } from '../../../base/icons/svg'; |
||||
import AbstractButton, { IProps as AbstractButtonProps } from '../../../base/toolbox/components/AbstractButton'; |
||||
import { setWhiteboardOpen } from '../../actions.any'; |
||||
import { isWhiteboardButtonVisible } from '../../functions'; |
||||
|
||||
/** |
||||
* Component that renders a toolbar button for the whiteboard. |
||||
*/ |
||||
class WhiteboardButton extends AbstractButton<AbstractButtonProps> { |
||||
accessibilityLabel = 'toolbar.accessibilityLabel.showWhiteboard'; |
||||
icon = IconWhiteboard; |
||||
label = 'toolbar.showWhiteboard'; |
||||
tooltip = 'toolbar.showWhiteboard'; |
||||
|
||||
/** |
||||
* Handles clicking / pressing the button, and opens the whiteboard view. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_handleClick() { |
||||
this.props.dispatch(setWhiteboardOpen(true)); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 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 { |
||||
visible: isWhiteboardButtonVisible(state) |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(_mapStateToProps)(WhiteboardButton)); |
@ -0,0 +1,15 @@ |
||||
import React from 'react'; |
||||
|
||||
import AlertDialog from '../../../base/dialog/components/native/AlertDialog'; |
||||
|
||||
/** |
||||
* Dialog to inform the user that we couldn't load the whiteboard. |
||||
* |
||||
* @returns {JSX.Element} |
||||
*/ |
||||
const WhiteboardErrorDialog = () => ( |
||||
<AlertDialog |
||||
contentKey = 'info.whiteboardError' /> |
||||
); |
||||
|
||||
export default WhiteboardErrorDialog; |
@ -0,0 +1,43 @@ |
||||
import React from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { Text, TextStyle } from 'react-native'; |
||||
import { useSelector } from 'react-redux'; |
||||
|
||||
import ConfirmDialog from '../../../base/dialog/components/native/ConfirmDialog'; |
||||
import Link from '../../../base/react/components/native/Link'; |
||||
import { getWhiteboardConfig } from '../../functions'; |
||||
|
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* Component that renders the whiteboard user limit dialog. |
||||
* |
||||
* @returns {JSX.Element} |
||||
*/ |
||||
const WhiteboardLimitDialog = () => { |
||||
const { t } = useTranslation(); |
||||
const { limitUrl } = useSelector(getWhiteboardConfig); |
||||
|
||||
return ( |
||||
<ConfirmDialog |
||||
cancelLabel = { 'dialog.Ok' } |
||||
descriptionKey = { 'dialog.whiteboardLimitContent' } |
||||
isConfirmHidden = { true } |
||||
title = { 'dialog.whiteboardLimitTitle' }> |
||||
{limitUrl && ( |
||||
<Text style = { styles.limitUrlText as TextStyle }> |
||||
{` ${t('dialog.whiteboardLimitReference')} |
||||
`}
|
||||
<Link |
||||
style = { styles.limitUrl } |
||||
url = { limitUrl }> |
||||
{t('dialog.whiteboardLimitReferenceUrl')} |
||||
</Link> |
||||
. |
||||
</Text> |
||||
)} |
||||
</ConfirmDialog> |
||||
); |
||||
}; |
||||
|
||||
export default WhiteboardLimitDialog; |
@ -0,0 +1,37 @@ |
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native'; |
||||
|
||||
export const INDICATOR_COLOR = BaseTheme.palette.ui07; |
||||
|
||||
const WV_BACKGROUND = BaseTheme.palette.ui03; |
||||
|
||||
export default { |
||||
|
||||
backDrop: { |
||||
backgroundColor: WV_BACKGROUND, |
||||
flex: 1 |
||||
}, |
||||
|
||||
indicatorWrapper: { |
||||
alignItems: 'center', |
||||
backgroundColor: BaseTheme.palette.ui10, |
||||
height: '100%', |
||||
justifyContent: 'center' |
||||
}, |
||||
|
||||
webView: { |
||||
backgroundColor: WV_BACKGROUND, |
||||
flex: 1 |
||||
}, |
||||
|
||||
limitUrlText: { |
||||
alignItems: 'center', |
||||
display: 'flex', |
||||
marginBottom: BaseTheme.spacing[2], |
||||
textAlign: 'center' |
||||
}, |
||||
|
||||
limitUrl: { |
||||
color: BaseTheme.palette.link01, |
||||
fontWeight: 'bold' |
||||
} |
||||
}; |
@ -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; |
@ -0,0 +1,91 @@ |
||||
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} |
||||
*/ |
||||
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)); |
||||
}, 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 |
||||
*/ |
||||
_createMainElement(component: ComponentType<any>, props: Object) { |
||||
return ( |
||||
<JitsiThemeProvider> |
||||
<GlobalStyles /> |
||||
{super._createMainElement(component, props)} |
||||
</JitsiThemeProvider> |
||||
); |
||||
} |
||||
|
||||
/** |
||||
* Renders the platform specific dialog container. |
||||
* |
||||
* @returns {React$Element} |
||||
*/ |
||||
_renderDialogContainer() { |
||||
return null; |
||||
} |
||||
} |
@ -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; |
@ -0,0 +1,3 @@ |
||||
import { getLogger } from '../base/logging/functions'; |
||||
|
||||
export default getLogger('features/whiteboard'); |
@ -0,0 +1,89 @@ |
||||
import { createOpenWhiteboardEvent } from '../analytics/AnalyticsEvents'; |
||||
import { sendAnalytics } from '../analytics/functions'; |
||||
import { IStore } from '../app/types'; |
||||
import { getCurrentConference } from '../base/conference/functions'; |
||||
import { JitsiConferenceEvents } from '../base/lib-jitsi-meet'; |
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; |
||||
import StateListenerRegistry from '../base/redux/StateListenerRegistry'; |
||||
|
||||
import { SET_WHITEBOARD_OPEN } from './actionTypes'; |
||||
import { |
||||
notifyWhiteboardLimit, |
||||
resetWhiteboard, |
||||
restrictWhiteboard, |
||||
setWhiteboardOpen, |
||||
setupWhiteboard |
||||
} from './actions'; |
||||
import { WHITEBOARD_ID } from './constants'; |
||||
import { |
||||
isWhiteboardOpen, |
||||
shouldEnforceUserLimit, |
||||
shouldNotifyUserLimit |
||||
} from './functions'; |
||||
|
||||
MiddlewareRegistry.register((store: IStore) => next => action => { |
||||
const state = store.getState(); |
||||
|
||||
switch (action.type) { |
||||
case SET_WHITEBOARD_OPEN: { |
||||
const enforceUserLimit = shouldEnforceUserLimit(state); |
||||
const notifyUserLimit = shouldNotifyUserLimit(state); |
||||
|
||||
if (action.isOpen && !enforceUserLimit && !notifyUserLimit) { |
||||
sendAnalytics(createOpenWhiteboardEvent()); |
||||
|
||||
return next(action); |
||||
} |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
return next(action); |
||||
}); |
||||
|
||||
/** |
||||
* Set up state change listener to perform maintenance tasks when the conference |
||||
* is left or failed, e.g. Disable the whiteboard if it's left open. |
||||
*/ |
||||
StateListenerRegistry.register( |
||||
state => getCurrentConference(state), |
||||
(conference, { dispatch }, previousConference): void => { |
||||
if (conference !== previousConference) { |
||||
dispatch(resetWhiteboard()); |
||||
} |
||||
if (conference && !previousConference) { |
||||
conference.on(JitsiConferenceEvents.METADATA_UPDATED, (metadata: any) => { |
||||
if (metadata[WHITEBOARD_ID]) { |
||||
dispatch(setupWhiteboard({ |
||||
collabDetails: metadata[WHITEBOARD_ID].collabDetails |
||||
})); |
||||
dispatch(setWhiteboardOpen(true)); |
||||
} |
||||
}); |
||||
} |
||||
}); |
||||
|
||||
/** |
||||
* Set up state change listener to limit whiteboard access. |
||||
*/ |
||||
StateListenerRegistry.register( |
||||
state => shouldEnforceUserLimit(state), |
||||
(enforceUserLimit, { dispatch, getState }): void => { |
||||
if (isWhiteboardOpen(getState()) && enforceUserLimit) { |
||||
dispatch(restrictWhiteboard()); |
||||
} |
||||
} |
||||
); |
||||
|
||||
/** |
||||
* Set up state change listener to notify about whiteboard usage. |
||||
*/ |
||||
StateListenerRegistry.register( |
||||
state => shouldNotifyUserLimit(state), |
||||
(notifyUserLimit, { dispatch, getState }, prevNotifyUserLimit): void => { |
||||
if (isWhiteboardOpen(getState()) && notifyUserLimit && !prevNotifyUserLimit) { |
||||
dispatch(notifyWhiteboardLimit()); |
||||
} |
||||
} |
||||
); |
@ -0,0 +1,80 @@ |
||||
import { AnyAction } from 'redux'; |
||||
|
||||
import { IStore } from '../app/types'; |
||||
import { hideDialog, openDialog } from '../base/dialog/actions'; |
||||
import { isDialogOpen } from '../base/dialog/functions'; |
||||
import { getLocalParticipant } from '../base/participants/functions'; |
||||
import MiddlewareRegistry from '../base/redux/MiddlewareRegistry'; |
||||
import { navigateRoot } from '../mobile/navigation/rootNavigationContainerRef'; |
||||
import { screen } from '../mobile/navigation/routes'; |
||||
|
||||
import { SET_WHITEBOARD_OPEN } from './actionTypes'; |
||||
import { |
||||
notifyWhiteboardLimit, |
||||
restrictWhiteboard |
||||
} from './actions'; |
||||
import WhiteboardLimitDialog from './components/native/WhiteboardLimitDialog'; |
||||
import { |
||||
getCollabDetails, |
||||
getCollabServerUrl, |
||||
shouldEnforceUserLimit, |
||||
shouldNotifyUserLimit |
||||
} from './functions'; |
||||
import './middleware.any'; |
||||
|
||||
/** |
||||
* Middleware which intercepts whiteboard actions to handle changes to the related state. |
||||
* |
||||
* @param {Store} store - The redux store. |
||||
* @returns {Function} |
||||
*/ |
||||
MiddlewareRegistry.register((store: IStore) => (next: Function) => async (action: AnyAction) => { |
||||
const { dispatch, getState } = store; |
||||
const state = getState(); |
||||
|
||||
switch (action.type) { |
||||
case SET_WHITEBOARD_OPEN: { |
||||
const enforceUserLimit = shouldEnforceUserLimit(state); |
||||
const notifyUserLimit = shouldNotifyUserLimit(state); |
||||
|
||||
if (enforceUserLimit) { |
||||
dispatch(restrictWhiteboard(false)); |
||||
dispatch(openDialog(WhiteboardLimitDialog)); |
||||
|
||||
return next(action); |
||||
} |
||||
|
||||
if (action.isOpen) { |
||||
if (enforceUserLimit) { |
||||
dispatch(restrictWhiteboard()); |
||||
|
||||
return next(action); |
||||
} |
||||
|
||||
if (notifyUserLimit) { |
||||
dispatch(notifyWhiteboardLimit()); |
||||
} |
||||
|
||||
if (isDialogOpen(state, WhiteboardLimitDialog)) { |
||||
dispatch(hideDialog(WhiteboardLimitDialog)); |
||||
} |
||||
|
||||
const collabDetails = getCollabDetails(state); |
||||
const collabServerUrl = getCollabServerUrl(state); |
||||
const localParticipantName = getLocalParticipant(state)?.name; |
||||
|
||||
navigateRoot(screen.conference.whiteboard, { |
||||
collabDetails, |
||||
collabServerUrl, |
||||
localParticipantName |
||||
}); |
||||
|
||||
return next(action); |
||||
} |
||||
|
||||
break; |
||||
} |
||||
} |
||||
|
||||
return next(action); |
||||
}); |
@ -0,0 +1,33 @@ |
||||
<html xmlns="http://www.w3.org/1999/html"> |
||||
|
||||
<head> |
||||
<meta charset="utf-8"> |
||||
<meta http-equiv="content-type" content="text/html;charset=utf-8"> |
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
||||
<!--#include virtual="/base.html" --> |
||||
<link rel="stylesheet" href="css/all.css"> |
||||
<script> |
||||
window.EXCALIDRAW_ASSET_PATH = 'libs/'; |
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => { |
||||
if (!JitsiMeetJS.app) { |
||||
return; |
||||
} |
||||
|
||||
JitsiMeetJS.app.renderEntryPoint({ |
||||
Component: JitsiMeetJS.app.entryPoints.WHITEBOARD |
||||
}); |
||||
}); |
||||
</script> |
||||
<!--#include virtual="/title.html" --> |
||||
<script><!--#include virtual="/config.js" --></script> |
||||
<script><!--#include virtual="/interface_config.js" --></script> |
||||
<script src="libs/lib-jitsi-meet.min.js?v=139"></script> |
||||
<script src="libs/app.bundle.min.js?v=139"></script> |
||||
</head> |
||||
|
||||
<body> |
||||
<div id="react" role="main"></div> |
||||
</body> |
||||
|
||||
</html> |
Loading…
Reference in new issue