diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/channel_drawer/channel_drawer.js index bf44d9403..a9f82316c 100644 --- a/app/components/channel_drawer/channel_drawer.js +++ b/app/components/channel_drawer/channel_drawer.js @@ -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 ( { - 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 = ( - - - - ); - list = ( {title} - {settings} {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 diff --git a/app/components/channel_drawer/index.js b/app/components/channel_drawer/index.js index 4324d3834..f3f01faaa 100644 --- a/app/components/channel_drawer/index.js +++ b/app/components/channel_drawer/index.js @@ -43,4 +43,4 @@ function mapDispatchToProps(dispatch) { }; } -export default connect(mapStateToProps, mapDispatchToProps)(ChannelDrawer); +export default connect(mapStateToProps, mapDispatchToProps, null, {withRef: true})(ChannelDrawer); diff --git a/app/components/channel_drawer/teams_list/teams_list.js b/app/components/channel_drawer/teams_list/teams_list.js index 290b93eb0..d2f90c6a3 100644 --- a/app/components/channel_drawer/teams_list/teams_list.js +++ b/app/components/channel_drawer/teams_list/teams_list.js @@ -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) { diff --git a/app/components/profile_picture/index.js b/app/components/profile_picture/index.js index 90cb284d5..f9911663a 100644 --- a/app/components/profile_picture/index.js +++ b/app/components/profile_picture/index.js @@ -18,7 +18,7 @@ function mapStateToProps(state, ownProps) { } return { - theme: ownProps.theme || getTheme(state), + theme: getTheme(state), status, user }; diff --git a/app/components/profile_picture/profile_picture.js b/app/components/profile_picture/profile_picture.js index a2fdda87d..74a40b12e 100644 --- a/app/components/profile_picture/profile_picture.js +++ b/app/components/profile_picture/profile_picture.js @@ -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 = ( - ); @@ -100,7 +68,7 @@ export default class ProfilePicture extends PureComponent { source={{uri: pictureUrl}} defaultSource={placeholder} /> - {this.props.status && + {this.props.showStatus && {statusIcon} @@ -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' } }; }); diff --git a/app/components/safe_area_view/safe_area_view.android.js b/app/components/safe_area_view/safe_area_view.android.js index 2bbdc2dac..7fdea6e86 100644 --- a/app/components/safe_area_view/safe_area_view.android.js +++ b/app/components/safe_area_view/safe_area_view.android.js @@ -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 ( - - {this.props.children} + + {children} ); } diff --git a/app/components/safe_area_view/safe_area_view.ios.js b/app/components/safe_area_view/safe_area_view.ios.js index b27ba6caf..bd2404476 100644 --- a/app/components/safe_area_view/safe_area_view.ios.js +++ b/app/components/safe_area_view/safe_area_view.ios.js @@ -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 ( + + {headerComponent} + + ); + } + return ( {this.renderTopBar()} {children} + + {footerComponent} + ); } diff --git a/app/components/settings_drawer/drawer_item.js b/app/components/settings_drawer/drawer_item.js new file mode 100644 index 000000000..915f06353 --- /dev/null +++ b/app/components/settings_drawer/drawer_item.js @@ -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 = (); + } + + let icon; + if (leftComponent) { + icon = leftComponent; + } else if (iconType && iconName) { + icon = ( + + ); + } + + let label; + if (labelComponent) { + label = labelComponent; + } else if (i18nId) { + label = ( + + ); + } + + return ( + + + {icon && + + {icon} + + } + + + {label} + + {divider} + + + + ); + } +} + +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 + } + }; +}); diff --git a/app/components/settings_drawer/index.js b/app/components/settings_drawer/index.js new file mode 100644 index 000000000..37315c3c0 --- /dev/null +++ b/app/components/settings_drawer/index.js @@ -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); diff --git a/app/components/settings_drawer/settings_drawer.js b/app/components/settings_drawer/settings_drawer.js new file mode 100644 index 000000000..030e84b6d --- /dev/null +++ b/app/components/settings_drawer/settings_drawer.js @@ -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 ( + + ); + }; + + renderUserStatusLabel = (userId) => { + return ( + + ); + }; + + renderContent = () => { + const {currentUser, navigator, theme} = this.props; + const style = getStyleSheet(theme); + + return ( + } + headerComponent={} + navigator={navigator} + theme={theme} + > + + + + + + + + + + + + + + + ); + }; + + render() { + const {children} = this.props; + + return ( + + {children} + + ); + } +} + +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 + } + }; +}); diff --git a/app/components/settings_drawer/status_label/index.js b/app/components/settings_drawer/status_label/index.js new file mode 100644 index 000000000..2908adb66 --- /dev/null +++ b/app/components/settings_drawer/status_label/index.js @@ -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); diff --git a/app/components/settings_drawer/status_label/status_label.js b/app/components/settings_drawer/status_label/status_label.js new file mode 100644 index 000000000..4286f7b3e --- /dev/null +++ b/app/components/settings_drawer/status_label/status_label.js @@ -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 ( + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + label: { + color: changeOpacity(theme.centerChannelColor, 0.5), + flex: 1, + fontSize: 17, + textAlignVertical: 'center', + includeFontPadding: false + } + }; +}); diff --git a/app/components/settings_drawer/user_info/index.js b/app/components/settings_drawer/user_info/index.js new file mode 100644 index 000000000..ed2502623 --- /dev/null +++ b/app/components/settings_drawer/user_info/index.js @@ -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); diff --git a/app/components/settings_drawer/user_info/user_info.js b/app/components/settings_drawer/user_info/user_info.js new file mode 100644 index 000000000..63a955e89 --- /dev/null +++ b/app/components/settings_drawer/user_info/user_info.js @@ -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 ( + + + + + + + {`@${user.username}`} + + + {Boolean(fullName) && + + {fullName} + + } + + + + ); + } +} + +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 + } + }; +}); diff --git a/app/components/user_status/index.js b/app/components/user_status/index.js new file mode 100644 index 000000000..8aa725cea --- /dev/null +++ b/app/components/user_status/index.js @@ -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); diff --git a/app/components/user_status/user_status.js b/app/components/user_status/user_status.js new file mode 100644 index 000000000..e195800f4 --- /dev/null +++ b/app/components/user_status/user_status.js @@ -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 ( + + ); + } +} diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index 6465fdf4c..8131c5b07 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -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 ( - - - - - - - - - - + + + + - - - {LocalConfig.EnableMobileClientUpgrade && } - + + + + + + + + + {LocalConfig.EnableMobileClientUpgrade && } + + ); } diff --git a/app/screens/channel/channel_nav_bar/channel_drawer_button.js b/app/screens/channel/channel_nav_bar/channel_drawer_button.js index 701c1558c..a6c6c47fd 100644 --- a/app/screens/channel/channel_nav_bar/channel_drawer_button.js +++ b/app/screens/channel/channel_nav_bar/channel_drawer_button.js @@ -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 = ( + + ); + return ( - - + + {icon} {badge} @@ -140,7 +145,8 @@ class ChannelDrawerButton extends PureComponent { const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { return { container: { - width: 40 + width: 40, + zIndex: 45 }, wrapper: { alignItems: 'center', diff --git a/app/screens/channel/channel_nav_bar/channel_nav_bar.js b/app/screens/channel/channel_nav_bar/channel_nav_bar.js index 63e0e2296..5ade262e5 100644 --- a/app/screens/channel/channel_nav_bar/channel_nav_bar.js +++ b/app/screens/channel/channel_nav_bar/channel_nav_bar.js @@ -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 ( - - + + + + ); } diff --git a/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js b/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js index e7eecab89..6439edd89 100644 --- a/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js +++ b/app/screens/channel/channel_nav_bar/channel_search_button/channel_search_button.js @@ -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 ( - - - - - + + + + + + + ); } } -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 + } + }; }); diff --git a/app/screens/channel/channel_nav_bar/channel_title/channel_title.js b/app/screens/channel/channel_nav_bar/channel_title/channel_title.js index c2c2526cd..edb8d335e 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/channel_title.js +++ b/app/screens/channel/channel_nav_bar/channel_title/channel_title.js @@ -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 ( - - - {channelName} - - {icon} + + + + {channelName} + + {icon} + ); @@ -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' } }; }); diff --git a/app/screens/channel/channel_nav_bar/channel_title/index.js b/app/screens/channel/channel_nav_bar/channel_title/index.js index c7cabb106..663465b17 100644 --- a/app/screens/channel/channel_nav_bar/channel_title/index.js +++ b/app/screens/channel/channel_nav_bar/channel_title/index.js @@ -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) diff --git a/app/screens/channel/channel_nav_bar/settings_drawer_button.js b/app/screens/channel/channel_nav_bar/settings_drawer_button.js new file mode 100644 index 000000000..cb69c1c14 --- /dev/null +++ b/app/screens/channel/channel_nav_bar/settings_drawer_button.js @@ -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 = ( + + ); + + return ( + + + {icon} + + + ); + } +} + +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); diff --git a/app/screens/settings/general/index.js b/app/screens/settings/general/index.js index 9bcb427d1..ce7cc36bd 100644 --- a/app/screens/settings/general/index.js +++ b/app/screens/settings/general/index.js @@ -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) }; diff --git a/app/screens/settings/general/settings.js b/app/screens/settings/general/settings.js index 3822b8fcc..9fd3953fc 100644 --- a/app/screens/settings/general/settings.js +++ b/app/screens/settings/general/settings.js @@ -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 && - + } - - - - - ); @@ -329,9 +308,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { divider: { backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), height: 1 - }, - footer: { - marginTop: 35 } }; });