This commit is contained in:
41
react/features/base/dialog/actionTypes.ts
Normal file
41
react/features/base/dialog/actionTypes.ts
Normal 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';
|
||||
126
react/features/base/dialog/actions.ts
Normal file
126
react/features/base/dialog/actions.ts
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
57
react/features/base/dialog/components/functions.native.tsx
Normal file
57
react/features/base/dialog/components/functions.native.tsx
Normal 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;
|
||||
}
|
||||
168
react/features/base/dialog/components/native/AbstractDialog.ts
Normal file
168
react/features/base/dialog/components/native/AbstractDialog.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
56
react/features/base/dialog/components/native/AlertDialog.tsx
Normal file
56
react/features/base/dialog/components/native/AlertDialog.tsx
Normal 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));
|
||||
150
react/features/base/dialog/components/native/BottomSheet.tsx
Normal file
150
react/features/base/dialog/components/native/BottomSheet.tsx
Normal 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);
|
||||
@@ -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;
|
||||
172
react/features/base/dialog/components/native/ConfirmDialog.tsx
Normal file
172
react/features/base/dialog/components/native/ConfirmDialog.tsx
Normal 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));
|
||||
@@ -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);
|
||||
170
react/features/base/dialog/components/native/InputDialog.tsx
Normal file
170
react/features/base/dialog/components/native/InputDialog.tsx
Normal 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));
|
||||
@@ -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));
|
||||
265
react/features/base/dialog/components/native/styles.ts
Normal file
265
react/features/base/dialog/components/native/styles.ts
Normal 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
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
83
react/features/base/dialog/constants.ts
Normal file
83
react/features/base/dialog/constants.ts
Normal 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;
|
||||
45
react/features/base/dialog/functions.ts
Normal file
45
react/features/base/dialog/functions.ts
Normal 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')
|
||||
};
|
||||
}
|
||||
3
react/features/base/dialog/logger.ts
Normal file
3
react/features/base/dialog/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { getLogger } from '../logging/functions';
|
||||
|
||||
export default getLogger('features/base/dialog');
|
||||
63
react/features/base/dialog/reducer.ts
Normal file
63
react/features/base/dialog/reducer.ts
Normal 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;
|
||||
});
|
||||
Reference in New Issue
Block a user