[RN] Implement a new UI for the Toolbox

- 5 buttons in the (now single) toolbar
- Overflow menu in the form of a BottomSheet
- Filmstrip on the right when in wide mode
This commit is contained in:
Saúl Ibarra Corretgé 2018-05-15 13:18:42 +02:00 committed by Lyubo Marinov
parent c344a83376
commit f54f5df428
9 changed files with 412 additions and 312 deletions

View File

@ -9,6 +9,11 @@ export type Styles = {
*/ */
iconStyle: Object, iconStyle: Object,
/**
* Style for te item's label.
*/
labelStyle: Object,
/** /**
* Style for the item itself. * Style for the item itself.
*/ */

View File

@ -1,7 +1,7 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { TouchableHighlight } from 'react-native'; import { Text, TouchableHighlight, View } from 'react-native';
import { Icon } from '../../../base/font-icons'; import { Icon } from '../../../base/font-icons';
@ -26,9 +26,23 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
} }
/** /**
* Handles rendering of the actual item. * Helper function to render the {@code Icon} part of this item.
* *
* TODO: currently no handling for labels is implemented. * @private
* @returns {ReactElement}
*/
_renderIcon() {
const { styles } = this.props;
return (
<Icon
name = { this._getIconName() }
style = { styles && styles.iconStyle } />
);
}
/**
* Handles rendering of the actual item.
* *
* @protected * @protected
* @returns {ReactElement} * @returns {ReactElement}
@ -38,19 +52,38 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
accessibilityLabel, accessibilityLabel,
disabled, disabled,
onClick, onClick,
showLabel,
styles styles
} = this.props; } = this.props;
let children;
if (showLabel) {
// eslint-disable-next-line no-extra-parens
children = (
<View style = { styles && styles.style } >
{ this._renderIcon() }
<Text style = { styles && styles.labelStyle } >
{ this.label }
</Text>
</View>
);
} else {
children = this._renderIcon();
}
// When using a wrapper view, apply the style to it instead of
// applying it to the TouchableHighlight.
const style = showLabel ? undefined : styles && styles.style;
return ( return (
<TouchableHighlight <TouchableHighlight
accessibilityLabel = { accessibilityLabel } accessibilityLabel = { accessibilityLabel }
disabled = { disabled } disabled = { disabled }
onPress = { onClick } onPress = { onClick }
style = { styles && styles.style } style = { style }
underlayColor = { styles && styles.underlayColor } > underlayColor = { styles && styles.underlayColor } >
<Icon { children }
name = { this._getIconName() }
style = { styles && styles.iconStyle } />
</TouchableHighlight> </TouchableHighlight>
); );
} }

View File

@ -55,8 +55,8 @@ export default {
...filmstrip, ...filmstrip,
bottom: 0, bottom: 0,
flexDirection: 'column', flexDirection: 'column',
left: 0,
position: 'absolute', position: 'absolute',
right: 0,
top: 0 top: 0
}, },

View File

@ -0,0 +1,91 @@
// @flow
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { hideDialog, BottomSheet } from '../../../base/dialog';
import { AudioRouteButton } from '../../../mobile/audio-mode';
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
import { RoomLockButton } from '../../../room-lock';
import AudioOnlyButton from './AudioOnlyButton';
import ToggleCameraButton from './ToggleCameraButton';
import { overflowMenuItemStyles } from './styles';
type Props = {
/**
* Used for hiding the dialog when the selection was completed.
*/
dispatch: Function,
};
/**
* The exported React {@code Component}. We need a reference to the wrapped
* component in order to be able to hide it using the dialog hiding logic.
*/
// eslint-disable-next-line prefer-const
let OverflowMenu_;
/**
* Implements a React {@code Component} with some extra actions in addition to
* those in the toolbar.
*/
class OverflowMenu extends Component<Props> {
/**
* Initializes a new {@code OverflowMenu} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onCancel = this._onCancel.bind(this);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
* @returns {ReactElement}
*/
render() {
return (
<BottomSheet onCancel = { this._onCancel }>
<AudioRouteButton
showLabel = { true }
styles = { overflowMenuItemStyles } />
<ToggleCameraButton
showLabel = { true }
styles = { overflowMenuItemStyles } />
<AudioOnlyButton
showLabel = { true }
styles = { overflowMenuItemStyles } />
<RoomLockButton
showLabel = { true }
styles = { overflowMenuItemStyles } />
<PictureInPictureButton
showLabel = { true }
styles = { overflowMenuItemStyles } />
</BottomSheet>
);
}
_onCancel: () => void;
/**
* Hides the dialog.
*
* @private
* @returns {void}
*/
_onCancel() {
this.props.dispatch(hideDialog(OverflowMenu_));
}
}
OverflowMenu_ = connect()(OverflowMenu);
export default OverflowMenu_;

View File

@ -0,0 +1,39 @@
// @flow
import { connect } from 'react-redux';
import { openDialog } from '../../../base/dialog';
import { translate } from '../../../base/i18n';
import { AbstractButton } from '../../../base/toolbox';
import type { AbstractButtonProps } from '../../../base/toolbox';
import OverflowMenu from './OverflowMenu';
type Props = AbstractButtonProps & {
/**
* The redux {@code dispatch} function.
*/
dispatch: Function
}
/**
* An implementation of a button for showing the {@code OverflowMenu}.
*/
class OverflowMenuButton extends AbstractButton<Props, *> {
accessibilityLabel = 'Overflow menu';
iconName = 'icon-thumb-menu';
label = 'toolbar.moreActions';
/**
* Handles clicking / pressing the button.
*
* @private
* @returns {void}
*/
_handleClick() {
this.props.dispatch(openDialog(OverflowMenu));
}
}
export default translate(connect()(OverflowMenuButton));

View File

@ -1,62 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import { TouchableHighlight } from 'react-native';
import { connect } from 'react-redux';
import { Icon } from '../../../base/font-icons';
import AbstractToolbarButton from '../AbstractToolbarButton';
/**
* Represents a button in {@link Toolbar} on React Native.
*
* @extends AbstractToolbarButton
*/
class ToolbarButton extends AbstractToolbarButton {
/**
* {@code ToolbarButton} component's property types.
*
* @static
*/
static propTypes = {
...AbstractToolbarButton.propTypes,
/**
* Indicates whether this {@code ToolbarButton} is disabled.
*/
disabled: PropTypes.bool
};
/**
* Renders the button of this {@code ToolbarButton}.
*
* @param {Object} children - The children, if any, to be rendered inside
* the button. Presumably, contains the icon of this {@code ToolbarButton}.
* @protected
* @returns {ReactElement} The button of this {@code ToolbarButton}.
*/
_renderButton(children) {
const props = {};
'accessibilityLabel' in this.props
&& (props.accessibilityLabel = this.props.accessibilityLabel);
'disabled' in this.props && (props.disabled = this.props.disabled);
'onClick' in this.props && (props.onPress = this._onClick);
'style' in this.props && (props.style = this.props.style);
'underlayColor' in this.props
&& (props.underlayColor = this.props.underlayColor);
return React.createElement(TouchableHighlight, props, children);
}
/**
* Renders the icon of this {@code ToolbarButton}.
*
* @inheritdoc
*/
_renderIcon() {
return super._renderIcon(Icon);
}
}
export default connect()(ToolbarButton);

View File

@ -9,52 +9,28 @@ import {
isNarrowAspectRatio, isNarrowAspectRatio,
makeAspectRatioAware makeAspectRatioAware
} from '../../../base/responsive-ui'; } from '../../../base/responsive-ui';
import { ColorPalette } from '../../../base/styles';
import { InviteButton } from '../../../invite'; import { InviteButton } from '../../../invite';
import { AudioRouteButton } from '../../../mobile/audio-mode';
import { PictureInPictureButton } from '../../../mobile/picture-in-picture';
import { RoomLockButton } from '../../../room-lock';
import AudioMuteButton from '../AudioMuteButton'; import AudioMuteButton from '../AudioMuteButton';
import AudioOnlyButton from './AudioOnlyButton';
import HangupButton from '../HangupButton'; import HangupButton from '../HangupButton';
import styles from './styles';
import ToggleCameraButton from './ToggleCameraButton';
import VideoMuteButton from '../VideoMuteButton'; import VideoMuteButton from '../VideoMuteButton';
/** import OverflowMenuButton from './OverflowMenuButton';
* Styles for the hangup button. import styles, {
*/ hangupButtonStyles,
const hangupButtonStyles = { toolbarButtonStyles,
iconStyle: styles.whitePrimaryToolbarButtonIcon, toolbarToggledButtonStyles
style: styles.hangup, } from './styles';
underlayColor: ColorPalette.buttonUnderlay
};
/** /**
* Styles for buttons in the primary toolbar. * Number of buttons in the toolbar.
*/ */
const primaryToolbarButtonStyles = { const NUM_TOOLBAR_BUTTONS = 4;
iconStyle: styles.primaryToolbarButtonIcon,
style: styles.primaryToolbarButton
};
/** /**
* Styles for buttons in the primary toolbar. * Factor relating the hangup button and other toolbar buttons.
*/ */
const primaryToolbarToggledButtonStyles = { const TOOLBAR_BUTTON_SIZE_FACTOR = 0.8;
iconStyle: styles.whitePrimaryToolbarButtonIcon,
style: styles.whitePrimaryToolbarButton
};
/**
* Styles for buttons in the secondary toolbar.
*/
const secondaryToolbarButtonStyles = {
iconStyle: styles.secondaryToolbarButtonIcon,
style: styles.secondaryToolbarButton,
underlayColor: 'transparent'
};
/** /**
* The type of {@link Toolbox}'s React {@code Component} props. * The type of {@link Toolbox}'s React {@code Component} props.
@ -62,20 +38,85 @@ const secondaryToolbarButtonStyles = {
type Props = { type Props = {
/** /**
* The indicator which determines whether the toolbox is enabled. * The indicator which determines whether the toolbox is visible.
*/ */
_enabled: boolean, _visible: boolean,
/** /**
* Flag showing whether toolbar is visible. * The redux {@code dispatch} function.
*/ */
_visible: boolean dispatch: Function
};
/**
* The type of {@link Toolbox}'s React {@code Component} state.
*/
type State = {
/**
* The detected width for this component.
*/
width: number
}; };
/** /**
* Implements the conference toolbox on React Native. * Implements the conference toolbox on React Native.
*/ */
class Toolbox extends Component<Props> { class Toolbox extends Component<Props, State> {
state = {
width: 0
};
/**
* Initializes a new {@code Toolbox} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this._onLayout = this._onLayout.bind(this);
}
_onLayout: (Object) => void;
/**
* Handles the "on layout" View's event and stores the width as state.
*
* @param {Object} event - The "on layout" event object/structure passed
* by react-native.
* @private
* @returns {void}
*/
_onLayout({ nativeEvent: { layout: { width } } }) {
this.setState({ width });
}
/**
* Calculates how large our toolbar buttons can be, given the available
* width. In the future we might want to have a size threshold, and once
* it's passed a completely different style could be used, akin to the web.
*
* @private
* @returns {number}
*/
_calculateToolbarButtonSize() {
const width = this.state.width;
const hangupButtonSize = styles.hangupButton.width;
let buttonSize
= (width - hangupButtonSize
- (NUM_TOOLBAR_BUTTONS * styles.toolbarButton.margin * 2))
/ NUM_TOOLBAR_BUTTONS;
// Make sure it's an even number.
buttonSize = 2 * Math.round(buttonSize / 2);
// The button should be at most 80% of the hangup button's size.
return Math.min(
buttonSize, hangupButtonSize * TOOLBAR_BUTTON_SIZE_FACTOR);
}
/** /**
* Implements React's {@link Component#render()}. * Implements React's {@link Component#render()}.
* *
@ -83,84 +124,54 @@ class Toolbox extends Component<Props> {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
render() { render() {
if (!this.props._enabled) {
return null;
}
const toolboxStyle const toolboxStyle
= isNarrowAspectRatio(this) = isNarrowAspectRatio(this)
? styles.toolboxNarrow ? styles.toolboxNarrow
: styles.toolboxWide; : styles.toolboxWide;
const { _visible } = this.props;
const buttonStyles = {
...toolbarButtonStyles
};
const toggledButtonStyles = {
...toolbarToggledButtonStyles
};
if (_visible && this.state.width) {
const buttonSize = this._calculateToolbarButtonSize();
const extraStyle = {
borderRadius: buttonSize / 2,
height: buttonSize,
width: buttonSize
};
buttonStyles.style = [ buttonStyles.style, extraStyle ];
toggledButtonStyles.style
= [ toggledButtonStyles.style, extraStyle ];
}
return ( return (
<Container <Container
onLayout = { this._onLayout }
style = { toolboxStyle } style = { toolboxStyle }
visible = { this.props._visible } > visible = { _visible } >
{ this._renderToolbars() } <View
pointerEvents = 'box-none'
style = { styles.toolbar }>
<InviteButton styles = { buttonStyles } />
<AudioMuteButton
styles = { buttonStyles }
toggledStyles = { toggledButtonStyles } />
<HangupButton styles = { hangupButtonStyles } />
<VideoMuteButton
styles = { buttonStyles }
toggledStyles = { toggledButtonStyles } />
<OverflowMenuButton
styles = { buttonStyles }
toggledStyles = { toggledButtonStyles } />
</View>
</Container> </Container>
); );
} }
/**
* Renders the toolbar which contains the primary buttons such as hangup,
* audio and video mute.
*
* @private
* @returns {ReactElement}
*/
_renderPrimaryToolbar() {
return (
<View
key = 'primaryToolbar'
pointerEvents = 'box-none'
style = { styles.primaryToolbar }>
<AudioMuteButton
styles = { primaryToolbarButtonStyles }
toggledStyles = { primaryToolbarToggledButtonStyles } />
<HangupButton styles = { hangupButtonStyles } />
<VideoMuteButton
styles = { primaryToolbarButtonStyles }
toggledStyles = { primaryToolbarToggledButtonStyles } />
</View>
);
}
/**
* Renders the toolbar which contains the secondary buttons such as toggle
* camera facing mode.
*
* @private
* @returns {ReactElement}
*/
_renderSecondaryToolbar() {
return (
<View
key = 'secondaryToolbar'
pointerEvents = 'box-none'
style = { styles.secondaryToolbar }>
<AudioRouteButton styles = { secondaryToolbarButtonStyles } />
<ToggleCameraButton styles = { secondaryToolbarButtonStyles } />
<AudioOnlyButton styles = { secondaryToolbarButtonStyles } />
<RoomLockButton styles = { secondaryToolbarButtonStyles } />
<InviteButton styles = { secondaryToolbarButtonStyles } />
<PictureInPictureButton
styles = { secondaryToolbarButtonStyles } />
</View>
);
}
/**
* Renders the primary and the secondary toolbars.
*
* @private
* @returns {[ReactElement, ReactElement]}
*/
_renderToolbars() {
return [
this._renderSecondaryToolbar(),
this._renderPrimaryToolbar()
];
}
} }
/** /**
@ -179,21 +190,7 @@ function _mapStateToProps(state: Object): Object {
const { enabled, visible } = state['features/toolbox']; const { enabled, visible } = state['features/toolbox'];
return { return {
/** _visible: enabled && visible
* The indicator which determines whether the toolbox is enabled.
*
* @private
* @type {boolean}
*/
_enabled: enabled,
/**
* Flag showing whether toolbox is visible.
*
* @protected
* @type {boolean}
*/
_visible: visible
}; };
} }

View File

@ -1,2 +1 @@
export { default as ToolbarButton } from './ToolbarButton';
export { default as Toolbox } from './Toolbox'; export { default as Toolbox } from './Toolbox';

View File

@ -1,152 +1,83 @@
import { BoxModel, ColorPalette, createStyleSheet } from '../../../base/styles'; import { BoxModel, ColorPalette, createStyleSheet } from '../../../base/styles';
/** /**
* The base style for toolbars. * The style of toolbar buttons.
*
* @type {Object}
*/ */
const _toolbar = { const toolbarButton = {
flex: 0,
position: 'absolute'
};
/**
* The base style of toolbar buttons (in primaryToolbar and secondaryToolbar).
*
* @type {Object}
*/
const _toolbarButton = {
flex: 0,
justifyContent: 'center',
opacity: 0.7
};
/**
* The base icon style of toolbar buttons (in primaryToolbar and
* secondaryToolbar).
*
* @type {Object}
*/
const _toolbarButtonIcon = {
alignSelf: 'center'
};
/**
* The style of toolbar buttons in primaryToolbar.
*/
const primaryToolbarButton = {
..._toolbarButton,
backgroundColor: ColorPalette.white, backgroundColor: ColorPalette.white,
borderRadius: 30, borderRadius: 20,
borderWidth: 0, borderWidth: 0,
flex: 0,
flexDirection: 'row', flexDirection: 'row',
height: 60, height: 40,
justifyContent: 'center',
margin: BoxModel.margin, margin: BoxModel.margin,
width: 60 marginBottom: BoxModel.margin / 2,
opacity: 0.7,
width: 40
}; };
/** /**
* The icon style of the toolbar buttons in primaryToolbar. * The icon style of the toolbar buttons.
*
* @type {Object}
*/ */
const primaryToolbarButtonIcon = { const toolbarButtonIcon = {
..._toolbarButtonIcon, alignSelf: 'center',
color: ColorPalette.darkGrey, color: ColorPalette.darkGrey,
fontSize: 24 fontSize: 22
}; };
/** /**
* The icon style of the toolbar buttons in secondaryToolbar. * The Toolbox and toolbar related styles.
*
* @type {Object}
*/ */
const secondaryToolbarButtonIcon = { const styles = createStyleSheet({
..._toolbarButtonIcon,
color: ColorPalette.white,
fontSize: 18
};
/**
* The (conference) Toolbox/Toolbar related styles.
*/
export default createStyleSheet({
/** /**
* The style of the toolbar button in {@link #primaryToolbar} which * The style of the toolbar button which hangs the current conference up.
* hangs the current conference up.
*/ */
hangup: { hangupButton: {
...primaryToolbarButton, ...toolbarButton,
backgroundColor: ColorPalette.red backgroundColor: ColorPalette.red,
borderRadius: 30,
height: 60,
width: 60
}, },
/** /**
* The icon style of toolbar buttons in {@link #primaryToolbar} which * The icon style of toolbar buttons which hangs the current conference up.
* hangs the current conference up.
*/ */
hangupButtonIcon: { hangupButtonIcon: {
...primaryToolbarButtonIcon, ...toolbarButtonIcon,
color: ColorPalette.white color: ColorPalette.white,
fontSize: 24
}, },
/** /**
* The style of the toolbar which contains the primary buttons such as * The style of the toolbar.
* hangup, audio and video mute.
*/ */
primaryToolbar: { toolbar: {
..._toolbar, alignItems: 'center',
bottom: 0, bottom: 0,
flex: 0,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
left: 0, left: 0,
position: 'absolute',
right: 0 right: 0
}, },
/** /**
* The style of toolbar buttons in {@link #primaryToolbar}. * The style of toolbar buttons.
*/ */
primaryToolbarButton, toolbarButton,
/** /**
* The icon style of the toolbar buttons in {@link #primaryToolbar}. * The icon style of the toolbar buttons.
*/ */
primaryToolbarButtonIcon, toolbarButtonIcon,
/** /**
* The style of the toolbar which contains the secondary buttons such as * The style of the root/top-level {@link Container} of {@link Toolbox}.
* toggle camera facing mode. * This is the narrow layout version which locates the toolbar on top of
*/ * the filmstrip, at the bottom of the screen.
secondaryToolbar: {
..._toolbar,
bottom: 0,
flexDirection: 'column',
right: 0,
top: 0
},
/**
* The style of toolbar buttons in {@link #secondaryToolbar}.
*/
secondaryToolbarButton: {
..._toolbarButton,
backgroundColor: ColorPalette.darkGrey,
borderRadius: 20,
flexDirection: 'column',
height: 40,
margin: BoxModel.margin / 2,
width: 40
},
/**
* The icon style of the toolbar buttons in {@link #secondaryToolbar}.
*/
secondaryToolbarButtonIcon,
/**
* The style of the root/top-level {@link Container} of {@link Toolbox}
* which contains {@link Toolbar}s. This is narrow layout version which
* spans from the top of the screen to the top of the filmstrip located at
* the bottom of the screen.
*/ */
toolboxNarrow: { toolboxNarrow: {
flexDirection: 'column', flexDirection: 'column',
@ -154,11 +85,9 @@ export default createStyleSheet({
}, },
/** /**
* The style of the root/top-level {@link Container} of {@link Toolbox} * The style of the root/top-level {@link Container} of {@link Toolbox}.
* which contains {@link Toolbar}s. This is wide layout version which spans * This is the wide layout version which locates the toolbar at the bottom
* from the top to the bottom of the screen and is located to the right of * of the screen.
* the filmstrip which is displayed as a column on the left side of the
* screen.
*/ */
toolboxWide: { toolboxWide: {
bottom: 0, bottom: 0,
@ -169,20 +98,89 @@ export default createStyleSheet({
}, },
/** /**
* The style of toolbar buttons in {@link #primaryToolbar} which display * The style of toolbar buttons which display white icons.
* white icons.
*/ */
whitePrimaryToolbarButton: { whiteToolbarButton: {
...primaryToolbarButton, ...toolbarButton,
backgroundColor: ColorPalette.buttonUnderlay backgroundColor: ColorPalette.buttonUnderlay
}, },
/** /**
* The icon style of toolbar buttons in {@link #primaryToolbar} which * The icon style of toolbar buttons which display white icons.
* display white icons.
*/ */
whitePrimaryToolbarButtonIcon: { whiteToolbarButtonIcon: {
...primaryToolbarButtonIcon, ...toolbarButtonIcon,
color: ColorPalette.white color: ColorPalette.white
} }
}); });
export default styles;
/**
* Styles for the hangup button.
*/
export const hangupButtonStyles = {
iconStyle: styles.whiteToolbarButtonIcon,
style: styles.hangupButton,
underlayColor: ColorPalette.buttonUnderlay
};
/**
* Styles for buttons in the toolbar.
*/
export const toolbarButtonStyles = {
iconStyle: styles.toolbarButtonIcon,
style: styles.toolbarButton
};
/**
* Styles for toggled buttons in the toolbar.
*/
export const toolbarToggledButtonStyles = {
iconStyle: styles.whiteToolbarButtonIcon,
style: styles.whiteToolbarButton
};
/**
* Styles for the {@code OverflowMenu} items.
*
* These have been implemented as per the Material Design guidelines:
* {@link https://material.io/guidelines/components/bottom-sheets.html}.
*/
const overflowMenuStyles = createStyleSheet({
/**
* Container style for a {@code ToolboxItem} rendered in the
* {@code OverflowMenu}.
*/
container: {
alignItems: 'center',
flexDirection: 'row',
height: 48
},
/**
* Style for the {@code Icon} element in a {@code ToolboxItem} rendered in
* the {@code OverflowMenu}.
*/
icon: {
fontSize: 24
},
/**
* Style for the label in a {@code ToolboxItem} rendered in the
* {@code OverflowMenu}.
*/
label: {
flex: 1,
fontSize: 16,
marginLeft: 32,
opacity: 0.87
}
});
export const overflowMenuItemStyles = {
iconStyle: overflowMenuStyles.icon,
labelStyle: overflowMenuStyles.label,
style: overflowMenuStyles.container,
underlayColor: '#eee'
};