[RN] Replace chat modal with SlidingView

This commit is contained in:
Bettenbuk Zoltan 2019-03-20 22:23:19 +01:00 committed by Zoltan Bettenbuk
parent 2a5adfc601
commit 13212a5980
11 changed files with 364 additions and 333 deletions

View File

@ -1,178 +0,0 @@
// @flow
import React, { PureComponent, type Node } from 'react';
import { Animated, TouchableWithoutFeedback, View } from 'react-native';
import styles, { SIDEBAR_WIDTH } from './styles';
/**
* The type of the React {@code Component} props of {@link SideBar}.
*/
type Props = {
/**
* The children of {@code SideBar}.
*/
children: Node,
/**
* Callback to notify the containing {@code Component} that the sidebar is
* closing.
*/
onHide: Function,
/**
* Whether the menu (of the {@code SideBar}?) is displayed/rendered/shown.
*/
show: boolean
};
/**
* The type of the React {@code Component} state of {@link SideBar}.
*/
type State = {
/**
* Whether the side overlay should be displayed/rendered/shown.
*/
showOverlay: boolean,
/**
* The native animation object.
*/
sliderAnimation: Animated.Value
};
/**
* A generic animated side bar to be used for left-side, hamburger-style menus.
*/
export default class SideBar extends PureComponent<Props, State> {
/**
* Implements React's {@link Component#getDerivedStateFromProps()}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: Props, prevState: State) {
return {
showOverlay: props.show || prevState.showOverlay
};
}
/**
* Initializes a new {@code SideBar} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
this.state = {
showOverlay: false,
sliderAnimation: new Animated.Value(0)
};
// Bind event handlers so they are only bound once per instance.
this._onHideMenu = this._onHideMenu.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}.
*
* @inheritdoc
*/
componentDidMount() {
this._setShow(this.props.show);
}
/**
* Implements React's {@link Component#componentDidUpdate()}.
*
* @inheritdoc
*/
componentDidUpdate() {
this._setShow(this.props.show);
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
return (
<View
pointerEvents = 'box-none'
style = { styles.sideMenuContainer } >
{
this.state.showOverlay
&& <TouchableWithoutFeedback
onPress = { this._onHideMenu } >
<View style = { styles.sideMenuShadow } />
</TouchableWithoutFeedback>
}
<Animated.View style = { this._getContentStyle() }>
{ this.props.children }
</Animated.View>
</View>
);
}
_getContentStyle: () => Array<Object>;
/**
* Assembles a style array for the sidebar content.
*
* @private
* @returns {Array<Object>}
*/
_getContentStyle() {
return [
styles.sideMenuContent,
{ transform: [ { translateX: this.state.sliderAnimation } ] }
];
}
_onHideMenu: () => void;
/**
* Hides the side menu.
*
* @private
* @returns {void}
*/
_onHideMenu() {
this._setShow(false);
const { onHide } = this.props;
onHide && onHide();
}
_setShow: (boolean) => void;
/**
* Shows/hides the side menu.
*
* @param {boolean} show - If the side menu is to be made visible,
* {@code true}; otherwise, {@code false}.
* @private
* @returns {void}
*/
_setShow(show) {
Animated
.timing(
/* value */ this.state.sliderAnimation,
/* config */ {
toValue: show ? SIDEBAR_WIDTH : 0,
useNativeDriver: true
})
.start(({ finished }) => {
finished && !show && this.setState({ showOverlay: false });
// XXX Technically, the arrow function can further be simplified
// by removing the {} and returning the boolean expression
// above. Practically and unfortunately though, Flow freaks out
// and states that Animated.timing doesn't exist!?
});
}
}

View File

@ -0,0 +1,265 @@
// @flow
import React, { PureComponent, type Node } from 'react';
import {
Animated,
Dimensions,
TouchableWithoutFeedback,
View
} from 'react-native';
import { type StyleType } from '../../../styles';
import styles from './slidingviewstyles';
/**
* The type of the React {@code Component} props of {@link SlidingView}.
*/
type Props = {
/**
* The children of {@code SlidingView}.
*/
children: Node,
/**
* Callback to notify the containing {@code Component} that the view is
* closing.
*/
onHide: Function,
/**
* Position of the SlidingView: 'left', 'right', 'top', 'bottom'.
* later).
*/
position: string,
/**
* Whether the {@code SlidingView} is to be displayed/rendered/shown or not.
*/
show: boolean,
/**
* Style of the animated view.
*/
style: StyleType
};
/**
* The type of the React {@code Component} state of {@link SlidingView}.
*/
type State = {
/**
* Whether the sliding overlay should be displayed/rendered/shown.
*/
showOverlay: boolean,
/**
* The native animation object.
*/
sliderAnimation: Animated.Value,
/**
* Offset to move the view out of the screen.
*/
positionOffset: number
};
/**
* A generic animated slider view to be used for animated menus.
*/
export default class SlidingView extends PureComponent<Props, State> {
/**
* True if the component is mounted.
*/
_mounted: boolean;
/**
* Implements React's {@link Component#getDerivedStateFromProps()}.
*
* @inheritdoc
*/
static getDerivedStateFromProps(props: Props, prevState: State) {
return {
showOverlay: props.show || prevState.showOverlay
};
}
/**
* Initializes a new {@code SlidingView} instance.
*
* @inheritdoc
*/
constructor(props: Props) {
super(props);
const { height, width } = Dimensions.get('window');
const { position } = props;
let positionOffset = height;
if (position === 'left' || position === 'right') {
positionOffset = width;
}
this.state = {
showOverlay: false,
sliderAnimation: new Animated.Value(0),
positionOffset
};
// Bind event handlers so they are only bound once per instance.
this._onHideMenu = this._onHideMenu.bind(this);
}
/**
* Implements React's {@link Component#componentDidMount()}.
*
* @inheritdoc
*/
componentDidMount() {
this._mounted = true;
this._setShow(this.props.show);
}
/**
* Implements React's {@link Component#componentDidUpdate()}.
*
* @inheritdoc
*/
componentDidUpdate() {
this._setShow(this.props.show);
}
/**
* Implements React's {@link Component#componentWillUnmount()}.
*
* @inheritdoc
*/
componentWillUnmount() {
this._mounted = false;
}
/**
* Implements React's {@link Component#render()}.
*
* @inheritdoc
*/
render() {
const { showOverlay } = this.state;
if (!showOverlay) {
return null;
}
return (
<View
pointerEvents = 'box-none'
style = { styles.sliderViewContainer } >
<TouchableWithoutFeedback
onPress = { this._onHideMenu } >
<View style = { styles.sliderViewShadow } />
</TouchableWithoutFeedback>
<Animated.View
style = { this._getContentStyle() }>
{ this.props.children }
</Animated.View>
</View>
);
}
_getContentStyle: () => Array<Object>;
/**
* Assembles a style array for the SlideView content.
*
* @private
* @returns {Array<Object>}
*/
_getContentStyle() {
const style = {
...this.props.style,
...styles.sliderViewContent
};
const { positionOffset } = this.state;
switch (this.props.position) {
case 'bottom':
Object.assign(style, {
bottom: -positionOffset,
left: 0,
right: 0,
top: positionOffset
}, {
transform: [ { translateY: this.state.sliderAnimation } ]
});
break;
case 'left':
Object.assign(style, {
bottom: 0,
left: -positionOffset,
right: positionOffset,
top: 0
}, {
transform: [ { translateX: this.state.sliderAnimation } ]
});
break;
}
return style;
}
_onHideMenu: () => void;
/**
* Hides the slider.
*
* @private
* @returns {void}
*/
_onHideMenu() {
this._setShow(false);
const { onHide } = this.props;
onHide && onHide();
}
_setShow: (boolean) => void;
/**
* Shows/hides the slider menu.
*
* @param {boolean} show - If the slider view is to be made visible,
* {@code true}; otherwise, {@code false}.
* @private
* @returns {void}
*/
_setShow(show) {
if (!this._mounted) {
return;
}
const { positionOffset } = this.state;
const { position } = this.props;
let toValue = positionOffset;
if (position === 'bottom' || position === 'right') {
toValue = -positionOffset;
}
Animated
.timing(
/* value */ this.state.sliderAnimation,
/* config */ {
duration: 200,
toValue: show ? toValue : 0,
useNativeDriver: true
})
.start(({ finished }) => {
finished && this._mounted && !show
&& this.setState({ showOverlay: false });
});
}
}

View File

@ -20,7 +20,7 @@ export { default as NavigateSectionListSectionHeader }
export { default as PagedList } from './PagedList';
export { default as Pressable } from './Pressable';
export { default as SectionList } from './SectionList';
export { default as SideBar } from './SideBar';
export { default as SlidingView } from './SlidingView';
export { default as Switch } from './Switch';
export { default as Text } from './Text';
export { default as TintedView } from './TintedView';

View File

@ -0,0 +1,31 @@
// @flow
import { StyleSheet } from 'react-native';
import { OVERLAY_Z_INDEX } from '../../constants';
export default {
/**
* The topmost container of the side bar.
*/
sliderViewContainer: {
...StyleSheet.absoluteFillObject,
zIndex: OVERLAY_Z_INDEX
},
/**
* The container of the actual content of the side menu.
*/
sliderViewContent: {
position: 'absolute'
},
/**
* The opaque area that covers the rest of the screen, when the side bar is
* open.
*/
sliderViewShadow: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)'
}
};

View File

@ -1,7 +1,5 @@
// @flow
import { StyleSheet } from 'react-native';
import { BoxModel, ColorPalette, createStyleSheet } from '../../../styles';
const AVATAR_OPACITY = 0.4;
@ -9,7 +7,6 @@ const OVERLAY_FONT_COLOR = 'rgba(255, 255, 255, 0.6)';
const SECONDARY_ACTION_BUTTON_SIZE = 30;
export const AVATAR_SIZE = 65;
export const SIDEBAR_WIDTH = 250;
export const UNDERLAY_COLOR = 'rgba(255, 255, 255, 0.2)';
/**
@ -241,35 +238,6 @@ const SECTION_LIST_STYLES = {
}
};
const SIDEBAR_STYLES = {
/**
* The topmost container of the side bar.
*/
sideMenuContainer: {
...StyleSheet.absoluteFillObject
},
/**
* The container of the actual content of the side menu.
*/
sideMenuContent: {
bottom: 0,
left: -SIDEBAR_WIDTH,
position: 'absolute',
top: 0,
width: SIDEBAR_WIDTH
},
/**
* The opaque area that covers the rest of the screen, when the side bar is
* open.
*/
sideMenuShadow: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0, 0, 0, 0.5)'
}
};
export const TINTED_VIEW_DEFAULT = {
backgroundColor: ColorPalette.appBackground,
opacity: 0.8
@ -281,6 +249,5 @@ export const TINTED_VIEW_DEFAULT = {
*/
export default createStyleSheet({
...PAGED_LIST_STYLES,
...SECTION_LIST_STYLES,
...SIDEBAR_STYLES
...SECTION_LIST_STYLES
});

View File

@ -0,0 +1,7 @@
// @flow
/**
* Z-index for components that are to be rendered like an overlay, to be over
* everything, such as modal-type of components, or dialogs.
*/
export const OVERLAY_Z_INDEX = 1000;

View File

@ -1,31 +1,28 @@
// @flow
import React from 'react';
import { SafeAreaView } from 'react-native';
import { SafeAreaView, View } from 'react-native';
import { GiftedChat } from 'react-native-gifted-chat';
import { translate } from '../../../base/i18n';
import { BackButton, Header, HeaderLabel, Modal } from '../../../base/react';
import {
BackButton,
Header,
HeaderLabel,
SlidingView
} from '../../../base/react';
import { connect } from '../../../base/redux';
import AbstractChat, {
_mapDispatchToProps,
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
_mapStateToProps,
type Props
} from '../AbstractChat';
import ChatMessage from './ChatMessage';
import styles from './styles';
type Props = AbstractProps & {
/**
* True if the chat window should have a solid BG render.
*/
_solidBackground: boolean
}
/**
* Implements a React native component that renders the chat window (modal) of
* the mobile client.
@ -55,32 +52,24 @@ class Chat extends AbstractChat<Props> {
// of messages.
const messages
= this.props._messages.map(this._transformMessage).reverse();
const modalStyle = [
styles.modalBackdrop
];
if (this.props._solidBackground) {
// We only use a transparent background, when we are in a video
// meeting to give a user a glympse of what's happening. Otherwise
// we use a non-transparent background.
modalStyle.push(styles.solidModalBackdrop);
}
return (
<Modal
onRequestClose = { this.props._onToggleChat }
visible = { this.props._isOpen }>
<Header>
<BackButton onPress = { this.props._onToggleChat } />
<HeaderLabel labelKey = 'chat.title' />
</Header>
<SafeAreaView style = { modalStyle }>
<GiftedChat
messages = { messages }
onSend = { this._onSend }
renderMessage = { this._renderMessage } />
</SafeAreaView>
</Modal>
<SlidingView
position = 'bottom'
show = { this.props._isOpen } >
<View style = { styles.chatContainer }>
<Header>
<BackButton onPress = { this.props._onToggleChat } />
<HeaderLabel labelKey = 'chat.title' />
</Header>
<SafeAreaView style = { styles.backdrop }>
<GiftedChat
messages = { messages }
onSend = { this._onSend }
renderMessage = { this._renderMessage } />
</SafeAreaView>
</View>
</SlidingView>
);
}
@ -146,21 +135,4 @@ class Chat extends AbstractChat<Props> {
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @returns {{
* _solidBackground: boolean
* }}
*/
function _mapStateToProps(state) {
const abstractReduxProps = _abstractMapStateToProps(state);
return {
...abstractReduxProps,
_solidBackground: state['features/base/conference'].audioOnly
};
}
export default translate(connect(_mapStateToProps, _mapDispatchToProps)(Chat));

View File

@ -8,8 +8,8 @@ import { Avatar } from '../../../base/participants';
import { connect } from '../../../base/redux';
import AbstractChatMessage, {
_mapStateToProps as _abstractMapStateToProps,
type Props as AbstractProps
_mapStateToProps,
type Props
} from '../AbstractChatMessage';
import styles from './styles';
@ -23,14 +23,6 @@ const AVATAR_SIZE = 32;
*/
const TIMESTAMP_FORMAT = 'H:mm';
type Props = AbstractProps & {
/**
* True if the chat window has a solid BG so then we have to adopt in style.
*/
_solidBackground: boolean
}
/**
* Renders a single chat message.
*/
@ -54,9 +46,6 @@ class ChatMessage extends AbstractChatMessage<Props> {
const textWrapperStyle = [
styles.textWrapper
];
const timeTextStyles = [
styles.timeText
];
if (localMessage) {
// The wrapper needs to be aligned to the right.
@ -69,10 +58,6 @@ class ChatMessage extends AbstractChatMessage<Props> {
textWrapperStyle.push(styles.systemTextWrapper);
}
if (this.props._solidBackground) {
timeTextStyles.push(styles.solidBGTimeText);
}
return (
<View style = { styles.messageWrapper } >
{
@ -92,7 +77,7 @@ class ChatMessage extends AbstractChatMessage<Props> {
{ message.text }
</Text>
</View>
<Text style = { timeTextStyles }>
<Text style = { styles.timeText }>
{ timeStamp }
</Text>
</View>
@ -133,20 +118,4 @@ class ChatMessage extends AbstractChatMessage<Props> {
}
}
/**
* Maps part of the Redux state to the props of this component.
*
* @param {Object} state - The Redux state.
* @param {Props} ownProps - The own props of the component.
* @returns {{
* _solidBackground: boolean
* }}
*/
function _mapStateToProps(state, ownProps) {
return {
..._abstractMapStateToProps(state, ownProps),
_solidBackground: state['features/base/conference'].audioOnly
};
}
export default translate(connect(_mapStateToProps)(ChatMessage));

View File

@ -1,9 +1,6 @@
// @flow
import {
ColorPalette,
createStyleSheet
} from '../../../base/styles';
import { ColorPalette } from '../../../base/styles';
/**
* The styles of the feature chat.
@ -13,7 +10,7 @@ import {
* need to extract the brand colors and sizes into a branding feature (planned
* for the future).
*/
export default createStyleSheet({
export default {
/**
* Wrapper View for the avatar.
@ -22,6 +19,19 @@ export default createStyleSheet({
marginRight: 8
},
/**
* Background of the chat screen.
*/
backdrop: {
backgroundColor: ColorPalette.white,
flex: 1
},
chatContainer: {
flex: 1,
flexDirection: 'column'
},
/**
* Wrapper for the details together, such as name, message and time.
*/
@ -58,16 +68,6 @@ export default createStyleSheet({
marginVertical: 4
},
/**
* Background of the chat screen. Currently it's set to a transparent value
* as the idea is that the participant would still want to see at least a
* part of the video when he/she is in the chat window.
*/
modalBackdrop: {
backgroundColor: 'rgba(127, 127, 127, 0.8)',
flex: 1
},
/**
* Style modifier for the {@code detailsWrapper} for own messages.
*/
@ -84,17 +84,6 @@ export default createStyleSheet({
borderTopRightRadius: 0
},
solidBGTimeText: {
color: 'rgb(164, 184, 209)'
},
/**
* Style modifier for the chat window when we're in audio only mode.
*/
solidModalBackdrop: {
backgroundColor: ColorPalette.white
},
/**
* Style modifier for system (error) messages.
*/
@ -118,7 +107,7 @@ export default createStyleSheet({
* Text node for the timestamp.
*/
timeText: {
color: ColorPalette.white,
color: 'rgb(164, 184, 209)',
fontSize: 13
}
});
};

View File

@ -11,7 +11,7 @@ import {
} from '../../base/participants';
import {
Header,
SideBar
SlidingView
} from '../../base/react';
import { connect } from '../../base/redux';
import { setSettingsViewVisible } from '../../settings';
@ -83,9 +83,11 @@ class WelcomePageSideBar extends Component<Props> {
*/
render() {
return (
<SideBar
<SlidingView
onHide = { this._onHideSideBar }
show = { this.props._visible }>
position = 'left'
show = { this.props._visible }
style = { styles.sideBar } >
<Header style = { styles.sideBarHeader }>
<Avatar
size = { SIDEBAR_AVATAR_SIZE }
@ -116,7 +118,7 @@ class WelcomePageSideBar extends Component<Props> {
url = { SEND_FEEDBACK_URL } />
</ScrollView>
</SafeAreaView>
</SideBar>
</SlidingView>
);
}

View File

@ -171,6 +171,13 @@ export default createStyleSheet({
flexDirection: 'column'
},
/**
* Container of the side bar.
*/
sideBar: {
width: 250
},
/**
* The body of the side bar where the items are.
*/