Right drawer (#1275)

* Add right sidebar

* initial implementation of the right drawer

* Do not hide status bar on drawer open

* initial options in the RHD

* Fix android layout

* feedback review
This commit is contained in:
enahum 2017-12-22 15:50:02 -03:00 committed by GitHub
parent db3556325e
commit a708a1755b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 1058 additions and 252 deletions

View file

@ -50,8 +50,8 @@ export default class ChannelDrawer extends Component {
theme: PropTypes.object.isRequired
};
closeLeftHandle = null;
openLeftHandle = null;
closeHandle = null;
openHandle = null;
swiperIndex = 1;
constructor(props) {
@ -71,7 +71,6 @@ export default class ChannelDrawer extends Component {
}
componentDidMount() {
EventEmitter.on('open_channel_drawer', this.openChannelDrawer);
EventEmitter.on('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
@ -104,7 +103,6 @@ export default class ChannelDrawer extends Component {
}
componentWillUnmount() {
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle);
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
@ -132,15 +130,15 @@ export default class ChannelDrawer extends Component {
handleDrawerClose = () => {
this.resetDrawer();
if (this.closeLeftHandle) {
InteractionManager.clearInteractionHandle(this.closeLeftHandle);
this.closeLeftHandle = null;
if (this.closeHandle) {
InteractionManager.clearInteractionHandle(this.closeHandle);
this.closeHandle = null;
}
};
handleDrawerCloseStart = () => {
if (!this.closeLeftHandle) {
this.closeLeftHandle = InteractionManager.createInteractionHandle();
if (!this.closeHandle) {
this.closeHandle = InteractionManager.createInteractionHandle();
}
};
@ -149,15 +147,15 @@ export default class ChannelDrawer extends Component {
Keyboard.dismiss();
}
if (this.openLeftHandle) {
InteractionManager.clearInteractionHandle(this.openLeftHandle);
this.openLeftHandle = null;
if (this.openHandle) {
InteractionManager.clearInteractionHandle(this.openHandle);
this.openHandle = null;
}
};
handleDrawerOpenStart = () => {
if (!this.openLeftHandle) {
this.openLeftHandle = InteractionManager.createInteractionHandle();
if (!this.openHandle) {
this.openHandle = InteractionManager.createInteractionHandle();
}
};
@ -367,6 +365,7 @@ export default class ChannelDrawer extends Component {
return (
<SafeAreaView
backgroundColor={theme.sidebarHeaderBg}
footerColor={theme.sidebarHeaderBg}
navigator={navigator}
>
<DrawerSwiper

View file

@ -5,15 +5,12 @@ import React from 'react';
import PropTypes from 'prop-types';
import {
Platform,
TouchableHighlight,
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import SearchBar from 'app/components/search_bar';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FilteredList from './filtered_list';
@ -57,30 +54,6 @@ class ChannelsList extends React.PureComponent {
}
};
openSettingsModal = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
screen: 'Settings',
title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
navigatorButtons: {
leftButtons: [{
id: 'close-settings',
icon: this.closeButton
}]
}
});
});
onSearch = (term) => {
this.setState({term});
};
@ -107,7 +80,6 @@ class ChannelsList extends React.PureComponent {
const {searching, term} = this.state;
const styles = getStyleSheet(theme);
let settings;
let list;
if (searching) {
list = (
@ -118,19 +90,6 @@ class ChannelsList extends React.PureComponent {
/>
);
} else {
settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={this.openSettingsModal}
underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)}
>
<AwesomeIcon
name='cog'
style={styles.settings}
/>
</TouchableHighlight>
);
list = (
<List
navigator={navigator}
@ -183,7 +142,6 @@ class ChannelsList extends React.PureComponent {
/>
</View>
{title}
{settings}
</View>
</View>
{list}
@ -228,26 +186,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
position: 'relative',
top: -1
},
settingsContainer: {
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 10,
...Platform.select({
android: {
height: 46,
marginRight: 6
},
ios: {
height: 44,
marginRight: 8
}
})
},
settings: {
color: theme.sidebarHeaderTextColor,
fontSize: 18,
fontWeight: '300'
},
titleContainer: { // These aren't used by this component, but they are passed down to the list component
alignItems: 'center',
flex: 1,
@ -266,6 +204,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
searchContainer: {
flex: 1,
paddingRight: 10,
...Platform.select({
android: {
marginBottom: 1

View file

@ -43,4 +43,4 @@ function mapDispatchToProps(dispatch) {
};
}
export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawer);
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelDrawer);

View file

@ -6,6 +6,7 @@ import PropTypes from 'prop-types';
import {
FlatList,
Platform,
StatusBar,
Text,
TouchableHighlight,
View
@ -44,6 +45,7 @@ class TeamsList extends PureComponent {
}
selectTeam = (teamId) => {
StatusBar.setHidden(false, 'slide');
requestAnimationFrame(() => {
const {actions, closeChannelDrawer, currentTeamId} = this.props;
if (teamId !== currentTeamId) {

View file

@ -18,7 +18,7 @@ function mapStateToProps(state, ownProps) {
}
return {
theme: ownProps.theme || getTheme(state),
theme: getTheme(state),
status,
user
};

View file

@ -5,22 +5,13 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Image, Platform, View} from 'react-native';
import {General} from 'mattermost-redux/constants';
import {AwayIcon, DndIcon, OfflineIcon, OnlineIcon} from 'app/components/status_icons';
import UserStatus from 'app/components/user_status';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import placeholder from 'assets/images/profile.jpg';
import {Client4} from 'mattermost-redux/client';
const statusToIcon = {
away: AwayIcon,
dnd: DndIcon,
offline: OfflineIcon,
online: OnlineIcon
};
const STATUS_BUFFER = Platform.select({
ios: 3,
android: 2
@ -32,6 +23,7 @@ export default class ProfilePicture extends PureComponent {
statusBorderWidth: PropTypes.number,
statusSize: PropTypes.number,
user: PropTypes.object,
showStatus: PropTypes.bool,
status: PropTypes.string,
theme: PropTypes.object.isRequired,
actions: PropTypes.shape({
@ -40,6 +32,7 @@ export default class ProfilePicture extends PureComponent {
};
static defaultProps = {
showStatus: true,
size: 128,
statusBorderWidth: 2,
statusSize: 14
@ -60,35 +53,10 @@ export default class ProfilePicture extends PureComponent {
pictureUrl = Client4.getProfilePictureUrl(this.props.user.id, this.props.user.last_picture_update);
}
let Icon;
let iconColor;
if (this.props.status && statusToIcon[this.props.status]) {
Icon = statusToIcon[this.props.status];
switch (this.props.status) {
case General.AWAY:
iconColor = theme.awayIndicator;
break;
case General.DND:
iconColor = theme.dndIndicator;
break;
case General.ONLINE:
iconColor = theme.onlineIndicator;
break;
default:
iconColor = theme.centerChannelColor;
break;
}
} else {
Icon = statusToIcon.offline;
iconColor = theme.centerChannelColor;
}
const statusIcon = (
<Icon
height={this.props.statusSize}
width={this.props.statusSize}
color={iconColor}
<UserStatus
size={this.props.statusSize}
status={this.props.status}
/>
);
@ -100,7 +68,7 @@ export default class ProfilePicture extends PureComponent {
source={{uri: pictureUrl}}
defaultSource={placeholder}
/>
{this.props.status &&
{this.props.showStatus &&
<View style={[style.statusWrapper, {borderRadius: this.props.statusSize / 2}]}>
{statusIcon}
</View>
@ -123,21 +91,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
},
status: {
color: theme.centerChannelBg
},
online: {
backgroundColor: theme.onlineIndicator
},
away: {
backgroundColor: theme.awayIndicator
},
dnd: {
backgroundColor: 'red'
},
offline: {
backgroundColor: theme.centerChannelBg
},
offlineIcon: {
borderColor: '#bababa'
}
};
});

View file

@ -7,13 +7,21 @@ import {StyleSheet, View} from 'react-native';
export default class SafeAreaAndroid extends PureComponent {
static propTypes = {
children: PropTypes.node.isRequired
backgroundColor: PropTypes.string,
children: PropTypes.node.isRequired,
theme: PropTypes.object.isRequired
};
render() {
const {backgroundColor, children, theme} = this.props;
let bgColor = theme.centerChannelBg;
if (backgroundColor) {
bgColor = backgroundColor;
}
return (
<View style={style.container}>
{this.props.children}
<View style={[style.container, {backgroundColor: bgColor}]}>
{children}
</View>
);
}

View file

@ -15,10 +15,13 @@ export default class SafeAreaIos extends PureComponent {
backgroundColor: PropTypes.string,
children: PropTypes.node.isRequired,
excludeHeader: PropTypes.bool,
footerColor: PropTypes.string,
footerComponent: PropTypes.node,
forceTop: PropTypes.number,
keyboardOffset: PropTypes.number.isRequired,
navBarBackgroundColor: PropTypes.string,
navigator: PropTypes.object,
headerComponent: PropTypes.node,
theme: PropTypes.object.isRequired
};
@ -103,7 +106,7 @@ export default class SafeAreaIos extends PureComponent {
renderTopBar = () => {
const {safeAreaInsets, statusBarHeight} = this.state;
const {excludeHeader, forceTop, navBarBackgroundColor, theme} = this.props;
const {headerComponent, excludeHeader, forceTop, navBarBackgroundColor, theme} = this.props;
const hideTopBar = excludeHeader || !statusBarHeight;
if (hideTopBar) {
@ -120,6 +123,20 @@ export default class SafeAreaIos extends PureComponent {
top = forceTop;
}
if (headerComponent) {
return (
<View
style={{
backgroundColor: topColor,
height: top,
zIndex: 10
}}
>
{headerComponent}
</View>
);
}
return (
<View
style={{
@ -132,7 +149,7 @@ export default class SafeAreaIos extends PureComponent {
};
render() {
const {backgroundColor, children, keyboardOffset, theme} = this.props;
const {backgroundColor, children, footerColor, footerComponent, keyboardOffset, theme} = this.props;
const {keyboard, safeAreaInsets} = this.state;
let bgColor = theme.centerChannelBg;
@ -140,6 +157,11 @@ export default class SafeAreaIos extends PureComponent {
bgColor = backgroundColor;
}
let bottomColor = theme.centerChannelBg;
if (footerColor) {
bottomColor = footerColor;
}
let offset = 0;
if (keyboardOffset && this.isX) {
offset = keyboardOffset;
@ -149,12 +171,14 @@ export default class SafeAreaIos extends PureComponent {
<View
style={{
flex: 1,
paddingBottom: keyboard ? offset : safeAreaInsets.bottom - 15,
backgroundColor: bgColor
}}
>
{this.renderTopBar()}
{children}
<View style={{height: keyboard ? offset : safeAreaInsets.bottom - 15, backgroundColor: bottomColor}}>
{footerComponent}
</View>
</View>
);
}

View file

@ -0,0 +1,150 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {TouchableOpacity, View} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import VectorIcon from 'app/components/vector_icon.js';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class DrawerItem extends PureComponent {
static propTypes = {
centered: PropTypes.bool,
defaultMessage: PropTypes.string,
i18nId: PropTypes.string,
iconName: PropTypes.string,
iconType: PropTypes.oneOf(['fontawesome', 'foundation', 'ion', 'material']),
isDestructor: PropTypes.bool,
labelComponent: PropTypes.node,
leftComponent: PropTypes.node,
onPress: PropTypes.func,
separator: PropTypes.bool,
theme: PropTypes.object.isRequired
};
static defaultProps = {
defaultMessage: '',
isDestructor: false,
separator: true
};
render() {
const {
centered,
defaultMessage,
i18nId,
iconName,
iconType,
isDestructor,
labelComponent,
leftComponent,
onPress,
separator,
theme
} = this.props;
const style = getStyleSheet(theme);
const destructor = {};
if (isDestructor) {
destructor.color = theme.errorTextColor;
}
let divider;
if (separator) {
divider = (<View style={style.divider}/>);
}
let icon;
if (leftComponent) {
icon = leftComponent;
} else if (iconType && iconName) {
icon = (
<VectorIcon
name={iconName}
type={iconType}
style={[style.icon, destructor]}
/>
);
}
let label;
if (labelComponent) {
label = labelComponent;
} else if (i18nId) {
label = (
<FormattedText
id={i18nId}
defaultMessage={defaultMessage}
style={[style.label, destructor, centered ? style.centerLabel : {}]}
/>
);
}
return (
<TouchableOpacity
onPress={onPress}
>
<View style={style.container}>
{icon &&
<View style={style.iconContainer}>
{icon}
</View>
}
<View style={style.wrapper}>
<View style={style.labelContainer}>
{label}
</View>
{divider}
</View>
</View>
</TouchableOpacity>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
alignItems: 'center',
backgroundColor: theme.centerChannelBg,
flexDirection: 'row',
height: 50
},
iconContainer: {
width: 45,
height: 50,
alignItems: 'center',
justifyContent: 'center',
marginLeft: 5
},
icon: {
color: theme.linkColor,
fontSize: 22
},
wrapper: {
flex: 1
},
labelContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row'
},
centerLabel: {
textAlign: 'center',
textAlignVertical: 'center'
},
label: {
color: changeOpacity(theme.centerChannelColor, 0.5),
flex: 1,
fontSize: 17,
textAlignVertical: 'center',
includeFontPadding: false
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.2),
height: 1
}
};
});

View file

@ -0,0 +1,28 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {logout} from 'mattermost-redux/actions/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import SettingsDrawer from './settings_drawer';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
function mapStateToProps(state) {
return {
currentUser: getCurrentUser(state),
theme: getTheme(state)
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
logout
}, dispatch)
};
}
export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(SettingsDrawer);

View file

@ -0,0 +1,310 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape} from 'react-intl';
import {
BackHandler,
InteractionManager,
Keyboard,
Platform,
ScrollView,
View
} from 'react-native';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import SafeAreaView from 'app/components/safe_area_view';
import Drawer from 'app/components/drawer';
import UserStatus from 'app/components/user_status';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import DrawerItem from './drawer_item';
import UserInfo from './user_info';
import StatusLabel from './status_label';
const DRAWER_INITIAL_OFFSET = 80;
export default class SettingsDrawer extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
logout: PropTypes.func.isRequired
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
currentUser: PropTypes.object.isRequired,
navigator: PropTypes.object,
theme: PropTypes.object.isRequired
};
static defaultProps = {
currentUser: {}
};
static contextTypes = {
intl: intlShape
};
constructor(props) {
super(props);
this.closeHandle = null;
this.openHandle = null;
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
this.closeButton = source;
});
}
componentDidMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack);
}
handleAndroidBack = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
return true;
}
return false;
};
openSettingsDrawer = () => {
this.props.blurPostTextBox();
if (this.refs.drawer && !this.refs.drawer.isOpened()) {
this.refs.drawer.open();
}
};
closeSettingsDrawer = () => {
if (this.refs.drawer && this.refs.drawer.isOpened()) {
this.refs.drawer.close();
}
};
handleDrawerTween = (ratio) => {
const opacity = (ratio / 2);
EventEmitter.emit('drawer_opacity', opacity);
return {
mainOverlay: {
backgroundColor: '#000',
elevation: 3,
opacity
}
};
};
handleDrawerClose = () => {
if (this.closeHandle) {
InteractionManager.clearInteractionHandle(this.closeHandle);
this.closeHandle = null;
}
};
handleDrawerCloseStart = () => {
if (!this.closeHandle) {
this.closeHandle = InteractionManager.createInteractionHandle();
}
};
handleDrawerOpen = () => {
Keyboard.dismiss();
if (this.openHandle) {
InteractionManager.clearInteractionHandle(this.openHandle);
this.openHandle = null;
}
};
handleDrawerOpenStart = () => {
if (!this.openHandle) {
this.openHandle = InteractionManager.createInteractionHandle();
}
};
goToSettings = wrapWithPreventDoubleTap(() => {
const {intl} = this.context;
const {navigator, theme} = this.props;
this.closeSettingsDrawer();
navigator.showModal({
screen: 'Settings',
title: intl.formatMessage({id: 'mobile.routes.settings', defaultMessage: 'Settings'}),
animationType: 'slide-up',
animated: true,
backButtonTitle: '',
navigatorStyle: {
navBarTextColor: theme.sidebarHeaderTextColor,
navBarBackgroundColor: theme.sidebarHeaderBg,
navBarButtonColor: theme.sidebarHeaderTextColor,
screenBackgroundColor: theme.centerChannelBg
},
navigatorButtons: {
leftButtons: [{
id: 'close-settings',
icon: this.closeButton
}]
}
});
});
logout = wrapWithPreventDoubleTap(() => {
const {logout} = this.props.actions;
this.closeSettingsDrawer();
InteractionManager.runAfterInteractions(logout);
});
renderUserStatusIcon = (userId) => {
return (
<UserStatus
size={18}
userId={userId}
/>
);
};
renderUserStatusLabel = (userId) => {
return (
<StatusLabel userId={userId}/>
);
};
renderContent = () => {
const {currentUser, navigator, theme} = this.props;
const style = getStyleSheet(theme);
return (
<SafeAreaView
backgroundColor={theme.centerChannelBg}
navBarBackgroundColor={theme.centerChannelBg}
footerColor={theme.centerChannelBg}
footerComponent={<View style={style.container}/>}
headerComponent={<View style={style.container}/>}
navigator={navigator}
theme={theme}
>
<View style={style.container}>
<ScrollView
alwaysBounceVertical={false}
contentContainerStyle={style.wrapper}
>
<UserInfo user={currentUser}/>
<View style={style.block}>
<DrawerItem
labelComponent={this.renderUserStatusLabel(currentUser.id)}
leftComponent={this.renderUserStatusIcon(currentUser.id)}
separator={true}
theme={theme}
/>
<DrawerItem
defaultMessage='Settings'
i18nId='mobile.routes.settings'
iconName='ios-options'
iconType='ion'
onPress={this.goToSettings}
separator={false}
theme={theme}
/>
</View>
<View style={style.separator}/>
<View style={style.block}>
<DrawerItem
centered={true}
defaultMessage='Logout'
i18nId='sidebar_right_menu.logout'
isDestructor={true}
onPress={this.logout}
separator={false}
theme={theme}
/>
</View>
</ScrollView>
</View>
</SafeAreaView>
);
};
render() {
const {children} = this.props;
return (
<Drawer
ref='drawer'
onOpenStart={this.handleDrawerOpenStart}
onOpen={this.handleDrawerOpen}
onClose={this.handleDrawerClose}
onCloseStart={this.handleDrawerCloseStart}
side='right'
captureGestures='open'
type='overlay'
acceptTap={true}
acceptPanOnDrawer={false}
disabled={false}
content={this.renderContent()}
tapToClose={true}
openDrawerOffset={DRAWER_INITIAL_OFFSET}
onRequestClose={this.closeSettingsDrawer}
panOpenMask={0.05}
panCloseMask={DRAWER_INITIAL_OFFSET}
panThreshold={0.25}
acceptPan={true}
negotiatePan={true}
useInteractionManager={false}
tweenDuration={100}
tweenHandler={this.handleDrawerTween}
elevation={5}
bottomPanOffset={Platform.OS === 'ios' ? 46 : 64}
topPanOffset={Platform.OS === 'ios' ? 64 : 46}
styles={{
main: {
shadowColor: '#000000',
shadowOpacity: 0.4,
shadowRadius: 12,
shadowOffset: {
width: -4,
height: 0
}
}
}}
>
{children}
</Drawer>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
backgroundColor: changeOpacity(theme.centerChannelColor, 0.03)
},
wrapper: {
flex: 1,
paddingTop: 0
},
block: {
borderBottomColor: changeOpacity(theme.centerChannelColor, 0.1),
borderBottomWidth: 1,
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
borderTopWidth: 1
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1
},
separator: {
marginTop: 35
}
};
});

View file

@ -0,0 +1,18 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
import StatusLabel from './status_label';
function mapStateToProps(state, ownProps) {
return {
status: getStatusForUserId(state, ownProps.userId),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(StatusLabel);

View file

@ -0,0 +1,63 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {General} from 'mattermost-redux/constants';
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class UserInfo extends PureComponent {
static propTypes = {
status: PropTypes.string,
theme: PropTypes.object.isRequired
};
static defaultProps = {
status: General.OFFLINE
};
render() {
const {status, theme} = this.props;
const style = getStyleSheet(theme);
let i18nId = 'status_dropdown.set_offline';
let defaultMessage = 'Offline';
switch (status) {
case General.AWAY:
i18nId = 'status_dropdown.set_away';
defaultMessage = 'Away';
break;
case General.DND:
i18nId = 'status_dropdown.set_dnd';
defaultMessage = 'Do Not Disturb';
break;
case General.ONLINE:
i18nId = 'status_dropdown.set_online';
defaultMessage = 'Online';
break;
}
return (
<FormattedText
id={i18nId}
defaultMessage={defaultMessage}
style={style.label}
/>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
label: {
color: changeOpacity(theme.centerChannelColor, 0.5),
flex: 1,
fontSize: 17,
textAlignVertical: 'center',
includeFontPadding: false
}
};
});

View file

@ -0,0 +1,16 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import UserInfo from './user_info';
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(UserInfo);

View file

@ -0,0 +1,85 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {Text, TouchableOpacity, View} from 'react-native';
import {emptyFunction} from 'app/utils/general';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import ProfilePicture from 'app/components/profile_picture';
export default class UserInfo extends PureComponent {
static propTypes = {
user: PropTypes.object.isRequired,
onPress: PropTypes.func,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onPress: emptyFunction
};
render() {
const {user, onPress, theme} = this.props;
const {first_name: firstName, last_name: lastName} = user;
const style = getStyleSheet(theme);
let fullName;
if (firstName && lastName) {
fullName = `${firstName} ${lastName}`;
} else if (firstName) {
fullName = firstName;
} else if (lastName) {
fullName = lastName;
}
return (
<TouchableOpacity onPress={onPress}>
<View style={style.container}>
<ProfilePicture
size={50}
showStatus={false}
userId={user.id}
/>
<View style={style.wrapper}>
<View>
<Text style={style.username}>
{`@${user.username}`}
</Text>
</View>
{Boolean(fullName) &&
<View>
<Text style={style.fullName}>{fullName}</Text>
</View>
}
</View>
</View>
</TouchableOpacity>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
alignItems: 'center',
padding: 15,
flexDirection: 'row'
},
wrapper: {
marginLeft: 10
},
username: {
color: theme.centerChannelColor,
marginVertical: 5,
fontWeight: '600',
fontSize: 15
},
fullName: {
color: changeOpacity(theme.centerChannelColor, 0.5),
fontSize: 15
}
};
});

View file

@ -0,0 +1,23 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getStatusForUserId} from 'mattermost-redux/selectors/entities/users';
import UserStatus from './user_status';
function mapStateToProps(state, ownProps) {
let status = ownProps.status;
if (!status && ownProps.userId) {
status = getStatusForUserId(state, ownProps.userId);
}
return {
status,
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(UserStatus);

View file

@ -0,0 +1,58 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {General} from 'mattermost-redux/constants';
import {AwayIcon, DndIcon, OfflineIcon, OnlineIcon} from 'app/components/status_icons';
const statusToIcon = {
away: AwayIcon,
dnd: DndIcon,
offline: OfflineIcon,
online: OnlineIcon
};
export default class UserStatus extends PureComponent {
static propTypes = {
size: PropTypes.number,
status: PropTypes.string,
theme: PropTypes.object.isRequired
};
static defaultProps = {
size: 14,
status: General.OFFLINE
};
render() {
const {size, status, theme} = this.props;
const Icon = statusToIcon[status];
let iconColor;
switch (status) {
case General.AWAY:
iconColor = theme.awayIndicator;
break;
case General.DND:
iconColor = theme.dndIndicator;
break;
case General.ONLINE:
iconColor = theme.onlineIndicator;
break;
default:
iconColor = theme.centerChannelColor;
break;
}
return (
<Icon
height={size}
width={size}
color={iconColor}
/>
);
}
}

View file

@ -8,10 +8,12 @@ import {
Platform,
View
} from 'react-native';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import ClientUpgradeListener from 'app/components/client_upgrade_listener';
import ChannelDrawer from 'app/components/channel_drawer';
import SettingsDrawer from 'app/components/settings_drawer';
import ChannelLoader from 'app/components/channel_loader';
import KeyboardLayout from 'app/components/layout/keyboard_layout';
import Loading from 'app/components/loading';
@ -103,6 +105,18 @@ class Channel extends PureComponent {
this.postTextbox.getWrappedInstance().getWrappedInstance().blur();
};
channelDrawerRef = (ref) => {
if (ref) {
this.channelDrawer = ref.getWrappedInstance();
}
};
settingsDrawerRef = (ref) => {
if (ref) {
this.settingsDrawer = ref.getWrappedInstance();
}
};
goToChannelInfo = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
const options = {
@ -164,6 +178,18 @@ class Channel extends PureComponent {
});
};
openChannelDrawer = () => {
if (this.channelDrawer) {
this.channelDrawer.openChannelDrawer();
}
};
openSettingsDrawer = () => {
if (this.settingsDrawer) {
this.settingsDrawer.openSettingsDrawer();
}
};
retryLoadChannels = () => {
this.loadChannels(this.props.currentTeamId);
};
@ -197,30 +223,39 @@ class Channel extends PureComponent {
return (
<ChannelDrawer
ref={this.channelDrawerRef}
blurPostTextBox={this.blurPostTextBox}
intl={intl}
navigator={navigator}
>
<SafeAreaView navigator={navigator}>
<StatusBar/>
<OfflineIndicator/>
<ChannelNavBar
navigator={navigator}
onPress={this.goToChannelInfo}
/>
<KeyboardLayout>
<View style={style.postList}>
<ChannelPostList navigator={navigator}/>
</View>
<ChannelLoader theme={theme}/>
<PostTextbox
ref={this.attachPostTextbox}
<SettingsDrawer
ref={this.settingsDrawerRef}
blurPostTextBox={this.blurPostTextBox}
navigator={navigator}
>
<SafeAreaView navigator={navigator}>
<StatusBar/>
<OfflineIndicator/>
<ChannelNavBar
navigator={navigator}
openChannelDrawer={this.openChannelDrawer}
openSettingsDrawer={this.openSettingsDrawer}
onPress={this.goToChannelInfo}
/>
<ChannelLoader theme={theme}/>
</KeyboardLayout>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</SafeAreaView>
<KeyboardLayout>
<View style={style.postList}>
<ChannelPostList navigator={navigator}/>
</View>
<ChannelLoader theme={theme}/>
<PostTextbox
ref={this.attachPostTextbox}
navigator={navigator}
/>
<ChannelLoader theme={theme}/>
</KeyboardLayout>
{LocalConfig.EnableMobileClientUpgrade && <ClientUpgradeListener navigator={navigator}/>}
</SafeAreaView>
</SettingsDrawer>
</ChannelDrawer>
);
}

View file

@ -16,7 +16,7 @@ import Icon from 'react-native-vector-icons/Ionicons';
import Badge from 'app/components/badge';
import PushNotifications from 'app/push_notifications';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {preventDoubleTap} from 'app/utils/tap';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import {getUnreadsInCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
@ -26,10 +26,11 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
class ChannelDrawerButton extends PureComponent {
static propTypes = {
currentTeamId: PropTypes.string.isRequired,
myTeamMembers: PropTypes.object,
theme: PropTypes.object,
openDrawer: PropTypes.func.isRequired,
messageCount: PropTypes.number,
mentionCount: PropTypes.number
mentionCount: PropTypes.number,
myTeamMembers: PropTypes.object,
theme: PropTypes.object
};
static defaultProps = {
@ -74,9 +75,9 @@ class ChannelDrawerButton extends PureComponent {
this.setState({opacity: value > 0 ? 0.1 : 1});
};
handlePress = () => {
EventEmitter.emit('open_channel_drawer');
};
handlePress = wrapWithPreventDoubleTap(() => {
this.props.openDrawer();
});
render() {
const {
@ -113,23 +114,27 @@ class ChannelDrawerButton extends PureComponent {
count={badgeCount}
minHeight={20}
minWidth={20}
onPress={() => preventDoubleTap(this.handlePress, this)}
onPress={this.handlePress}
/>
);
}
const icon = (
<Icon
name='md-menu'
size={25}
color={theme.sidebarHeaderTextColor}
/>
);
return (
<TouchableOpacity
{...this.panResponder.panHandlers}
onPress={this.handlePress}
style={style.container}
>
<View style={[style.wrapper, {opacity: this.state.opacity, zIndex: 30}]}>
<Icon
name='md-menu'
size={25}
color={theme.sidebarHeaderTextColor}
/>
<View style={[style.wrapper, {opacity: this.state.opacity}]}>
{icon}
{badge}
</View>
</TouchableOpacity>
@ -140,7 +145,8 @@ class ChannelDrawerButton extends PureComponent {
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
width: 40
width: 40,
zIndex: 45
},
wrapper: {
alignItems: 'center',

View file

@ -11,11 +11,14 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
import ChannelDrawerButton from './channel_drawer_button';
import ChannelSearchButton from './channel_search_button';
import ChannelTitle from './channel_title';
import SettingDrawerButton from './settings_drawer_button';
export default class ChannelNavBar extends PureComponent {
static propTypes = {
isLandscape: PropTypes.bool.isRequired,
navigator: PropTypes.object.isRequired,
openChannelDrawer: PropTypes.func.isRequired,
openSettingsDrawer: PropTypes.func.isRequired,
onPress: PropTypes.func.isRequired,
theme: PropTypes.object.isRequired
};
@ -27,7 +30,8 @@ export default class ChannelNavBar extends PureComponent {
}
render() {
const {isLandscape, navigator, theme, onPress} = this.props;
const {isLandscape, navigator, onPress, theme} = this.props;
const {openChannelDrawer, openSettingsDrawer} = this.props;
const style = getStyleFromTheme(theme);
const padding = {paddingHorizontal: 0};
@ -45,12 +49,17 @@ export default class ChannelNavBar extends PureComponent {
return (
<View style={[style.header, padding, {height}]}>
<ChannelDrawerButton/>
<ChannelTitle onPress={onPress}/>
<ChannelDrawerButton openDrawer={openChannelDrawer}/>
<ChannelTitle
onPress={onPress}
height={height}
/>
<View style={{flex: 1}}/>
<ChannelSearchButton
navigator={navigator}
theme={theme}
/>
<SettingDrawerButton openDrawer={openSettingsDrawer}/>
</View>
);
}

View file

@ -7,13 +7,13 @@
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
StyleSheet,
TouchableOpacity,
View
} from 'react-native';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
const SEARCH = 'search';
@ -50,33 +50,45 @@ export default class ChannelSearchButton extends PureComponent {
theme
} = this.props;
const style = getStyle(theme);
return (
<TouchableOpacity
onPress={this.handlePress}
style={style.container}
>
<View style={style.wrapper}>
<AwesomeIcon
name='search'
size={18}
color={theme.sidebarHeaderTextColor}
/>
</View>
</TouchableOpacity>
<View style={style.container}>
<TouchableOpacity
onPress={this.handlePress}
style={style.flex}
>
<View style={style.wrapper}>
<AwesomeIcon
name='search'
size={18}
style={style.icon}
/>
</View>
</TouchableOpacity>
</View>
);
}
}
const style = StyleSheet.create({
container: {
width: 40
},
wrapper: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
paddingHorizontal: 10,
zIndex: 30
}
const getStyle = makeStyleSheetFromTheme((theme) => {
return {
container: {
width: 40,
zIndex: 45
},
flex: {
flex: 1
},
wrapper: {
alignItems: 'flex-end',
flex: 1,
flexDirection: 'column',
justifyContent: 'center'
},
icon: {
backgroundColor: theme.sidebarHeaderBg,
color: theme.sidebarHeaderTextColor
}
};
});

View file

@ -16,7 +16,9 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelTitle extends PureComponent {
static propTypes = {
currentChannelName: PropTypes.string,
deviceWidth: PropTypes.number,
displayName: PropTypes.string,
height: PropTypes.number,
onPress: PropTypes.func,
theme: PropTypes.object
};
@ -28,7 +30,7 @@ export default class ChannelTitle extends PureComponent {
};
render() {
const {currentChannelName, displayName, onPress, theme} = this.props;
const {currentChannelName, deviceWidth, displayName, height, onPress, theme} = this.props;
const channelName = displayName || currentChannelName;
const style = getStyle(theme);
let icon;
@ -44,18 +46,20 @@ export default class ChannelTitle extends PureComponent {
return (
<TouchableOpacity
style={style.container}
style={[style.container, {height, width: deviceWidth}]}
onPress={onPress}
>
<View style={style.wrapper}>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.text}
>
{channelName}
</Text>
{icon}
<View style={[style.wrapper, {width: deviceWidth}]}>
<View style={style.innerContainer}>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={style.text}
>
{channelName}
</Text>
{icon}
</View>
</View>
</TouchableOpacity>
);
@ -65,24 +69,28 @@ export default class ChannelTitle extends PureComponent {
const getStyle = makeStyleSheetFromTheme((theme) => {
return {
container: {
flex: 1,
flexDirection: 'row'
position: 'absolute',
zIndex: 40
},
wrapper: {
alignItems: 'center',
flex: 1,
justifyContent: 'center'
},
innerContainer: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center',
marginHorizontal: 15
marginHorizontal: 55
},
icon: {
color: theme.sidebarHeaderTextColor,
marginHorizontal: 6
marginHorizontal: 5
},
text: {
color: theme.sidebarHeaderTextColor,
fontSize: 17,
fontWeight: 'bold'
fontWeight: 'bold',
textAlign: 'center'
}
};
});

View file

@ -4,15 +4,17 @@
import {connect} from 'react-redux';
import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getDimensions} from 'app/selectors/device';
import ChannelTitle from './channel_title';
function mapStateToProps(state) {
const currentChannel = getCurrentChannel(state);
return {
...getDimensions(state),
currentChannelName: currentChannel ? currentChannel.display_name : '',
displayName: state.views.channel.displayName,
theme: getTheme(state)

View file

@ -0,0 +1,84 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import {
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
class SettingDrawerButton extends PureComponent {
static propTypes = {
openDrawer: PropTypes.func.isRequired,
theme: PropTypes.object
};
static defaultProps = {
theme: {}
};
handlePress = wrapWithPreventDoubleTap(() => {
this.props.openDrawer();
});
render() {
const {theme} = this.props;
const style = getStyleFromTheme(theme);
const icon = (
<Icon
name='md-more'
size={25}
color={theme.sidebarHeaderTextColor}
/>
);
return (
<TouchableOpacity
onPress={this.handlePress}
style={style.container}
>
<View style={style.wrapper}>
{icon}
</View>
</TouchableOpacity>
);
}
}
const getStyleFromTheme = makeStyleSheetFromTheme((theme) => {
return {
container: {
width: 44,
zIndex: 45
},
wrapper: {
alignItems: 'center',
flex: 1,
flexDirection: 'column',
justifyContent: 'center',
marginLeft: 8,
zIndex: 30
},
mention: {
color: theme.mentionColor,
fontSize: 10
}
};
});
function mapStateToProps(state) {
return {
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(SettingDrawerButton);

View file

@ -5,7 +5,6 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {clearErrors} from 'mattermost-redux/actions/errors';
import {logout} from 'mattermost-redux/actions/users';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getJoinableTeams} from 'mattermost-redux/selectors/entities/teams';
@ -33,7 +32,6 @@ function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
clearErrors,
logout,
purgeOfflineStore
}, dispatch)
};

View file

@ -5,7 +5,6 @@ import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {intlShape, injectIntl} from 'react-intl';
import {
InteractionManager,
Linking,
Platform,
ScrollView,
@ -25,7 +24,6 @@ class Settings extends PureComponent {
static propTypes = {
actions: PropTypes.shape({
clearErrors: PropTypes.func.isRequired,
logout: PropTypes.func.isRequired,
purgeOfflineStore: PropTypes.func.isRequired
}).isRequired,
config: PropTypes.object.isRequired,
@ -166,11 +164,6 @@ class Settings extends PureComponent {
});
});
logout = wrapWithPreventDoubleTap(() => {
const {logout} = this.props.actions;
InteractionManager.runAfterInteractions(logout);
});
onNavigatorEvent = (event) => {
if (event.type === 'NavBarButtonPress') {
if (event.id === 'close-settings') {
@ -271,15 +264,15 @@ class Settings extends PureComponent {
theme={theme}
/>
{LocalConfig.EnableMobileClientUpgrade && LocalConfig.EnableMobileClientUpgradeUserSetting &&
<SettingsItem
defaultMessage='Check for Upgrade'
i18nId='mobile.settings.modal.check_for_upgrade'
iconName='update'
iconType='material'
onPress={this.goToClientUpgrade}
showArrow={showArrow}
theme={theme}
/>
<SettingsItem
defaultMessage='Check for Upgrade'
i18nId='mobile.settings.modal.check_for_upgrade'
iconName='update'
iconType='material'
onPress={this.goToClientUpgrade}
showArrow={showArrow}
theme={theme}
/>
}
<SettingsItem
defaultMessage='About Mattermost'
@ -292,20 +285,6 @@ class Settings extends PureComponent {
theme={theme}
/>
<View style={style.divider}/>
<View style={style.footer}>
<View style={style.divider}/>
<SettingsItem
centered={true}
defaultMessage='Logout'
i18nId='sidebar_right_menu.logout'
isDestructor={true}
onPress={this.logout}
separator={false}
showArrow={false}
theme={theme}
/>
<View style={style.divider}/>
</View>
</ScrollView>
</View>
);
@ -329,9 +308,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1
},
footer: {
marginTop: 35
}
};
});