This commit is contained in:
247
react/features/whiteboard/components/native/Whiteboard.tsx
Normal file
247
react/features/whiteboard/components/native/Whiteboard.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
import { Route } from '@react-navigation/native';
|
||||
import React, { PureComponent } from 'react';
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
import { Platform, 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 { IconCloseLarge } from '../../../base/icons/svg';
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen';
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator';
|
||||
import { safeDecodeURIComponent } from '../../../base/util/uri';
|
||||
import HeaderNavigationButton
|
||||
from '../../../mobile/navigation/components/HeaderNavigationButton';
|
||||
import {
|
||||
goBack
|
||||
} from '../../../mobile/navigation/components/conference/ConferenceNavigationContainerRef';
|
||||
import { setupWhiteboard } from '../../actions.native';
|
||||
import { WHITEBOARD_ID } from '../../constants';
|
||||
import { getWhiteboardInfoForURIString } from '../../functions';
|
||||
import logger from '../../logger';
|
||||
|
||||
import WhiteboardErrorDialog from './WhiteboardErrorDialog';
|
||||
import styles, { INDICATOR_COLOR } from './styles';
|
||||
|
||||
interface IProps extends WithTranslation {
|
||||
|
||||
/**
|
||||
* 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}
|
||||
*/
|
||||
override componentDidMount() {
|
||||
const { navigation, t } = this.props;
|
||||
const headerLeft = () => {
|
||||
if (Platform.OS === 'ios') {
|
||||
return (
|
||||
<HeaderNavigationButton
|
||||
label = { t('dialog.close') }
|
||||
onPress = { goBack } />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HeaderNavigationButton
|
||||
onPress = { goBack }
|
||||
src = { IconCloseLarge } />
|
||||
);
|
||||
};
|
||||
|
||||
navigation.setOptions({ headerLeft });
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements React's {@link Component#render()}.
|
||||
*
|
||||
* @inheritdoc
|
||||
*/
|
||||
override 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
|
||||
domStorageEnabled = { false }
|
||||
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 }
|
||||
webviewDebuggingEnabled = { true } />
|
||||
</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 { conference, dispatch } = this.props;
|
||||
const collabData = JSON.parse(event.nativeEvent.data);
|
||||
|
||||
if (!collabData) {
|
||||
logger.error('Message payload is missing whiteboard collaboration data');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { collabDetails, collabServerUrl } = collabData;
|
||||
|
||||
if (collabDetails?.roomId && collabDetails?.roomKey && collabServerUrl) {
|
||||
dispatch(setupWhiteboard({
|
||||
collabDetails,
|
||||
collabServerUrl
|
||||
}));
|
||||
|
||||
// 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),
|
||||
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> {
|
||||
override accessibilityLabel = 'toolbar.accessibilityLabel.showWhiteboard';
|
||||
override icon = IconWhiteboard;
|
||||
override label = 'toolbar.showWhiteboard';
|
||||
override tooltip = 'toolbar.showWhiteboard';
|
||||
|
||||
/**
|
||||
* Handles clicking / pressing the button, and opens the whiteboard view.
|
||||
*
|
||||
* @private
|
||||
* @returns {void}
|
||||
*/
|
||||
override _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;
|
||||
37
react/features/whiteboard/components/native/styles.ts
Normal file
37
react/features/whiteboard/components/native/styles.ts
Normal file
@@ -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'
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user