mirror of https://github.com/jitsi/jitsi-meet
feat(overlay): native page reload dialog (#12667)
feat(overlay): native feature removal + replaced PageReloadOverlay with PageReloadDialogpull/12909/head jitsi-meet_8309
parent
22ded30b61
commit
3cb0df579c
@ -1,43 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component } from 'react'; |
||||
|
||||
import { Container, Text } from '../../react'; |
||||
import { type StyleType } from '../../styles'; |
||||
|
||||
import styles from './styles'; |
||||
|
||||
type Props = { |
||||
|
||||
/** |
||||
* Children of the component. |
||||
*/ |
||||
children: string | React$Node, |
||||
|
||||
style: ?StyleType |
||||
}; |
||||
|
||||
/** |
||||
* Generic dialog content container to provide the same styling for all custom |
||||
* dialogs. |
||||
*/ |
||||
export default class DialogContent extends Component<Props> { |
||||
/** |
||||
* Implements {@code Component#render}. |
||||
* |
||||
* @inheritdoc |
||||
*/ |
||||
render() { |
||||
const { children, style } = this.props; |
||||
|
||||
const childrenComponent = typeof children === 'string' |
||||
? <Text style = { style }>{ children }</Text> |
||||
: children; |
||||
|
||||
return ( |
||||
<Container style = { styles.dialogContainer }> |
||||
{ childrenComponent } |
||||
</Container> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,226 @@ |
||||
/* eslint-disable lines-around-comment */ |
||||
|
||||
// @ts-ignore
|
||||
import { randomInt } from '@jitsi/js-utils/random'; |
||||
import React, { Component } from 'react'; |
||||
import { WithTranslation } from 'react-i18next'; |
||||
import type { Dispatch } from 'redux'; |
||||
|
||||
import { appNavigate, reloadNow } from '../../../../app/actions.native'; |
||||
import { IReduxState } from '../../../../app/types'; |
||||
import { translate } from '../../../i18n/functions'; |
||||
import { isFatalJitsiConnectionError } from '../../../lib-jitsi-meet/functions.native'; |
||||
import { connect } from '../../../redux/functions'; |
||||
// @ts-ignore
|
||||
import logger from '../../logger'; |
||||
|
||||
// @ts-ignore
|
||||
import ConfirmDialog from './ConfirmDialog'; |
||||
|
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of |
||||
* {@link PageReloadDialog}. |
||||
*/ |
||||
interface IPageReloadDialogProps extends WithTranslation { |
||||
dispatch: Dispatch<any>; |
||||
isNetworkFailure: boolean; |
||||
reason: string; |
||||
} |
||||
|
||||
/** |
||||
* The type of the React {@code Component} state of |
||||
* {@link PageReloadDialog}. |
||||
*/ |
||||
interface IPageReloadDialogState { |
||||
message: string; |
||||
timeLeft: number; |
||||
timeoutSeconds: number; |
||||
title: string; |
||||
} |
||||
|
||||
/** |
||||
* Implements a React Component that is shown before the |
||||
* conference is reloaded. |
||||
* Shows a warning message and counts down towards the re-load. |
||||
*/ |
||||
class PageReloadDialog extends Component<IPageReloadDialogProps, IPageReloadDialogState> { |
||||
|
||||
// @ts-ignore
|
||||
_interval: IntervalID; |
||||
|
||||
/** |
||||
* Initializes a new PageReloadOverlay instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
* @public |
||||
*/ |
||||
constructor(props: IPageReloadDialogProps) { |
||||
super(props); |
||||
|
||||
const timeoutSeconds = 10 + randomInt(0, 20); |
||||
|
||||
let message, title; |
||||
|
||||
if (this.props.isNetworkFailure) { |
||||
title = 'dialog.conferenceDisconnectTitle'; |
||||
message = 'dialog.conferenceDisconnectMsg'; |
||||
} else { |
||||
title = 'dialog.conferenceReloadTitle'; |
||||
message = 'dialog.conferenceReloadMsg'; |
||||
} |
||||
|
||||
this.state = { |
||||
message, |
||||
timeLeft: timeoutSeconds, |
||||
timeoutSeconds, |
||||
title |
||||
}; |
||||
|
||||
this._onCancel = this._onCancel.bind(this); |
||||
this._onReloadNow = this._onReloadNow.bind(this); |
||||
} |
||||
|
||||
/** |
||||
* React Component method that executes once component is mounted. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
*/ |
||||
componentDidMount() { |
||||
const { dispatch } = this.props; |
||||
const { timeLeft } = this.state; |
||||
|
||||
logger.info( |
||||
`The conference will be reloaded after ${ |
||||
this.state.timeoutSeconds} seconds.`);
|
||||
|
||||
this._interval |
||||
= setInterval( |
||||
() => { |
||||
if (timeLeft === 0) { |
||||
if (this._interval) { |
||||
clearInterval(this._interval); |
||||
this._interval = undefined; |
||||
} |
||||
|
||||
dispatch(reloadNow()); |
||||
} else { |
||||
this.setState(prevState => { |
||||
return { |
||||
timeLeft: prevState.timeLeft - 1 |
||||
}; |
||||
}); |
||||
} |
||||
}, |
||||
1000); |
||||
} |
||||
|
||||
/** |
||||
* Clears the timer interval. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {void} |
||||
*/ |
||||
componentWillUnmount() { |
||||
if (this._interval) { |
||||
clearInterval(this._interval); |
||||
this._interval = undefined; |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Handle clicking of the "Cancel" button. It will navigate back to the |
||||
* welcome page. |
||||
* |
||||
* @private |
||||
* @returns {boolean} |
||||
*/ |
||||
_onCancel() { |
||||
clearInterval(this._interval); |
||||
this.props.dispatch(appNavigate(undefined)); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Handle clicking on the "Reload Now" button. It will navigate to the same |
||||
* conference URL as before immediately, without waiting for the timer to |
||||
* kick in. |
||||
* |
||||
* @private |
||||
* @returns {boolean} |
||||
*/ |
||||
_onReloadNow() { |
||||
clearInterval(this._interval); |
||||
this.props.dispatch(reloadNow()); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { t } = this.props; |
||||
const { message, timeLeft, title } = this.state; |
||||
|
||||
return ( |
||||
<ConfirmDialog |
||||
cancelLabel = 'dialog.Cancel' |
||||
confirmLabel = 'dialog.rejoinNow' |
||||
descriptionKey = { `${t(message, { seconds: timeLeft })}` } |
||||
onCancel = { this._onCancel } |
||||
onSubmit = { this._onReloadNow } |
||||
title = { title } /> |
||||
); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Maps (parts of) the redux state to the associated component's props. |
||||
* |
||||
* @param {Object} state - The redux state. |
||||
* @protected |
||||
* @returns {{ |
||||
* message: string, |
||||
* reason: string, |
||||
* title: string |
||||
* }} |
||||
*/ |
||||
function mapStateToProps(state: IReduxState) { |
||||
const { error: conferenceError } = state['features/base/conference']; |
||||
const { error: configError } = state['features/base/config']; |
||||
const { error: connectionError } = state['features/base/connection']; |
||||
const { fatalError } = state['features/overlay']; |
||||
|
||||
const fatalConnectionError |
||||
// @ts-ignore
|
||||
= connectionError && isFatalJitsiConnectionError(connectionError); |
||||
const fatalConfigError = fatalError === configError; |
||||
|
||||
const isNetworkFailure = fatalConfigError || fatalConnectionError; |
||||
|
||||
let reason; |
||||
|
||||
if (conferenceError) { |
||||
reason = `error.conference.${conferenceError.name}`; |
||||
} else if (connectionError) { |
||||
reason = `error.conference.${connectionError.name}`; |
||||
} else if (configError) { |
||||
reason = `error.config.${configError.name}`; |
||||
} else { |
||||
logger.error('No reload reason defined!'); |
||||
} |
||||
|
||||
return { |
||||
isNetworkFailure, |
||||
reason |
||||
}; |
||||
} |
||||
|
||||
export default translate(connect(mapStateToProps)(PageReloadDialog)); |
@ -1,14 +0,0 @@ |
||||
import { BoxModel, createStyleSheet } from '../../styles'; |
||||
|
||||
/** |
||||
* The React {@code Component} styles of {@code Dialog}. |
||||
*/ |
||||
export default createStyleSheet({ |
||||
/** |
||||
* Unified container for a consistent Dialog style. |
||||
*/ |
||||
dialogContainer: { |
||||
paddingHorizontal: BoxModel.padding, |
||||
paddingVertical: 1.5 * BoxModel.padding |
||||
} |
||||
}); |
@ -1,5 +0,0 @@ |
||||
/** |
||||
* Placeholder styles for web to be able to use cross platform components |
||||
* unmodified such as {@code DialogContent}. |
||||
*/ |
||||
export default {}; |
@ -1,14 +1,18 @@ |
||||
// @flow
|
||||
/* eslint-disable lines-around-comment */ |
||||
|
||||
import React from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
import { SafeAreaView, Text, View } from 'react-native'; |
||||
|
||||
// @ts-ignore
|
||||
import JitsiScreen from '../../../base/modal/components/JitsiScreen'; |
||||
import { LoadingIndicator } from '../../../base/react'; |
||||
// @ts-ignore
|
||||
import LoadingIndicator from '../../../base/react/components/native/LoadingIndicator'; |
||||
|
||||
// @ts-ignore
|
||||
import { TEXT_COLOR, navigationStyles } from './styles'; |
||||
|
||||
|
||||
const ConnectingPage = () => { |
||||
const { t } = useTranslation(); |
||||
|
@ -0,0 +1,31 @@ |
||||
/* eslint-disable max-len */ |
||||
|
||||
// @ts-ignore
|
||||
import { PageReloadDialog, openDialog } from '../base/dialog'; |
||||
|
||||
|
||||
/** |
||||
* Signals that the prompt for media permission is visible or not. |
||||
* |
||||
* @param {boolean} _isVisible - If the value is true - the prompt for media |
||||
* permission is visible otherwise the value is false/undefined. |
||||
* @param {string} _browser - The name of the current browser. |
||||
* @public |
||||
* @returns {{ |
||||
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, |
||||
* browser: {string}, |
||||
* isVisible: {boolean} |
||||
* }} |
||||
*/ |
||||
export function mediaPermissionPromptVisibilityChanged(_isVisible: boolean, _browser: string) { |
||||
// Dummy.
|
||||
} |
||||
|
||||
/** |
||||
* Opens {@link PageReloadDialog}. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function openPageReloadDialog() { |
||||
return openDialog(PageReloadDialog); |
||||
} |
@ -1,62 +0,0 @@ |
||||
import { |
||||
MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, |
||||
SET_FATAL_ERROR, |
||||
SET_PAGE_RELOAD_OVERLAY_CANCELED |
||||
} from './actionTypes'; |
||||
|
||||
/** |
||||
* Signals that the prompt for media permission is visible or not. |
||||
* |
||||
* @param {boolean} isVisible - If the value is true - the prompt for media |
||||
* permission is visible otherwise the value is false/undefined. |
||||
* @param {string} browser - The name of the current browser. |
||||
* @public |
||||
* @returns {{ |
||||
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, |
||||
* browser: {string}, |
||||
* isVisible: {boolean} |
||||
* }} |
||||
*/ |
||||
export function mediaPermissionPromptVisibilityChanged(isVisible: boolean, browser: string) { |
||||
return { |
||||
type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, |
||||
browser, |
||||
isVisible |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* The action indicates that an unrecoverable error has occurred and the reload |
||||
* screen will be displayed or hidden. |
||||
* |
||||
* @param {Object} fatalError - A critical error which was not claimed by any |
||||
* feature for error recovery (the recoverable flag was not set). If |
||||
* {@code undefined} then any fatal error currently stored will be discarded. |
||||
* @returns {{ |
||||
* type: SET_FATAL_ERROR, |
||||
* fatalError: ?Error |
||||
* }} |
||||
*/ |
||||
export function setFatalError(fatalError?: Object) { |
||||
return { |
||||
type: SET_FATAL_ERROR, |
||||
fatalError |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* The action indicates that the overlay was canceled. |
||||
* |
||||
* @param {Object} error - The error that caused the display of the overlay. |
||||
* |
||||
* @returns {{ |
||||
* type: SET_PAGE_RELOAD_OVERLAY_CANCELED, |
||||
* error: ?Error |
||||
* }} |
||||
*/ |
||||
export function setPageReloadOverlayCanceled(error: Object) { |
||||
return { |
||||
type: SET_PAGE_RELOAD_OVERLAY_CANCELED, |
||||
error |
||||
}; |
||||
} |
@ -0,0 +1,32 @@ |
||||
import { MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED } from './actionTypes'; |
||||
|
||||
|
||||
/** |
||||
* Signals that the prompt for media permission is visible or not. |
||||
* |
||||
* @param {boolean} isVisible - If the value is true - the prompt for media |
||||
* permission is visible otherwise the value is false/undefined. |
||||
* @param {string} browser - The name of the current browser. |
||||
* @public |
||||
* @returns {{ |
||||
* type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, |
||||
* browser: {string}, |
||||
* isVisible: {boolean} |
||||
* }} |
||||
*/ |
||||
export function mediaPermissionPromptVisibilityChanged(isVisible: boolean, browser: string) { |
||||
return { |
||||
type: MEDIA_PERMISSION_PROMPT_VISIBILITY_CHANGED, |
||||
browser, |
||||
isVisible |
||||
}; |
||||
} |
||||
|
||||
/** |
||||
* Opens {@link PageReloadDialog}. |
||||
* |
||||
* @returns {Function} |
||||
*/ |
||||
export function openPageReloadDialog() { |
||||
// Dummy
|
||||
} |
@ -1,3 +0,0 @@ |
||||
// @flow
|
||||
|
||||
export * from './native'; |
@ -1,3 +0,0 @@ |
||||
// @flow
|
||||
|
||||
export * from './web'; |
@ -1,4 +0,0 @@ |
||||
// @flow
|
||||
|
||||
export { default as OverlayContainer } from './OverlayContainer'; |
||||
export * from './_'; |
@ -1,38 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React, { Component, type Node } from 'react'; |
||||
import { SafeAreaView, View } from 'react-native'; |
||||
|
||||
import styles from './styles'; |
||||
|
||||
/** |
||||
* The type of the React {@code Component} props of {@code OverlayFrame}. |
||||
*/ |
||||
type Props = { |
||||
|
||||
/** |
||||
* The children components to be displayed into the overlay frame. |
||||
*/ |
||||
children: Node, |
||||
}; |
||||
|
||||
/** |
||||
* Implements a React component to act as the frame for overlays. |
||||
*/ |
||||
export default class OverlayFrame extends Component<Props> { |
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
return ( |
||||
<View style = { styles.container }> |
||||
<SafeAreaView style = { styles.safeContainer } > |
||||
{ this.props.children } |
||||
</SafeAreaView> |
||||
</View> |
||||
); |
||||
} |
||||
} |
@ -1,95 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import React from 'react'; |
||||
|
||||
import { appNavigate, reloadNow } from '../../../app/actions'; |
||||
import { ConfirmDialog } from '../../../base/dialog'; |
||||
import { translate } from '../../../base/i18n'; |
||||
import { connect } from '../../../base/redux'; |
||||
import { setFatalError, setPageReloadOverlayCanceled } from '../../actions'; |
||||
import AbstractPageReloadOverlay, { |
||||
type Props, |
||||
abstractMapStateToProps |
||||
} from '../AbstractPageReloadOverlay'; |
||||
|
||||
import OverlayFrame from './OverlayFrame'; |
||||
|
||||
|
||||
/** |
||||
* Implements a React Component for page reload overlay. Shown before the |
||||
* conference is reloaded. Shows a warning message and counts down towards the |
||||
* reload. |
||||
*/ |
||||
class PageReloadOverlay extends AbstractPageReloadOverlay<Props> { |
||||
_interval: IntervalID; |
||||
|
||||
/** |
||||
* Initializes a new PageReloadOverlay instance. |
||||
* |
||||
* @param {Object} props - The read-only properties with which the new |
||||
* instance is to be initialized. |
||||
* @public |
||||
*/ |
||||
constructor(props) { |
||||
super(props); |
||||
|
||||
this._onCancel = this._onCancel.bind(this); |
||||
this._onReloadNow = this._onReloadNow.bind(this); |
||||
} |
||||
|
||||
_onCancel: () => void; |
||||
|
||||
/** |
||||
* Handle clicking of the "Cancel" button. It will navigate back to the |
||||
* welcome page. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onCancel() { |
||||
clearInterval(this._interval); |
||||
this.props.dispatch(setPageReloadOverlayCanceled(this.props.error)); |
||||
this.props.dispatch(setFatalError(undefined)); |
||||
this.props.dispatch(appNavigate(undefined)); |
||||
} |
||||
|
||||
_onReloadNow: () => void; |
||||
|
||||
/** |
||||
* Handle clicking on the "Reload Now" button. It will navigate to the same |
||||
* conference URL as before immediately, without waiting for the timer to |
||||
* kick in. |
||||
* |
||||
* @private |
||||
* @returns {void} |
||||
*/ |
||||
_onReloadNow() { |
||||
clearInterval(this._interval); |
||||
this.props.dispatch(reloadNow()); |
||||
} |
||||
|
||||
/** |
||||
* Implements React's {@link Component#render()}. |
||||
* |
||||
* @inheritdoc |
||||
* @returns {ReactElement} |
||||
*/ |
||||
render() { |
||||
const { t } = this.props; |
||||
const { message, timeLeft, title } = this.state; |
||||
|
||||
return ( |
||||
<OverlayFrame> |
||||
<ConfirmDialog |
||||
cancelLabel = 'dialog.Cancel' |
||||
confirmLabel = 'dialog.rejoinNow' |
||||
descriptionKey = { `${t(message, { seconds: timeLeft })}` } |
||||
onCancel = { this._onCancel } |
||||
onSubmit = { this._onReloadNow } |
||||
title = { title } /> |
||||
</OverlayFrame> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default translate(connect(abstractMapStateToProps)(PageReloadOverlay)); |
@ -1,4 +0,0 @@ |
||||
// @flow
|
||||
|
||||
export { default as OverlayFrame } from './OverlayFrame'; |
||||
export { default as PageReloadOverlay } from './PageReloadOverlay'; |
@ -1,24 +0,0 @@ |
||||
// @flow
|
||||
|
||||
import { StyleSheet } from 'react-native'; |
||||
|
||||
import BaseTheme from '../../../base/ui/components/BaseTheme.native'; |
||||
|
||||
|
||||
/** |
||||
* The React {@code Component} styles of the overlay feature. |
||||
*/ |
||||
export default { |
||||
/** |
||||
* Style for a backdrop overlay covering the screen the the overlay is |
||||
* rendered. |
||||
*/ |
||||
container: { |
||||
...StyleSheet.absoluteFillObject, |
||||
backgroundColor: BaseTheme.palette.ui00 |
||||
}, |
||||
|
||||
safeContainer: { |
||||
flex: 1 |
||||
} |
||||
}; |
@ -1,7 +0,0 @@ |
||||
// @flow
|
||||
|
||||
export { default as OverlayFrame } from './OverlayFrame'; |
||||
|
||||
export { default as PageReloadOverlay } from './PageReloadOverlay'; |
||||
export { default as SuspendedOverlay } from './SuspendedOverlay'; |
||||
export { default as UserMediaPermissionsOverlay } from './UserMediaPermissionsOverlay'; |
@ -1,5 +0,0 @@ |
||||
// @flow
|
||||
|
||||
export * from './actions'; |
||||
export * from './components'; |
||||
export * from './functions'; |
@ -1,15 +0,0 @@ |
||||
import { ReactElement } from 'react'; |
||||
|
||||
// @ts-ignore
|
||||
import { PageReloadOverlay } from './components/native'; |
||||
|
||||
/** |
||||
* Returns the list of available platform specific overlays. |
||||
* |
||||
* @returns {Array<ReactElement>} |
||||
*/ |
||||
export function getOverlays(): Array<ReactElement> { |
||||
return [ |
||||
PageReloadOverlay |
||||
]; |
||||
} |
@ -1,20 +0,0 @@ |
||||
import { |
||||
PageReloadOverlay, |
||||
SuspendedOverlay, |
||||
UserMediaPermissionsOverlay |
||||
|
||||
// @ts-ignore
|
||||
} from './components/web'; |
||||
|
||||
/** |
||||
* Returns the list of available platform specific overlays. |
||||
* |
||||
* @returns {Array<Object>} |
||||
*/ |
||||
export function getOverlays(): Array<Object> { |
||||
return [ |
||||
PageReloadOverlay, |
||||
SuspendedOverlay, |
||||
UserMediaPermissionsOverlay |
||||
]; |
||||
} |
Loading…
Reference in new issue