[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,
/**
* Style for te item's label.
*/
labelStyle: Object,
/**
* Style for the item itself.
*/

View File

@ -1,7 +1,7 @@
// @flow
import React from 'react';
import { TouchableHighlight } from 'react-native';
import { Text, TouchableHighlight, View } from 'react-native';
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
* @returns {ReactElement}
@ -38,19 +52,38 @@ export default class ToolboxItem extends AbstractToolboxItem<Props> {
accessibilityLabel,
disabled,
onClick,
showLabel,
styles
} = 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 (
<TouchableHighlight
accessibilityLabel = { accessibilityLabel }
disabled = { disabled }
onPress = { onClick }
style = { styles && styles.style }
style = { style }
underlayColor = { styles && styles.underlayColor } >
<Icon
name = { this._getIconName() }
style = { styles && styles.iconStyle } />
{ children }
</TouchableHighlight>
);
}

View File

@ -55,8 +55,8 @@ export default {
...filmstrip,
bottom: 0,
flexDirection: 'column',
left: 0,
position: 'absolute',
right: 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,
makeAspectRatioAware
} from '../../../base/responsive-ui';
import { ColorPalette } from '../../../base/styles';
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 AudioOnlyButton from './AudioOnlyButton';
import HangupButton from '../HangupButton';
import styles from './styles';
import ToggleCameraButton from './ToggleCameraButton';
import VideoMuteButton from '../VideoMuteButton';
/**
* Styles for the hangup button.
*/
const hangupButtonStyles = {
iconStyle: styles.whitePrimaryToolbarButtonIcon,
style: styles.hangup,
underlayColor: ColorPalette.buttonUnderlay
};
import OverflowMenuButton from './OverflowMenuButton';
import styles, {
hangupButtonStyles,
toolbarButtonStyles,
toolbarToggledButtonStyles
} from './styles';
/**
* Styles for buttons in the primary toolbar.
* Number of buttons in the toolbar.
*/
const primaryToolbarButtonStyles = {
iconStyle: styles.primaryToolbarButtonIcon,
style: styles.primaryToolbarButton
};
const NUM_TOOLBAR_BUTTONS = 4;
/**
* Styles for buttons in the primary toolbar.
* Factor relating the hangup button and other toolbar buttons.
*/
const primaryToolbarToggledButtonStyles = {
iconStyle: styles.whitePrimaryToolbarButtonIcon,
style: styles.whitePrimaryToolbarButton
};
/**
* Styles for buttons in the secondary toolbar.
*/
const secondaryToolbarButtonStyles = {
iconStyle: styles.secondaryToolbarButtonIcon,
style: styles.secondaryToolbarButton,
underlayColor: 'transparent'
};
const TOOLBAR_BUTTON_SIZE_FACTOR = 0.8;
/**
* The type of {@link Toolbox}'s React {@code Component} props.
@ -62,20 +38,85 @@ const secondaryToolbarButtonStyles = {
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.
*/
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()}.
*
@ -83,84 +124,54 @@ class Toolbox extends Component<Props> {
* @returns {ReactElement}
*/
render() {
if (!this.props._enabled) {
return null;
}
const toolboxStyle
= isNarrowAspectRatio(this)
? styles.toolboxNarrow
: 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 (
<Container
onLayout = { this._onLayout }
style = { toolboxStyle }
visible = { this.props._visible } >
{ this._renderToolbars() }
visible = { _visible } >
<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>
);
}
/**
* 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'];
return {
/**
* 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
_visible: enabled && visible
};
}

View File

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

View File

@ -1,152 +1,83 @@
import { BoxModel, ColorPalette, createStyleSheet } from '../../../base/styles';
/**
* The base style for toolbars.
*
* @type {Object}
* The style of toolbar buttons.
*/
const _toolbar = {
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,
const toolbarButton = {
backgroundColor: ColorPalette.white,
borderRadius: 30,
borderRadius: 20,
borderWidth: 0,
flex: 0,
flexDirection: 'row',
height: 60,
height: 40,
justifyContent: 'center',
margin: BoxModel.margin,
width: 60
marginBottom: BoxModel.margin / 2,
opacity: 0.7,
width: 40
};
/**
* The icon style of the toolbar buttons in primaryToolbar.
*
* @type {Object}
* The icon style of the toolbar buttons.
*/
const primaryToolbarButtonIcon = {
..._toolbarButtonIcon,
const toolbarButtonIcon = {
alignSelf: 'center',
color: ColorPalette.darkGrey,
fontSize: 24
fontSize: 22
};
/**
* The icon style of the toolbar buttons in secondaryToolbar.
*
* @type {Object}
* The Toolbox and toolbar related styles.
*/
const secondaryToolbarButtonIcon = {
..._toolbarButtonIcon,
color: ColorPalette.white,
fontSize: 18
};
/**
* The (conference) Toolbox/Toolbar related styles.
*/
export default createStyleSheet({
const styles = createStyleSheet({
/**
* The style of the toolbar button in {@link #primaryToolbar} which
* hangs the current conference up.
* The style of the toolbar button which hangs the current conference up.
*/
hangup: {
...primaryToolbarButton,
backgroundColor: ColorPalette.red
hangupButton: {
...toolbarButton,
backgroundColor: ColorPalette.red,
borderRadius: 30,
height: 60,
width: 60
},
/**
* The icon style of toolbar buttons in {@link #primaryToolbar} which
* hangs the current conference up.
* The icon style of toolbar buttons which hangs the current conference up.
*/
hangupButtonIcon: {
...primaryToolbarButtonIcon,
color: ColorPalette.white
...toolbarButtonIcon,
color: ColorPalette.white,
fontSize: 24
},
/**
* The style of the toolbar which contains the primary buttons such as
* hangup, audio and video mute.
* The style of the toolbar.
*/
primaryToolbar: {
..._toolbar,
toolbar: {
alignItems: 'center',
bottom: 0,
flex: 0,
flexDirection: 'row',
justifyContent: 'center',
left: 0,
position: 'absolute',
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
* toggle camera facing mode.
*/
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.
* The style of the root/top-level {@link Container} of {@link Toolbox}.
* This is the narrow layout version which locates the toolbar on top of
* the filmstrip, at the bottom of the screen.
*/
toolboxNarrow: {
flexDirection: 'column',
@ -154,11 +85,9 @@ export default createStyleSheet({
},
/**
* The style of the root/top-level {@link Container} of {@link Toolbox}
* which contains {@link Toolbar}s. This is wide layout version which spans
* from the top to the bottom of the screen and is located to the right of
* the filmstrip which is displayed as a column on the left side of the
* screen.
* The style of the root/top-level {@link Container} of {@link Toolbox}.
* This is the wide layout version which locates the toolbar at the bottom
* of the screen.
*/
toolboxWide: {
bottom: 0,
@ -169,20 +98,89 @@ export default createStyleSheet({
},
/**
* The style of toolbar buttons in {@link #primaryToolbar} which display
* white icons.
* The style of toolbar buttons which display white icons.
*/
whitePrimaryToolbarButton: {
...primaryToolbarButton,
whiteToolbarButton: {
...toolbarButton,
backgroundColor: ColorPalette.buttonUnderlay
},
/**
* The icon style of toolbar buttons in {@link #primaryToolbar} which
* display white icons.
* The icon style of toolbar buttons which display white icons.
*/
whitePrimaryToolbarButtonIcon: {
...primaryToolbarButtonIcon,
whiteToolbarButtonIcon: {
...toolbarButtonIcon,
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'
};