init
Some checks failed
Close stale issues and PRs / stale (push) Has been cancelled

This commit is contained in:
2025-09-02 14:49:16 +08:00
commit 38ba663466
2885 changed files with 391107 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
/**
* The type of Redux action which closes a dialog
*
* {
* type: HIDE_DIALOG
* }
*/
export const HIDE_DIALOG = 'HIDE_DIALOG';
/**
* The type of Redux action which closes a sheet.
*
* {
* type: HIDE_SHEET
* }
*/
export const HIDE_SHEET = 'HIDE_SHEET';
/**
* The type of Redux action which begins a request to open a dialog.
*
* {
* type: OPEN_DIALOG,
* component: React.Component,
* props: PropTypes
* }
*
*/
export const OPEN_DIALOG = 'OPEN_DIALOG';
/**
* The type of Redux action which begins a request to open a sheet.
*
* {
* type: OPEN_SHEET,
* component: React.Component,
* props: PropTypes
* }
*
*/
export const OPEN_SHEET = 'OPEN_SHEET';

View File

@@ -0,0 +1,126 @@
import { ComponentType } from 'react';
import { IStore } from '../../app/types';
import {
HIDE_DIALOG,
HIDE_SHEET,
OPEN_DIALOG,
OPEN_SHEET
} from './actionTypes';
import { isDialogOpen } from './functions';
import logger from './logger';
/**
* Signals Dialog to close its dialog.
*
* @param {Object} [component] - The {@code Dialog} component to close/hide. If
* {@code undefined}, closes/hides {@code Dialog} regardless of which
* component it's rendering; otherwise, closes/hides {@code Dialog} only if
* it's rendering the specified {@code component}.
* @returns {{
* type: HIDE_DIALOG,
* component: (React.Component | undefined)
* }}
*/
export function hideDialog(component?: ComponentType<any>) {
logger.info(`Hide dialog: ${getComponentDisplayName(component)}`);
return {
type: HIDE_DIALOG,
component
};
}
/**
* Closes the active sheet.
*
* @returns {{
* type: HIDE_SHEET,
* }}
*/
export function hideSheet() {
return {
type: HIDE_SHEET
};
}
/**
* Signals Dialog to open dialog.
*
* @param {Object} component - The component to display as dialog.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
* @returns {{
* type: OPEN_DIALOG,
* component: React.Component,
* componentProps: (Object | undefined)
* }}
*/
export function openDialog(component: ComponentType<any>, componentProps?: Object) {
logger.info(`Open dialog: ${getComponentDisplayName(component)}`);
return {
type: OPEN_DIALOG,
component,
componentProps
};
}
/**
* Opens the requested sheet.
*
* @param {Object} component - The component to display as a sheet.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
* @returns {{
* type: OPEN_SHEET,
* component: React.Component,
* componentProps: (Object | undefined)
* }}
*/
export function openSheet(component: ComponentType<any>, componentProps?: Object) {
return {
type: OPEN_SHEET,
component,
componentProps
};
}
/**
* Signals Dialog to open a dialog with the specified component if the component
* is not already open. If it is open, then Dialog is signaled to close its
* dialog.
*
* @param {Object} component - The component to display as dialog.
* @param {Object} [componentProps] - The React {@code Component} props of the
* specified {@code component}.
* @returns {Function}
*/
export function toggleDialog(component: ComponentType<any>, componentProps?: Object) {
return (dispatch: IStore['dispatch'], getState: IStore['getState']) => {
if (isDialogOpen(getState, component)) {
dispatch(hideDialog(component));
} else {
dispatch(openDialog(component, componentProps));
}
};
}
/**
* Extracts a printable name for a dialog component.
*
* @param {Object} component - The component to extract the name for.
*
* @returns {string} The display name.
*/
function getComponentDisplayName(component?: ComponentType<any>) {
if (!component) {
return '';
}
const name = component.displayName ?? component.name ?? 'Component';
return name.replace('withI18nextTranslation(Connect(', '') // dialogs with translations
.replace('))', ''); // dialogs with translations suffix
}

View File

@@ -0,0 +1,72 @@
import React, { Component, ComponentType } from 'react';
import { IReduxState } from '../../../app/types';
import { IReactionEmojiProps } from '../../../reactions/constants';
/**
* The type of the React {@code Component} props of {@link DialogContainer}.
*/
interface IProps {
/**
* The component to render.
*/
_component?: ComponentType<any>;
/**
* The props to pass to the component that will be rendered.
*/
_componentProps?: Object;
/**
* Array of reactions to be displayed.
*/
_reactionsQueue: Array<IReactionEmojiProps>;
/**
* True if the UI is in a compact state where we don't show dialogs.
*/
_reducedUI: boolean;
}
/**
* Implements a DialogContainer responsible for showing all dialogs.
*/
export default class AbstractDialogContainer extends Component<IProps> {
/**
* Returns the dialog to be displayed.
*
* @private
* @returns {ReactElement|null}
*/
_renderDialogContent() {
const {
_component: component,
_reducedUI: reducedUI
} = this.props;
return (
component && !reducedUI
? React.createElement(component, this.props._componentProps)
: null);
}
}
/**
* Maps (parts of) the redux state to the associated
* {@code AbstractDialogContainer}'s props.
*
* @param {Object} state - The redux state.
* @private
* @returns {IProps}
*/
export function abstractMapStateToProps(state: IReduxState) {
const stateFeaturesBaseDialog = state['features/base/dialog'];
const { reducedUI } = state['features/base/responsive-ui'];
return {
_component: stateFeaturesBaseDialog.component,
_componentProps: stateFeaturesBaseDialog.componentProps,
_reducedUI: reducedUI
};
}

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { Text, TextStyle } from 'react-native';
import { brandedDialog as styles } from './native/styles';
/**
* Renders a specific {@code string} which may contain HTML.
*
* @param {string|undefined} html - The {@code string} which may
* contain HTML to render.
* @returns {ReactElement[]|string}
*/
export function renderHTML(html?: string) {
if (typeof html === 'string') {
// At the time of this writing, the specified HTML contains a couple
// of spaces one after the other. They do not cause a visible
// problem on Web, because the specified HTML is rendered as, well,
// HTML. However, we're not rendering HTML here.
// eslint-disable-next-line no-param-reassign
html = html.replace(/\s{2,}/gi, ' ');
// Render text in <b>text</b> in bold.
const opening = /<\s*b\s*>/gi;
const closing = /<\s*\/\s*b\s*>/gi;
let o;
let c;
let prevClosingLastIndex = 0;
const r = [];
// eslint-disable-next-line no-cond-assign
while (o = opening.exec(html)) {
closing.lastIndex = opening.lastIndex;
// eslint-disable-next-line no-cond-assign
if (c = closing.exec(html)) {
r.push(html.substring(prevClosingLastIndex, o.index));
r.push(
<Text style = { (styles.boldDialogText as TextStyle) }>
{ html.substring(opening.lastIndex, c.index) }
</Text>);
opening.lastIndex
= prevClosingLastIndex
= closing.lastIndex;
} else {
break;
}
}
if (prevClosingLastIndex < html.length) {
r.push(html.substring(prevClosingLastIndex));
}
return r;
}
return html;
}

View File

@@ -0,0 +1,168 @@
import { Component } from 'react';
import { IStore } from '../../../../app/types';
import { hideDialog } from '../../actions';
import { DialogProps } from '../../constants';
/**
* The type of the React {@code Component} props of {@link AbstractDialog}.
*/
export interface IProps extends DialogProps {
/**
* Used to show/hide the dialog on cancel.
*/
dispatch: IStore['dispatch'];
}
/**
* The type of the React {@code Component} state of {@link AbstractDialog}.
*/
export interface IState {
submitting?: boolean;
}
/**
* An abstract implementation of a dialog on Web/React and mobile/react-native.
*/
export default class AbstractDialog<P extends IProps, S extends IState = IState>
extends Component<P, S> {
_mounted: boolean;
/**
* Initializes a new {@code AbstractDialog} instance.
*
* @param {Object} props - The read-only React {@code Component} props with
* which the new instance is to be initialized.
*/
constructor(props: P) {
super(props);
// Bind event handlers so they are only bound once per instance.
this._onBack = this._onBack.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onSubmit = this._onSubmit.bind(this);
this._onSubmitFulfilled = this._onSubmitFulfilled.bind(this);
this._onSubmitRejected = this._onSubmitRejected.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}. Invoked
* immediately before mounting occurs.
*
* @inheritdoc
*/
override componentDidMount() {
this._mounted = true;
}
/**
* Implements React's {@link Component#componentWillUnmount()}. Invoked
* immediately before this component is unmounted and destroyed.
*
* @inheritdoc
*/
override componentWillUnmount() {
this._mounted = false;
}
/**
* Dispatches a redux action to hide this dialog.
*
* @returns {*} The return value of {@link hideDialog}.
*/
_hide() {
return this.props.dispatch(hideDialog());
}
_onBack() {
const { backDisabled = false, onBack } = this.props;
if (!backDisabled && (!onBack || onBack())) {
this._hide();
}
}
/**
* Dispatches a redux action to hide this dialog when it's canceled.
*
* @protected
* @returns {void}
*/
_onCancel() {
const { cancelDisabled = false, onCancel } = this.props;
if (!cancelDisabled && (!onCancel || onCancel())) {
this._hide();
}
}
/**
* Submits this {@code Dialog}. If the React {@code Component} prop
* {@code onSubmit} is defined, the function that is the value of the prop
* is invoked. If the function returns a {@code thenable}, then the
* resolution of the {@code thenable} is awaited. If the submission
* completes successfully, a redux action will be dispatched to hide this
* dialog.
*
* @protected
* @param {string} [value] - The submitted value if any.
* @returns {void}
*/
_onSubmit(value?: string) {
const { okDisabled = false, onSubmit } = this.props;
if (!okDisabled) {
this.setState({ submitting: true });
// Invoke the React Component prop onSubmit if any.
const r = !onSubmit || onSubmit(value);
// If the invocation returns a thenable, await its resolution;
// otherwise, treat the return value as a boolean indicating whether
// the submission has completed successfully.
let then;
if (r) {
switch (typeof r) {
case 'function':
case 'object':
then = r.then;
break;
}
}
if (typeof then === 'function' && then.length === 2) {
then.call(r, this._onSubmitFulfilled, this._onSubmitRejected);
} else if (r) {
this._onSubmitFulfilled();
} else {
this._onSubmitRejected();
}
}
}
/**
* Notifies this {@code AbstractDialog} that it has been submitted
* successfully. Dispatches a redux action to hide this dialog after it has
* been submitted.
*
* @private
* @returns {void}
*/
_onSubmitFulfilled() {
this._mounted && this.setState({ submitting: false });
this._hide();
}
/**
* Notifies this {@code AbstractDialog} that its submission has failed.
*
* @private
* @returns {void}
*/
_onSubmitRejected() {
this._mounted && this.setState({ submitting: false });
}
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import { WithTranslation } from 'react-i18next';
import Dialog from 'react-native-dialog';
import { connect } from 'react-redux';
import { translate } from '../../../i18n/functions';
import { _abstractMapStateToProps } from '../../functions';
import { renderHTML } from '../functions.native';
import AbstractDialog, { IProps as AbstractProps } from './AbstractDialog';
interface IProps extends AbstractProps, WithTranslation {
/**
* Untranslated i18n key of the content to be displayed.
*
* NOTE: This dialog also adds support to Object type keys that will be
* translated using the provided params. See i18n function
* {@code translate(string, Object)} for more details.
*/
contentKey: string | { key: string; params: Object; };
}
/**
* Implements an alert dialog, to simply show an error or a message,
* then disappear on dismiss.
*/
class AlertDialog extends AbstractDialog<IProps> {
/**
* Implements React's {@link Component#render}.
*
* @inheritdoc
*/
override render() {
const { contentKey, t } = this.props;
const content
= typeof contentKey === 'string'
? t(contentKey)
: renderHTML(t(contentKey.key, contentKey.params));
return (
<Dialog.Container
coverScreen = { false }
visible = { true }>
<Dialog.Description>
{ content }
</Dialog.Description>
<Dialog.Button
label = { t('dialog.Ok') }
onPress = { this._onSubmit } />
</Dialog.Container>
);
}
}
export default translate(connect(_abstractMapStateToProps)(AlertDialog));

View File

@@ -0,0 +1,150 @@
import React, { PureComponent, ReactNode } from 'react';
import { SafeAreaView, ScrollView, View, ViewStyle } from 'react-native';
import { connect } from 'react-redux';
import { IStore } from '../../../../app/types';
import SlidingView from '../../../react/components/native/SlidingView';
import { hideSheet } from '../../actions';
import { bottomSheetStyles as styles } from './styles';
/**
* The type of {@code BottomSheet}'s React {@code Component} prop types.
*/
type Props = {
/**
* Whether to add padding to scroll view.
*/
addScrollViewPadding?: boolean;
/**
* The children to be displayed within this component.
*/
children: ReactNode;
/**
* Redux Dispatch function.
*/
dispatch: IStore['dispatch'];
/**
* Handler for the cancel event, which happens when the user dismisses
* the sheet.
*/
onCancel?: Function;
/**
* Function to render a bottom sheet footer element, if necessary.
*/
renderFooter?: () => React.ReactNode;
/**
* Function to render a bottom sheet header element, if necessary.
*/
renderHeader?: Function;
/**
* Whether to show sliding view or not.
*/
showSlidingView?: boolean;
/**
* The component's external style.
*/
style?: Object;
};
/**
* A component emulating Android's BottomSheet.
*/
class BottomSheet extends PureComponent<Props> {
/**
* Default values for {@code BottomSheet} component's properties.
*
* @static
*/
static defaultProps = {
addScrollViewPadding: true,
showSlidingView: true
};
/**
* Initializes a new instance.
*
* @param {Props} props - The React {@code Component} props to initialize
* the new instance with.
*/
constructor(props: Props) {
super(props);
this._onCancel = this._onCancel.bind(this);
}
/**
* Handles the cancel event, when the user dismissed the sheet. By default we close it.
*
* @returns {void}
*/
_onCancel() {
if (this.props.onCancel) {
this.props.onCancel();
} else {
this.props.dispatch(hideSheet());
}
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const {
addScrollViewPadding,
renderHeader,
renderFooter,
showSlidingView,
style
} = this.props;
return (
<SlidingView
onHide = { this._onCancel }
position = 'bottom'
show = { Boolean(showSlidingView) }>
<View
pointerEvents = 'box-none'
style = { styles.sheetContainer as ViewStyle }>
<View
pointerEvents = 'box-none'
style = { styles.sheetAreaCover } />
{ renderHeader?.() }
<SafeAreaView
style = { [
styles.sheetItemContainer,
renderHeader
? styles.sheetHeader
: styles.sheet,
renderFooter && styles.sheetFooter,
style
] }>
<ScrollView
bounces = { false }
showsVerticalScrollIndicator = { false }
style = { [
renderFooter && styles.sheet,
addScrollViewPadding && styles.scrollView
] } >
{ this.props.children }
</ScrollView>
{ renderFooter?.() }
</SafeAreaView>
</View>
</SlidingView>
);
}
}
export default connect()(BottomSheet);

View File

@@ -0,0 +1,22 @@
import React, { Fragment } from 'react';
import { useSelector } from 'react-redux';
import { IReduxState } from '../../../../app/types';
const BottomSheetContainer: () => JSX.Element | null = (): JSX.Element | null => {
const { sheet, sheetProps } = useSelector((state: IReduxState) => state['features/base/dialog']);
const { reducedUI } = useSelector((state: IReduxState) => state['features/base/responsive-ui']);
if (!sheet || reducedUI) {
return null;
}
return (
<Fragment>
{ React.createElement(sheet, sheetProps) }
</Fragment>
);
};
export default BottomSheetContainer;

View File

@@ -0,0 +1,172 @@
import React from 'react';
import { WithTranslation } from 'react-i18next';
import Dialog from 'react-native-dialog';
import { connect } from 'react-redux';
import { translate } from '../../../i18n/functions';
import { renderHTML } from '../functions.native';
import AbstractDialog, { IProps as AbstractProps } from './AbstractDialog';
import styles from './styles';
/**
* The type of the React {@code Component} props of
* {@link ConfirmDialog}.
*/
interface IProps extends AbstractProps, WithTranslation {
/**
* The i18n key of the text label for the back button.
*/
backLabel?: string;
/**
* The i18n key of the text label for the cancel button.
*/
cancelLabel?: string;
/**
* The React {@code Component} children.
*/
children?: React.ReactNode;
/**
* The i18n key of the text label for the confirm button.
*/
confirmLabel?: string;
/**
* Dialog description key for translations.
*/
descriptionKey?: string | { key: string; params: string; };
/**
* Whether the back button is hidden.
*/
isBackHidden?: Boolean;
/**
* Whether the cancel button is hidden.
*/
isCancelHidden?: Boolean;
/**
* Whether or not the nature of the confirm button is destructive.
*/
isConfirmDestructive?: Boolean;
/**
* Whether or not the confirm button is hidden.
*/
isConfirmHidden?: Boolean;
/**
* Dialog title.
*/
title?: string;
/**
* Renders buttons vertically.
*/
verticalButtons?: boolean;
}
/**
* React Component for getting confirmation to stop a file recording session in
* progress.
*
* @augments Component
*/
class ConfirmDialog extends AbstractDialog<IProps> {
/**
* Default values for {@code ConfirmDialog} component's properties.
*
* @static
*/
static defaultProps = {
isConfirmDestructive: false,
isConfirmHidden: false
};
/**
* Renders the dialog description.
*
* @returns {React$Component}
*/
_renderDescription() {
const { descriptionKey, t } = this.props;
const description
= typeof descriptionKey === 'string'
? t(descriptionKey)
: renderHTML(
t(descriptionKey?.key ?? '', descriptionKey?.params)
);
return (
<Dialog.Description>
{ description }
</Dialog.Description>
);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
const {
backLabel,
cancelLabel,
children,
confirmLabel,
isBackHidden = true,
isCancelHidden,
isConfirmDestructive,
isConfirmHidden,
t,
title,
verticalButtons
} = this.props;
const dialogButtonStyle
= isConfirmDestructive
? styles.destructiveDialogButton : styles.dialogButton;
return (
<Dialog.Container
coverScreen = { false }
verticalButtons = { verticalButtons }
visible = { true }>
{
title && <Dialog.Title>
{ t(title) }
</Dialog.Title>
}
{ this._renderDescription() }
{ children }
{
!isBackHidden && <Dialog.Button
label = { t(backLabel || 'dialog.confirmBack') }
onPress = { this._onBack }
style = { styles.dialogButton } />
}
{
!isCancelHidden && <Dialog.Button
label = { t(cancelLabel || 'dialog.confirmNo') }
onPress = { this._onCancel }
style = { styles.dialogButton } />
}
{
!isConfirmHidden && <Dialog.Button
label = { t(confirmLabel || 'dialog.confirmYes') }
onPress = { this._onSubmit }
style = { dialogButtonStyle } />
}
</Dialog.Container>
);
}
}
export default translate(connect()(ConfirmDialog));

View File

@@ -0,0 +1,58 @@
import React, { Fragment } from 'react';
import { connect } from 'react-redux';
import { IReduxState } from '../../../../app/types';
import ReactionEmoji from '../../../../reactions/components/native/ReactionEmoji';
import { getReactionsQueue } from '../../../../reactions/functions.native';
import AbstractDialogContainer, {
abstractMapStateToProps
} from '../AbstractDialogContainer';
/**
* Implements a DialogContainer responsible for showing all dialogs. We will
* need a separate container so we can handle multiple dialogs by showing them
* simultaneously or queueing them.
*
* @augments AbstractDialogContainer
*/
class DialogContainer extends AbstractDialogContainer {
/**
* Returns the reactions to be displayed.
*
* @returns {Array<React$Element>}
*/
_renderReactions() {
const { _reactionsQueue } = this.props;
return _reactionsQueue.map(({ reaction, uid }, index) => (<ReactionEmoji
index = { index }
key = { uid }
reaction = { reaction }
uid = { uid } />));
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
return (
<Fragment>
{this._renderReactions()}
{this._renderDialogContent()}
</Fragment>
);
}
}
const mapStateToProps = (state: IReduxState) => {
return {
...abstractMapStateToProps(state),
_reactionsQueue: getReactionsQueue(state)
};
};
export default connect(mapStateToProps)(DialogContainer);

View File

@@ -0,0 +1,170 @@
import React from 'react';
import { WithTranslation } from 'react-i18next';
import { TextStyle } from 'react-native';
import Dialog from 'react-native-dialog';
import { connect } from 'react-redux';
import { translate } from '../../../i18n/functions';
import { _abstractMapStateToProps } from '../../functions';
import AbstractDialog, {
IProps as AbstractProps,
IState as AbstractState
} from './AbstractDialog';
import { inputDialog as styles } from './styles';
interface IProps extends AbstractProps, WithTranslation {
/**
* The dialog descriptionKey.
*/
descriptionKey?: string;
/**
* Whether to display the cancel button.
*/
disableCancel?: boolean;
/**
* An optional initial value to initiate the field with.
*/
initialValue?: string;
/**
* A message key to be shown for the user (e.g. An error that is defined after submitting the form).
*/
messageKey?: string;
/**
* Props for the text input.
*/
textInputProps?: Object;
/**
* The untranslated i18n key for the dialog title.
*/
titleKey?: string;
/**
* Validating of the input.
*/
validateInput?: Function;
}
interface IState extends AbstractState {
/**
* The current value of the field.
*/
fieldValue?: string;
/**
* The result of the input validation.
*/
isValid: boolean;
}
/**
* Implements a single field input dialog component.
*/
class InputDialog extends AbstractDialog<IProps, IState> {
/**
* Instantiates a new {@code InputDialog}.
*
* @inheritdoc
*/
constructor(props: IProps) {
super(props);
this.state = {
fieldValue: props.initialValue,
isValid: props.validateInput ? props.validateInput(props.initialValue) : true,
submitting: false
};
this._onChangeText = this._onChangeText.bind(this);
this._onSubmitValue = this._onSubmitValue.bind(this);
}
/**
* Implements {@code Component#render}.
*
* @inheritdoc
*/
override render() {
const {
descriptionKey,
messageKey,
t,
titleKey
} = this.props;
return (
<Dialog.Container
coverScreen = { false }
visible = { true }>
<Dialog.Title>
{ t(titleKey ?? '') }
</Dialog.Title>
{
descriptionKey && (
<Dialog.Description>
{ t(descriptionKey) }
</Dialog.Description>
)
}
<Dialog.Input
autoFocus = { true }
onChangeText = { this._onChangeText }
value = { this.state.fieldValue }
{ ...this.props.textInputProps } />
{
messageKey && (
<Dialog.Description
style = { styles.formMessage as TextStyle }>
{ t(messageKey) }
</Dialog.Description>
)
}
{!this.props.disableCancel && <Dialog.Button
label = { t('dialog.Cancel') }
onPress = { this._onCancel } />}
<Dialog.Button
disabled = { !this.state.isValid }
label = { t('dialog.Ok') }
onPress = { this._onSubmitValue } />
</Dialog.Container>
);
}
/**
* Callback to be invoked when the text in the field changes.
*
* @param {string} fieldValue - The updated field value.
* @returns {void}
*/
_onChangeText(fieldValue: string) {
if (this.props.validateInput) {
this.setState({
isValid: this.props.validateInput(fieldValue),
fieldValue
});
return;
}
this.setState({
fieldValue
});
}
/**
* Callback to be invoked when the value of this dialog is submitted.
*
* @returns {boolean}
*/
_onSubmitValue() {
return this._onSubmit(this.state.fieldValue);
}
}
export default translate(connect(_abstractMapStateToProps)(InputDialog));

View File

@@ -0,0 +1,221 @@
// @ts-expect-error
import { randomInt } from '@jitsi/js-utils/random';
import React, { Component } from 'react';
import { WithTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { appNavigate, reloadNow } from '../../../../app/actions.native';
import { IReduxState, IStore } from '../../../../app/types';
import { translate } from '../../../i18n/functions';
import { isFatalJitsiConnectionError } from '../../../lib-jitsi-meet/functions.native';
import { hideDialog } from '../../actions';
import logger from '../../logger';
import ConfirmDialog from './ConfirmDialog';
/**
* The type of the React {@code Component} props of
* {@link PageReloadDialog}.
*/
interface IProps extends WithTranslation {
conferenceError?: Error;
configError?: Error;
connectionError?: Error;
dispatch: IStore['dispatch'];
isNetworkFailure: boolean;
reason?: string;
}
/**
* The type of the React {@code Component} state of
* {@link PageReloadDialog}.
*/
interface IPageReloadDialogState {
timeLeft: number;
}
/**
* 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<IProps, IPageReloadDialogState> {
_interval?: number;
_timeoutSeconds: number;
/**
* Initializes a new PageReloadOverlay instance.
*
* @param {Object} props - The read-only properties with which the new
* instance is to be initialized.
* @public
*/
constructor(props: IProps) {
super(props);
this._timeoutSeconds = 10 + randomInt(0, 20);
this.state = {
timeLeft: this._timeoutSeconds
};
this._onCancel = this._onCancel.bind(this);
this._onReloadNow = this._onReloadNow.bind(this);
this._onReconnecting = this._onReconnecting.bind(this);
}
/**
* React Component method that executes once component is mounted.
*
* @inheritdoc
* @returns {void}
*/
override componentDidMount() {
const { timeLeft } = this.state;
logger.info(
`The conference will be reloaded after ${timeLeft} seconds.`
);
this._interval = setInterval(() =>
this._onReconnecting(), 1000);
}
/**
* Clears the timer interval.
*
* @inheritdoc
* @returns {void}
*/
override 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() {
const { dispatch } = this.props;
clearInterval(this._interval ?? 0);
dispatch(appNavigate(undefined));
return true;
}
/**
* Handles automatic reconnection.
*
* @private
* @returns {void}
*/
_onReconnecting() {
const { dispatch } = this.props;
const { timeLeft } = this.state;
if (timeLeft === 0) {
if (this._interval) {
dispatch(hideDialog());
this._onReloadNow();
this._interval = undefined;
}
}
this.setState({
timeLeft: timeLeft - 1
});
}
/**
* 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() {
const { dispatch } = this.props;
clearInterval(this._interval ?? 0);
dispatch(reloadNow());
return true;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
override render() {
const { isNetworkFailure, t } = this.props;
const { timeLeft } = this.state;
let message, title;
if (isNetworkFailure) {
title = 'dialog.conferenceDisconnectTitle';
message = 'dialog.conferenceDisconnectMsg';
} else {
title = 'dialog.conferenceReloadTitle';
message = 'dialog.conferenceReloadMsg';
}
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.
* @param {IProps} ownProps - The own props of the component.
* @protected
* @returns {{
* isNetworkFailure: boolean,
* reason: string
* }}
*/
function mapStateToProps(state: IReduxState, ownProps: IProps) {
const { conferenceError, configError, connectionError } = ownProps;
const fatalConnectionError
= connectionError && isFatalJitsiConnectionError(connectionError);
const isNetworkFailure = Boolean(configError || 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));

View File

@@ -0,0 +1,265 @@
import { StyleSheet } from 'react-native';
import ColorSchemeRegistry from '../../../color-scheme/ColorSchemeRegistry';
import { schemeColor } from '../../../color-scheme/functions';
import { BoxModel } from '../../../styles/components/styles/BoxModel';
import BaseTheme from '../../../ui/components/BaseTheme.native';
import { PREFERRED_DIALOG_SIZE } from '../../constants';
const BORDER_RADIUS = 5;
/**
* NOTE: These Material guidelines based values are currently only used in
* dialogs (and related) but later on it would be nice to export it into a base
* Material feature.
*/
export const MD_FONT_SIZE = 16;
export const MD_ITEM_HEIGHT = 48;
export const MD_ITEM_MARGIN_PADDING = BaseTheme.spacing[3];
/**
* Reusable (colored) style for text in any branded dialogs.
*/
const brandedDialogText = {
color: schemeColor('text'),
fontSize: MD_FONT_SIZE,
textAlign: 'center'
};
const brandedDialogLabelStyle = {
color: BaseTheme.palette.text01,
flexShrink: 1,
fontSize: MD_FONT_SIZE,
opacity: 0.90
};
const brandedDialogItemContainerStyle = {
alignItems: 'center',
flexDirection: 'row',
height: MD_ITEM_HEIGHT
};
const brandedDialogIconStyle = {
color: BaseTheme.palette.icon01,
fontSize: 24
};
export const inputDialog = {
formMessage: {
alignSelf: 'flex-start',
fontStyle: 'italic',
fontWeight: 'bold',
marginTop: BaseTheme.spacing[3]
}
};
/**
* The React {@code Component} styles of {@code BottomSheet}. These have
* been implemented as per the Material Design guidelines:
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
*/
export const bottomSheetStyles = {
sheetAreaCover: {
backgroundColor: 'transparent',
flex: 1
},
scrollView: {
paddingHorizontal: 0
},
/**
* Style for the container of the sheet.
*/
sheetContainer: {
alignItems: 'stretch',
flex: 1,
flexDirection: 'column',
justifyContent: 'flex-end',
maxWidth: 500,
marginLeft: 'auto',
marginRight: 'auto',
width: '100%'
},
sheetItemContainer: {
flex: -1,
maxHeight: '75%'
},
buttons: {
/**
* Style for the {@code Icon} element in a generic item of the menu.
*/
iconStyle: {
...brandedDialogIconStyle
},
/**
* Style for the label in a generic item rendered in the menu.
*/
labelStyle: {
...brandedDialogLabelStyle,
marginLeft: 16
},
/**
* Container style for a generic item rendered in the menu.
*/
style: {
...brandedDialogItemContainerStyle,
paddingHorizontal: MD_ITEM_MARGIN_PADDING
},
/**
* Additional style that is not directly used as a style object.
*/
underlayColor: BaseTheme.palette.ui04
},
/**
* Bottom sheet's base style.
*/
sheet: {
backgroundColor: BaseTheme.palette.ui02,
borderTopLeftRadius: 16,
borderTopRightRadius: 16
},
/**
* Bottom sheet's base style with header.
*/
sheetHeader: {
backgroundColor: BaseTheme.palette.ui02
},
/**
* Bottom sheet's background color with footer.
*/
sheetFooter: {
backgroundColor: BaseTheme.palette.ui01
}
};
export default {
dialogButton: {
...BaseTheme.typography.bodyLongBold
},
destructiveDialogButton: {
...BaseTheme.typography.bodyLongBold,
color: BaseTheme.palette.actionDanger
}
};
export const brandedDialog = {
/**
* The style of bold {@code Text} rendered by the {@code Dialog}s of the
* feature authentication.
*/
boldDialogText: {
fontWeight: 'bold'
},
buttonFarRight: {
borderBottomRightRadius: BORDER_RADIUS
},
buttonWrapper: {
alignItems: 'stretch',
borderRadius: BORDER_RADIUS,
flexDirection: 'row'
},
mainWrapper: {
alignSelf: 'stretch',
padding: BoxModel.padding * 2,
// The added bottom padding is to compensate the empty space around the
// close icon.
paddingBottom: BoxModel.padding * 3
},
overlayTouchable: {
...StyleSheet.absoluteFillObject
}
};
/**
* Color schemed styles for all the component based on the abstract dialog.
*/
ColorSchemeRegistry.register('Dialog', {
button: {
backgroundColor: '#44A5FF',
flex: 1,
padding: BoxModel.padding * 1.5
},
/**
* Separator line for the buttons in a dialog.
*/
buttonSeparator: {
borderRightColor: schemeColor('border'),
borderRightWidth: 1
},
buttonLabel: {
color: schemeColor('buttonLabel'),
fontSize: MD_FONT_SIZE,
textAlign: 'center'
},
/**
* Style of the close icon on a dialog.
*/
closeStyle: {
color: schemeColor('icon'),
fontSize: MD_FONT_SIZE
},
/**
* Base style of the dialogs.
*/
dialog: {
alignItems: 'stretch',
backgroundColor: schemeColor('background'),
borderColor: schemeColor('border'),
borderRadius: BORDER_RADIUS,
borderWidth: 1,
flex: 1,
flexDirection: 'column',
maxWidth: PREFERRED_DIALOG_SIZE
},
/**
* Field on an input dialog.
*/
field: {
...brandedDialogText,
borderBottomWidth: 1,
borderColor: schemeColor('border'),
margin: BoxModel.margin,
textAlign: 'left'
},
/**
* Style for the field label on an input dialog.
*/
fieldLabel: {
...brandedDialogText,
margin: BoxModel.margin,
textAlign: 'left'
},
text: {
...brandedDialogText,
color: BaseTheme.palette.text01
},
topBorderContainer: {
borderTopColor: BaseTheme.palette.ui07,
borderTopWidth: 1
}
});

View File

@@ -0,0 +1,58 @@
import { Component } from 'react';
/**
* The type of the React {@code Component} props of {@link AbstractDialogTab}.
*/
export interface IProps {
/**
* Callback to invoke on change.
*/
onTabStateChange: Function;
/**
* The id of the tab.
*/
tabId: number;
}
/**
* Abstract React {@code Component} for tabs of the DialogWithTabs component.
*
* @augments Component
*/
class AbstractDialogTab<P extends IProps, S> extends Component<P, S> {
/**
* Initializes a new {@code AbstractDialogTab} instance.
*
* @param {P} props - The read-only properties with which the new
* instance is to be initialized.
*/
constructor(props: P) {
super(props);
// Bind event handler so it is only bound once for every instance.
this._onChange = this._onChange.bind(this);
}
/**
* Uses the onTabStateChange function to pass the changed state of the
* controlled tab component to the controlling DialogWithTabs component.
*
* @param {Object} change - Object that contains the changed property and
* value.
* @returns {void}
*/
_onChange(change: Object) {
const { onTabStateChange, tabId } = this.props;
onTabStateChange(tabId, {
...this.props,
...change
});
}
}
export default AbstractDialogTab;

View File

@@ -0,0 +1,83 @@
import { ReactNode } from 'react';
export type DialogProps = {
/**
* Whether back button is disabled. Enabled by default.
*/
backDisabled?: boolean;
/**
* Optional i18n key to change the back button title.
*/
backKey?: string;
/**
* Whether cancel button is disabled. Enabled by default.
*/
cancelDisabled?: boolean;
/**
* Optional i18n key to change the cancel button title.
*/
cancelKey?: string;
/**
* The React {@code Component} children which represents the dialog's body.
*/
children?: ReactNode;
/**
* Is ok button enabled/disabled. Enabled by default.
*/
okDisabled?: boolean;
/**
* Optional i18n key to change the ok button title.
*/
okKey?: string;
/**
* The handler for onBack event.
*/
onBack?: Function;
/**
* The handler for onCancel event.
*/
onCancel?: Function;
/**
* The handler for the event when submitting the dialog.
*/
onSubmit?: Function;
/**
* Additional style to be applied on the dialog.
*
* NOTE: Not all dialog types support this!
*/
style?: Object;
/**
* Key to use for showing a title.
*/
titleKey?: string;
/**
* The string to use as a title instead of {@code titleKey}. If a truthy
* value is specified, it takes precedence over {@code titleKey} i.e.
* The latter is unused.
*/
titleString?: string;
};
/**
* A preferred (or optimal) dialog size. This constant is reused in many
* components, where dialog size optimization is suggested.
*
* NOTE: Even though we support valious devices, including tablets, we don't
* want the dialogs to be oversized even on larger devices. This number seems
* to be a good compromise, but also easy to update.
*/
export const PREFERRED_DIALOG_SIZE = 300;

View File

@@ -0,0 +1,45 @@
import { ComponentType } from 'react';
import { IReduxState } from '../../app/types';
import { IStateful } from '../app/types';
import ColorSchemeRegistry from '../color-scheme/ColorSchemeRegistry';
import { toState } from '../redux/functions';
/**
* Checks if any {@code Dialog} is currently open.
*
* @param {IStateful} stateful - The redux store, the redux
* {@code getState} function, or the redux state itself.
* @returns {boolean}
*/
export function isAnyDialogOpen(stateful: IStateful) {
return Boolean(toState(stateful)['features/base/dialog'].component);
}
/**
* Checks if a {@code Dialog} with a specific {@code component} is currently
* open.
*
* @param {IStateful} stateful - The redux store, the redux
* {@code getState} function, or the redux state itself.
* @param {React.Component} component - The {@code component} of a
* {@code Dialog} to be checked.
* @returns {boolean}
*/
export function isDialogOpen(stateful: IStateful, component: ComponentType<any>) {
return toState(stateful)['features/base/dialog'].component === component;
}
/**
* Maps part of the Redux state to the props of any Dialog based component.
*
* @param {IReduxState} state - The Redux state.
* @returns {{
* _dialogStyles: StyleType
* }}
*/
export function _abstractMapStateToProps(state: IReduxState) {
return {
_dialogStyles: ColorSchemeRegistry.get(state, 'Dialog')
};
}

View File

@@ -0,0 +1,3 @@
import { getLogger } from '../logging/functions';
export default getLogger('features/base/dialog');

View File

@@ -0,0 +1,63 @@
import { ComponentType } from 'react';
import ReducerRegistry from '../redux/ReducerRegistry';
import { assign } from '../redux/functions';
import {
HIDE_DIALOG,
HIDE_SHEET,
OPEN_DIALOG,
OPEN_SHEET
} from './actionTypes';
export interface IDialogState {
component?: ComponentType;
componentProps?: Object;
sheet?: ComponentType;
sheetProps?: Object;
}
/**
* Reduces redux actions which show or hide dialogs.
*
* @param {IDialogState} state - The current redux state.
* @param {Action} action - The redux action to reduce.
* @param {string} action.type - The type of the redux action to reduce..
* @returns {State} The next redux state that is the result of reducing the
* specified action.
*/
ReducerRegistry.register<IDialogState>('features/base/dialog', (state = {}, action): IDialogState => {
switch (action.type) {
case HIDE_DIALOG: {
const { component } = action;
if (typeof component === 'undefined' || state.component === component) {
return assign(state, {
component: undefined,
componentProps: undefined
});
}
break;
}
case OPEN_DIALOG:
return assign(state, {
component: action.component,
componentProps: action.componentProps
});
case HIDE_SHEET:
return assign(state, {
sheet: undefined,
sheetProps: undefined
});
case OPEN_SHEET:
return assign(state, {
sheet: action.component,
sheetProps: action.componentProps
});
}
return state;
});