PLT-5880 Channel drawer UI (#365)

* fix drawer size and added tweener

* added opacity to drawer buttons

* Fix bad merge for navigation modal

* Fix channel drawer container UI

* Open Account Settings modal from channel drawer

* Fix modal props for options and image preview

* Channel drawer item UI

* Fix bottom unread indicator overlap
This commit is contained in:
enahum 2017-03-20 17:31:10 -03:00 committed by Harrison Healey
parent ac49aa6c11
commit 383a770fde
11 changed files with 555 additions and 463 deletions

View file

@ -158,8 +158,10 @@ export function showOptionsModal(options) {
return async (dispatch, getState) => {
dispatch({
type: NavigationTypes.NAVIGATION_MODAL,
route: Routes.OptionsModal,
props: options
route: {
...Routes.OptionsModal,
props: options
}
}, getState);
};
}

View file

@ -17,8 +17,14 @@ import {getPosts, getPostsSince} from 'mattermost-redux/actions/posts';
import {getFilesForPost} from 'mattermost-redux/actions/files';
import {savePreferences, deletePreferences} from 'mattermost-redux/actions/preferences';
import {getTeamMembersByIds} from 'mattermost-redux/actions/teams';
import {Constants, UsersTypes} from 'mattermost-redux/constants';
import {getChannelByName, getDirectChannelName, isDirectChannelVisible} from 'mattermost-redux/utils/channel_utils';
import {getProfilesInChannel} from 'mattermost-redux/actions/users';
import {Constants, Preferences, UsersTypes} from 'mattermost-redux/constants';
import {
getChannelByName,
getDirectChannelName,
isDirectChannelVisible,
isGroupChannelVisible
} from 'mattermost-redux/utils/channel_utils';
import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils';
export function loadChannelsIfNecessary(teamId) {
@ -49,8 +55,10 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
const {channels} = state.entities.channels;
const {myPreferences} = state.entities.preferences;
const {membersInTeam} = state.entities.teams;
const dmPrefs = getPreferencesByCategory(myPreferences, Constants.CATEGORY_DIRECT_CHANNEL_SHOW);
const dmPrefs = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_DIRECT_CHANNEL_SHOW);
const gmPrefs = getPreferencesByCategory(myPreferences, Preferences.CATEGORY_GROUP_CHANNEL_SHOW);
const members = [];
const loadProfilesForChannels = [];
for (const [key, pref] of dmPrefs) {
if (pref.value === 'true') {
@ -58,6 +66,19 @@ export function loadProfilesAndTeamMembersForDMSidebar(teamId) {
}
}
for (const [key, pref] of gmPrefs) {
if (pref.value === 'true') {
loadProfilesForChannels.push(key);
}
}
if (loadProfilesForChannels.length) {
for (let i = 0; i < loadProfilesForChannels.length; i++) {
const channelId = loadProfilesForChannels[i];
getProfilesInChannel(teamId, channelId, 0)(dispatch, getState);
}
}
let membersToLoad = members;
if (membersInTeam[teamId]) {
membersToLoad = members.filter((m) => !membersInTeam[teamId].has(m));
@ -125,9 +146,14 @@ export function selectInitialChannel(teamId) {
const currentChannel = channels[currentChannelId];
const {myPreferences} = state.entities.preferences;
const isDMVisible = currentChannel && currentChannel.type === Constants.DM_CHANNEL &&
isDirectChannelVisible(currentUserId, myPreferences, currentChannel);
const isGMVisible = currentChannel && currentChannel.type === Constants.GM_CHANNEL &&
isGroupChannelVisible(myPreferences, currentChannel);
if (currentChannel && myMembers[currentChannelId] &&
(currentChannel.team_id === teamId || (currentChannel.type === Constants.DM_CHANNEL &&
isDirectChannelVisible(currentUserId, myPreferences, currentChannel)))) {
(currentChannel.team_id === teamId || isDMVisible || isGMVisible)) {
await handleSelectChannel(currentChannelId)(dispatch, getState);
return;
}

View file

@ -1,27 +1,29 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {TouchableHighlight, Text, View} from 'react-native';
import React, {PropTypes, PureComponent} from 'react';
import {
StyleSheet,
TouchableHighlight,
Text,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import {OnlineStatus, AwayStatus, OfflineStatus} from 'app/components/status_icons';
import {changeOpacity} from 'app/utils/theme';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {Constants} from 'mattermost-redux/constants';
import Badge from 'app/components/badge';
export default class ChannelDrawerItem extends React.Component {
export default class ChannelDrawerItem extends PureComponent {
static propTypes = {
channel: React.PropTypes.object.isRequired,
onSelectChannel: React.PropTypes.func.isRequired,
handleClose: React.PropTypes.func,
onLongPress: React.PropTypes.func,
isActive: React.PropTypes.bool.isRequired,
hasUnread: React.PropTypes.bool.isRequired,
mentions: React.PropTypes.number.isRequired,
theme: React.PropTypes.object.isRequired
channel: PropTypes.object.isRequired,
onSelectChannel: PropTypes.func.isRequired,
onLongPress: PropTypes.func,
isActive: PropTypes.bool.isRequired,
hasUnread: PropTypes.bool.isRequired,
mentions: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
render() {
@ -30,79 +32,50 @@ export default class ChannelDrawerItem extends React.Component {
theme,
mentions,
hasUnread,
isActive,
handleClose
isActive
} = this.props;
let iconColor = changeOpacity(theme.centerChannelColor, 0.7);
const isDirectMessage = channel.type === Constants.DM_CHANNEL;
let icon;
const style = getStyleSheet(theme);
let activeItem;
let activeIcon;
let unreadIcon;
let activeGroupBox;
let unreadGroupBox;
let activeGroup;
let unreadGroup;
let activeText;
let unreadText;
let activeBorder;
let icon;
let badge;
if (mentions && !isActive) {
const badgeStyle = {
position: 'absolute',
top: 12,
right: 10,
flexDirection: 'row',
backgroundColor: theme.mentionBj
};
const mentionStyle = {
color: theme.mentionColor,
fontSize: 14
};
badge = (
<Badge
style={badgeStyle}
countStyle={mentionStyle}
count={mentions}
minHeight={20}
minWidth={20}
/>
<View style={style.badgeContainer}>
<Text style={style.badge}>
{mentions}
</Text>
</View>
);
}
const itemStyle = {
alignItems: 'center',
height: 45,
paddingLeft: 20,
paddingRight: 10,
flex: 1,
flexDirection: 'row'
};
const style = {
marginLeft: 5,
opacity: 0.6,
fontSize: 15,
color: theme.sidebarText
};
const activeStyle = {
width: 5,
height: 45,
backgroundColor: theme.sidebarTextActiveBorder,
position: 'absolute'
};
if (hasUnread) {
style.fontWeight = 'bold';
style.color = theme.sidebarUnreadText;
style.opacity = 1;
iconColor = theme.centerChannelColor;
unreadIcon = style.iconUnread;
unreadText = style.textUnread;
unreadGroupBox = style.groupBoxUnread;
unreadGroup = style.groupUnread;
}
if (isActive) {
iconColor = theme.sidebarTextActiveColor;
style.color = theme.sidebarTextActiveColor;
style.opacity = 1;
itemStyle.backgroundColor = changeOpacity(theme.sidebarTextActiveColor, 0.1);
activeItem = style.itemActive;
activeIcon = style.iconActive;
activeGroupBox = style.groupBoxActive;
activeGroup = style.groupActive;
activeText = style.textActive;
activeBorder = (
<View style={activeStyle}/>
<View style={style.borderActive}/>
);
}
@ -110,102 +83,182 @@ export default class ChannelDrawerItem extends React.Component {
icon = (
<Icon
name='globe'
size={15}
color={iconColor}
style={[style.icon, unreadIcon, activeIcon]}
/>
);
} else if (channel.type === Constants.PRIVATE_CHANNEL) {
icon = (
<Icon
name='lock'
size={15}
color={iconColor}
style={[style.icon, unreadIcon, activeIcon]}
/>
);
} else if (channel.type === Constants.GM_CHANNEL) {
icon = (
<View style={style.groupContainer}>
<View style={[style.groupBox, unreadGroupBox, activeGroupBox]}>
<Text style={[style.group, unreadGroup, activeGroup]}>
{channel.display_name.split(',').length}
</Text>
</View>
</View>
);
} else {
switch (channel.status) {
case Constants.ONLINE:
icon = (
<OnlineStatus
width={13}
height={13}
color={theme.onlineIndicator}
/>
<View style={style.statusIcon}>
<OnlineStatus
width={12}
height={12}
color={theme.onlineIndicator}
/>
</View>
);
break;
case Constants.AWAY:
icon = (
<AwayStatus
width={13}
height={13}
color={theme.awayIndicator}
/>
<View style={style.statusIcon}>
<AwayStatus
width={12}
height={12}
color={theme.awayIndicator}
/>
</View>
);
break;
default:
icon = (
<OfflineStatus
width={13}
height={13}
color={changeOpacity(theme.centerChannelColor, 0.7)}
/>
<View style={style.statusIcon}>
<OfflineStatus
width={12}
height={12}
color={changeOpacity(theme.centerChannelColor, 0.4)}
/>
</View>
);
break;
}
}
let closeButton = null;
if (isDirectMessage && !badge) {
const closeStyle = {
position: 'absolute',
justifyContent: 'center',
alignItems: 'center',
opacity: 0.4,
width: 50,
height: 50,
right: 0,
flexDirection: 'row'
};
closeButton = (
<TouchableHighlight
style={closeStyle}
onPress={() => handleClose(channel)}
>
<Icon
name='times'
size={13}
color={theme.sidebarText}
/>
</TouchableHighlight>
);
}
return (
<TouchableHighlight
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.3)}
underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)}
onPress={() => this.props.onSelectChannel(channel)}
delayLongPress={1000}
onLongPress={() => {
this.props.onLongPress(channel);
}}
>
<View style={{flex: 1}}>
<View style={style.container}>
{activeBorder}
<View style={itemStyle}>
<View style={[style.item, activeItem]}>
{icon}
<Text
style={style}
style={[style.text, unreadText, activeText]}
ellipsizeMode='tail'
numberOfLines={1}
>
{channel.display_name}
</Text>
{badge}
{closeButton}
</View>
</View>
</TouchableHighlight>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
height: 44
},
borderActive: {
backgroundColor: theme.sidebarTextActiveBorder,
width: 5
},
item: {
alignItems: 'center',
height: 44,
flex: 1,
flexDirection: 'row',
paddingLeft: 16
},
itemActive: {
backgroundColor: changeOpacity(theme.sidebarTextActiveColor, 0.1),
paddingLeft: 11
},
icon: {
color: changeOpacity(theme.sidebarText, 0.4),
fontSize: 12,
paddingRight: 12
},
iconActive: {
color: theme.sidebarTextActiveColor
},
iconUnread: {
color: theme.sidebarUnreadText
},
statusIcon: {
paddingRight: 12
},
groupContainer: {
paddingRight: 12
},
groupBox: {
alignItems: 'center',
borderWidth: 1,
borderColor: changeOpacity(theme.sidebarText, 0.4),
height: 15,
justifyContent: 'center',
width: 12
},
groupBoxActive: {
borderColor: theme.sidebarTextActiveColor
},
groupBoxUnread: {
borderColor: theme.sidebarUnreadText
},
group: {
color: changeOpacity(theme.sidebarText, 0.4),
fontSize: 10,
fontWeight: '600'
},
groupActive: {
color: theme.sidebarTextActiveColor
},
groupUnread: {
color: theme.sidebarUnreadText
},
text: {
color: changeOpacity(theme.sidebarText, 0.4),
flex: 1,
fontSize: 14,
fontWeight: '600',
lineHeight: 16,
paddingRight: 40
},
textActive: {
color: theme.sidebarTextActiveColor
},
textUnread: {
color: theme.sidebarUnreadText
},
badgeContainer: {
alignItems: 'center',
backgroundColor: theme.mentionBj,
height: 15,
justifyContent: 'center',
marginRight: 16,
width: 15
},
badge: {
color: theme.mentionColor,
fontSize: 10,
fontWeight: '600'
}
});
});

View file

@ -1,8 +1,8 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import deepEqual from 'deep-equal';
import React, {PropTypes, Component} from 'react';
import {
Alert,
ListView,
@ -13,13 +13,16 @@ import {
View
} from 'react-native';
import {injectIntl, intlShape} from 'react-intl';
import {Constants} from 'mattermost-redux/constants';
import LineDivider from 'app/components/line_divider';
import ChannelDrawerItem from './channel_drawer_item';
import AwesomeIcon from 'react-native-vector-icons/FontAwesome';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import {Constants} from 'mattermost-redux/constants';
import ChannelDrawerItem from './channel_drawer_item';
import UnreadIndicator from './unread_indicator';
import deepEqual from 'deep-equal';
import Icon from 'react-native-vector-icons/FontAwesome';
class ChannelDrawerList extends Component {
static propTypes = {
@ -33,6 +36,7 @@ class ChannelDrawerList extends Component {
actions: PropTypes.shape({
closeDMChannel: PropTypes.func.isRequired,
goToCreateChannel: PropTypes.func.isRequired,
goToModalAccountSettings: React.PropTypes.func.isRequired,
leaveChannel: PropTypes.func.isRequired,
markFavorite: PropTypes.func.isRequired,
unmarkFavorite: PropTypes.func.isRequired,
@ -291,30 +295,12 @@ class ChannelDrawerList extends Component {
mentions={mentions}
onSelectChannel={this.onSelectChannel}
onLongPress={this.onShowModal}
handleClose={this.handleClose}
isActive={channel.isCurrent}
theme={this.props.theme}
/>
);
};
renderSectionAction = (action) => {
const {theme} = this.props;
return (
<TouchableHighlight
style={Styles.more}
onPress={action}
>
<Icon
name='plus-circle'
size={18}
color={theme.sidebarText}
/>
</TouchableHighlight>
);
};
createPrivateChannel = () => {
this.props.actions.goToCreateChannel(Constants.PRIVATE_CHANNEL);
};
@ -326,76 +312,38 @@ class ChannelDrawerList extends Component {
return data;
}
const {
theme
} = props;
const {theme} = this.props;
const styles = getStyleSheet(theme);
const {
favoriteChannels,
publicChannels,
privateChannels,
directChannels,
directNonTeamChannels
directAndGroupChannels
} = props.channels;
if (favoriteChannels.length) {
data.push(
<FormattedText
style={[Styles.title, {color: theme.sidebarText}]}
id='sidebar.favorite'
defaultMessage='FAVORITES'
/>,
this.renderTitle(styles, 'sidebar.favorite', 'FAVORITES', null, favoriteChannels.length > 0),
...favoriteChannels
);
}
data.push(
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
<FormattedText
style={[Styles.title, {color: theme.sidebarText}]}
id='sidebar.channels'
defaultMessage='CHANNELS'
/>
{this.renderSectionAction(this.props.actions.showMoreChannelsModal)}
</View>,
this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', this.props.actions.showMoreChannelsModal, publicChannels.length > 0),
...publicChannels
);
data.push(
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
<FormattedText
style={[Styles.title, {color: theme.sidebarText}]}
id='sidebar.pg'
defaultMessage='PRIVATE GROUPS'
/>
{this.renderSectionAction(this.createPrivateChannel)}
</View>,
this.renderTitle(styles, 'sidebar.pg', 'PRIVATE GROUPS', this.createPrivateChannel, privateChannels.length > 0),
...privateChannels
);
data.push(
<View style={{flex: 1, flexDirection: 'row', alignItems: 'center'}}>
<FormattedText
style={[Styles.title, {color: theme.sidebarText}]}
id='sidebar.direct'
defaultMessage='DIRECT MESSAGES'
/>
{this.renderSectionAction(this.props.actions.showDirectMessagesModal)}
</View>,
...directChannels
this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', this.props.actions.showDirectMessagesModal, directAndGroupChannels.length > 0),
...directAndGroupChannels
);
if (directNonTeamChannels.length) {
data.push(
<LineDivider
color={theme.sidebarTextActiveBorder}
translationId='sidebar.otherMembers'
translationText='Outside this team'
/>,
...directNonTeamChannels
);
}
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
this.findUnreadChannels(data);
@ -403,6 +351,28 @@ class ChannelDrawerList extends Component {
return data;
};
renderSectionAction = (styles, action) => {
return (
<TouchableHighlight
style={styles.actionContainer}
onPress={action}
>
<MaterialIcon
name='add'
style={styles.action}
/>
</TouchableHighlight>
);
};
renderDivider = (styles, marginLeft) => {
return (
<View
style={[styles.divider, {marginLeft}]}
/>
);
};
renderRow = (rowData) => {
if (rowData && rowData.id) {
return this.createChannelElement(rowData);
@ -410,6 +380,23 @@ class ChannelDrawerList extends Component {
return rowData;
};
renderTitle = (styles, id, defaultMessage, action, bottomDivider) => {
return (
<View>
{this.renderDivider(styles, 0)}
<View style={styles.titleContainer}>
<FormattedText
style={styles.title}
id={id}
defaultMessage={defaultMessage}
/>
{action && this.renderSectionAction(styles, action)}
</View>
{bottomDivider && this.renderDivider(styles, 16)}
</View>
);
};
setScrollContainer = (ref) => {
this.scrollContainer = ref;
};
@ -419,19 +406,30 @@ class ChannelDrawerList extends Component {
return <Text>{'Loading'}</Text>;
}
const {
theme
} = this.props;
const {theme} = this.props;
const styles = getStyleSheet(theme);
const settings = (
<TouchableHighlight
style={styles.settingsContainer}
onPress={() => this.props.actions.goToModalAccountSettings()}
>
<AwesomeIcon
name='cog'
style={styles.settings}
/>
</TouchableHighlight>
);
let above;
let below;
if (this.state.showAbove) {
above = (
<UnreadIndicator
style={{top: 55, backgroundColor: theme.mentionBj, width: (this.width - 40)}}
style={[styles.above, {width: (this.width - 40)}]}
text={(
<FormattedText
style={[Styles.indicatorText, {color: theme.mentionColor}]}
style={styles.indicatorText}
id='sidebar.unreadAbove'
defaultMessage='Unread post(s) above'
/>
@ -443,10 +441,10 @@ class ChannelDrawerList extends Component {
if (this.state.showBelow) {
below = (
<UnreadIndicator
style={{bottom: 15, backgroundColor: theme.mentionBj, width: (this.width - 40)}}
style={[styles.below, {width: (this.width - 40)}]}
text={(
<FormattedText
style={[Styles.indicatorText, {color: theme.mentionColor}]}
style={styles.indicatorText}
id='sidebar.unreadBelow'
defaultMessage='Unread post(s) below'
/>
@ -457,21 +455,24 @@ class ChannelDrawerList extends Component {
return (
<View
style={[Styles.container, {backgroundColor: theme.sidebarBg}]}
style={styles.container}
onLayout={this.onLayout}
>
<View style={[Styles.headerContainer, {backgroundColor: theme.sidebarHeaderBg}]}>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={[Styles.header, {color: theme.sidebarHeaderTextColor}]}
>
{this.props.currentTeam.display_name}
</Text>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
<Text
ellipsizeMode='tail'
numberOfLines={1}
style={styles.header}
>
{this.props.currentTeam.display_name}
</Text>
{settings}
</View>
</View>
<ListView
ref={this.setScrollContainer}
style={Styles.scrollContainer}
style={styles.scrollContainer}
dataSource={this.state.dataSource}
renderRow={this.renderRow}
onChangeVisibleRows={this.updateUnreadIndicators}
@ -483,59 +484,99 @@ class ChannelDrawerList extends Component {
}
}
const Styles = StyleSheet.create({
container: {
flex: 1
},
scrollContainer: {
flex: 1
},
headerContainer: {
flexDirection: 'column',
...Platform.select({
ios: {
height: 64,
justifyContent: 'flex-end'
},
android: {
height: 56,
justifyContent: 'center',
paddingTop: 10
}
}),
width: 300,
paddingLeft: 10,
paddingBottom: 12
},
header: {
fontSize: 18,
fontWeight: 'bold'
},
title: {
paddingTop: 10,
paddingRight: 10,
paddingLeft: 10,
paddingBottom: 5,
fontSize: 15,
opacity: 0.6
},
indicatorText: {
paddingVertical: 2,
paddingHorizontal: 4,
backgroundColor: 'transparent',
fontSize: 14,
textAlign: 'center',
textAlignVertical: 'center'
},
more: {
position: 'absolute',
opacity: 0.6,
right: 0,
width: 50,
height: 30,
alignItems: 'center',
justifyContent: 'center'
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return StyleSheet.create({
container: {
backgroundColor: theme.sidebarBg,
flex: 1
},
statusBar: {
backgroundColor: theme.sidebarHeaderBg,
...Platform.select({
ios: {
paddingTop: 20
}
})
},
scrollContainer: {
flex: 1,
marginBottom: 10
},
headerContainer: {
alignItems: 'center',
backgroundColor: theme.sidebarHeaderBg,
flexDirection: 'row',
height: 44,
paddingLeft: 16
},
header: {
color: theme.sidebarHeaderTextColor,
flex: 1,
fontSize: 14,
fontWeight: 'normal',
lineHeight: 16
},
settingsContainer: {
alignItems: 'center',
height: 44,
justifyContent: 'center',
width: 50
},
settings: {
color: theme.sidebarText,
fontSize: 18,
fontWeight: '300'
},
titleContainer: {
alignItems: 'center',
flex: 1,
flexDirection: 'row',
height: 48,
marginLeft: 16
},
title: {
flex: 1,
color: theme.sidebarText,
opacity: 1,
fontSize: 15,
fontWeight: '500',
letterSpacing: 0.8,
lineHeight: 18
},
divider: {
backgroundColor: changeOpacity(theme.centerChannelColor, 0.1),
height: 1
},
actionContainer: {
alignItems: 'center',
height: 48,
justifyContent: 'center',
width: 50
},
action: {
color: theme.sidebarText,
fontSize: 16,
fontWeight: '500',
lineHeight: 18
},
above: {
backgroundColor: theme.mentionBj,
top: 55
},
below: {
backgroundColor: theme.mentionBj,
bottom: 15
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
});
});
export default injectIntl(ChannelDrawerList);

View file

@ -5,8 +5,9 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {
requestCloseModal,
goToModalAccountSettings,
goToCreateChannel,
requestCloseModal,
showMoreChannelsModal,
showDirectMessagesModal,
showOptionsModal
@ -32,6 +33,7 @@ function mapDispatchToProps(dispatch) {
actions: bindActionCreators({
closeDMChannel,
goToCreateChannel,
goToModalAccountSettings,
leaveChannel,
markFavorite,
unmarkFavorite,

View file

@ -1,83 +0,0 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React from 'react';
import {StyleSheet, View} from 'react-native';
import FormattedText from 'app/components/formatted_text';
const Styles = StyleSheet.create({
dividerLine: {
flex: 1,
height: 1,
backgroundColor: '#b3b3b3'
},
dividerContainer: {
height: 20,
marginLeft: 15,
marginRight: 15
},
dividerText: {
flex: 1,
textAlign: 'center'
}
});
export default class LineDivider extends React.Component {
static propTypes = {
color: React.PropTypes.string.isRequired,
translationId: React.PropTypes.string,
translationText: React.PropTypes.string,
side: React.PropTypes.oneOf(['left', 'right', 'center'])
};
static defaultProps = {
side: 'right'
};
renderLine() {
return (
<View style={[Styles.dividerLine, {backgroundColor: this.props.color}]}/>
);
}
render() {
let renderText;
if (this.props.translationId && this.props.translationText) {
renderText = (
<View style={Styles.dividerContainer}>
<FormattedText
style={[Styles.dividerText, {color: this.props.color}]}
id={this.props.translationId}
defaultMessage={this.props.translationText}
/>
</View>
);
}
let content = (
<View style={{flexDirection: 'row', alignItems: 'center'}}>
{this.renderLine()}
{renderText}
</View>
);
if (this.props.side === 'left') {
content = (
<View style={{flexDirection: 'row', alignItems: 'center'}}>
{renderText}
{this.renderLine()}
</View>
);
} else if (this.props.side === 'center') {
content = (
<View style={{flexDirection: 'row', alignItems: 'center'}}>
{this.renderLine()}
{renderText}
{this.renderLine()}
</View>
);
}
return content;
}
}

View file

@ -18,6 +18,8 @@ import {RouteTransitions} from 'app/navigation/routes';
import {getTheme} from 'app/selectors/preferences';
import ErrorList from 'app/components/error_list';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
import NavigationModal from './navigation_modal';
const navigationPanResponder = NavigationExperimental.Card.CardStackPanResponder;
@ -39,7 +41,7 @@ class Router extends React.Component {
state = {
deviceHeight,
deviceWidth
}
};
emitHeaderEvent = (key) => (event) => {
const subscription = this.headerEventSubscriptions[`${key}-${event}`];
@ -201,6 +203,19 @@ class Router extends React.Component {
}
}
handleDrawerTween = (ratio) => {
const opacity = (ratio / 2);
EventEmitter.emit('drawer_opacity', opacity);
return {
mainOverlay: {
backgroundColor: '#000',
opacity
}
};
};
render = () => {
const {
index,
@ -237,14 +252,15 @@ class Router extends React.Component {
disabled={modalVisible}
content={leftDrawerContent}
tapToClose={true}
openDrawerOffset={0.2}
openDrawerOffset={42}
onRequestClose={this.props.actions.closeDrawers}
panOpenMask={0.4}
panCloseMask={0.2}
panOpenMask={0.3}
panCloseMask={42}
panThreshold={0.2}
acceptPan={navigationProps.allowMenuSwipe}
negotiatePan={true}
useInteractionManager={true}
tweenHandler={this.handleDrawerTween}
>
<Drawer
open={rightDrawerOpen}
@ -253,14 +269,15 @@ class Router extends React.Component {
disabled={modalVisible}
content={rightDrawerContent}
tapToClose={true}
openDrawerOffset={0.2}
openDrawerOffset={50}
onRequestClose={this.props.actions.closeDrawers}
panOpenMask={0.4}
panCloseMask={0.2}
panOpenMask={0.3}
panCloseMask={50}
panThreshold={0.2}
acceptPan={navigationProps.allowMenuSwipe}
negotiatePan={true}
useInteractionManager={true}
tweenHandler={this.handleDrawerTween}
>
<NavigationExperimental.Transitioner
style={{flex: 1}}

View file

@ -1,88 +1,112 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import React, {PropTypes, PureComponent} from 'react';
import {connect} from 'react-redux';
import {
TouchableOpacity,
View
} from 'react-native';
import Icon from 'react-native-vector-icons/FontAwesome';
import Badge from 'app/components/badge';
import {getUnreads} from 'mattermost-redux/selectors/entities/channels';
import Badge from 'app/components/badge';
import {getTheme} from 'app/selectors/preferences';
function ChannelDrawerButton(props) {
let badge;
let badgeCount = props.mentionCount;
import {getUnreads} from 'mattermost-redux/selectors/entities/channels';
import EventEmitter from 'mattermost-redux/utils/event_emitter';
if (!badgeCount && props.messageCount) {
badgeCount = -1;
class ChannelDrawerButton extends PureComponent {
static propTypes = {
emitter: PropTypes.func.isRequired,
theme: PropTypes.object,
messageCount: PropTypes.number,
mentionCount: PropTypes.number
};
static defaultProps = {
currentChannel: {},
theme: {},
messageCount: 0,
mentionCount: 0
};
constructor(props) {
super(props);
this.state = {
opacity: 1
};
}
if (badgeCount !== 0) {
const badgeStyle = {
backgroundColor: 'rgb(214, 73, 70)',
borderRadius: 10,
flexDirection: 'row',
height: 20,
left: 5,
padding: 3,
position: 'absolute',
right: 0,
top: 5,
width: 20
};
componentDidMount() {
EventEmitter.on('drawer_opacity', this.setOpacity);
}
const mentionStyle = {
color: '#fff',
fontSize: 10
};
componentWillUnmount() {
EventEmitter.off('drawer_opacity', this.setOpacity);
}
badge = (
<Badge
style={badgeStyle}
countStyle={mentionStyle}
count={badgeCount}
minHeight={5}
minWidth={5}
onPress={() => props.emitter('open_channel_drawer')}
/>
setOpacity = (value) => {
this.setState({opacity: value > 0 ? 0.1 : 1});
};
render() {
let badge;
let badgeCount = this.props.mentionCount;
if (!badgeCount && this.props.messageCount) {
badgeCount = -1;
}
if (badgeCount !== 0) {
const badgeStyle = {
backgroundColor: 'rgb(214, 73, 70)',
borderRadius: 10,
flexDirection: 'row',
height: 20,
left: 5,
padding: 3,
position: 'absolute',
right: 0,
top: 5,
width: 20
};
const mentionStyle = {
color: '#fff',
fontSize: 10
};
badge = (
<Badge
style={badgeStyle}
countStyle={mentionStyle}
count={badgeCount}
minHeight={5}
minWidth={5}
onPress={() => this.props.emitter('open_channel_drawer')}
/>
);
}
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1, opacity: this.state.opacity}}>
<TouchableOpacity
onPress={() => this.props.emitter('open_channel_drawer')}
style={{height: 25, width: 25, marginLeft: 10, marginRight: 10}}
>
<Icon
name='bars'
size={25}
color={this.props.theme.sidebarHeaderTextColor}
/>
</TouchableOpacity>
{badge}
</View>
);
}
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
<TouchableOpacity
onPress={() => props.emitter('open_channel_drawer')}
style={{height: 25, width: 25, marginLeft: 10, marginRight: 10}}
>
<Icon
name='bars'
size={25}
color={props.theme.sidebarHeaderTextColor}
/>
</TouchableOpacity>
{badge}
</View>
);
}
ChannelDrawerButton.propTypes = {
emitter: PropTypes.func.isRequired,
theme: PropTypes.object,
messageCount: PropTypes.number,
mentionCount: PropTypes.number
};
ChannelDrawerButton.defaultProps = {
currentChannel: {},
theme: {},
messageCount: 0,
mentionCount: 0
};
function mapStateToProps(state) {
return {
theme: getTheme(state),

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PropTypes} from 'react';
import React, {PropTypes, PureComponent} from 'react';
import {connect} from 'react-redux';
import {
TouchableOpacity,
@ -11,31 +11,55 @@ import Icon from 'react-native-vector-icons/FontAwesome';
import {getTheme} from 'app/selectors/preferences';
function ChannelMenuButton(props) {
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1}}>
<TouchableOpacity onPress={() => props.emitter('open_right_menu')}>
<Icon
name='ellipsis-v'
size={25}
color={props.theme.sidebarHeaderTextColor}
style={{paddingHorizontal: 20}}
/>
</TouchableOpacity>
</View>
);
import EventEmitter from 'mattermost-redux/utils/event_emitter';
class ChannelMenuButton extends PureComponent {
static propTypes = {
emitter: PropTypes.func.isRequired,
theme: PropTypes.object
};
static defaultProps = {
currentChannel: {},
theme: {}
};
constructor(props) {
super(props);
this.state = {
opacity: 1
};
}
componentDidMount() {
EventEmitter.on('drawer_opacity', this.setOpacity);
}
componentWillUnmount() {
EventEmitter.off('drawer_opacity', this.setOpacity);
}
setOpacity = (value) => {
this.setState({opacity: value > 0 ? 0.1 : 1});
};
render() {
return (
<View style={{flexDirection: 'column', justifyContent: 'center', alignItems: 'center', flex: 1, opacity: this.state.opacity}}>
<TouchableOpacity onPress={() => this.props.emitter('open_right_menu')}>
<Icon
name='ellipsis-v'
size={25}
color={this.props.theme.sidebarHeaderTextColor}
style={{paddingHorizontal: 20}}
/>
</TouchableOpacity>
</View>
);
}
}
ChannelMenuButton.propTypes = {
emitter: PropTypes.func.isRequired,
theme: PropTypes.object
};
ChannelMenuButton.defaultProps = {
currentChannel: {},
theme: {}
};
function mapStateToProps(state) {
return {
theme: getTheme(state)

View file

@ -22,7 +22,6 @@ export default class RightMenuDrawer extends React.Component {
currentUserId: React.PropTypes.string.isRequired,
currentTeamId: React.PropTypes.string.isRequired,
actions: React.PropTypes.shape({
goToModalAccountSettings: React.PropTypes.func.isRequired,
goToModalSelectTeam: React.PropTypes.func.isRequired,
clearErrors: React.PropTypes.func.isRequired,
logout: React.PropTypes.func.isRequired
@ -60,18 +59,6 @@ export default class RightMenuDrawer extends React.Component {
return (
<ScrollView style={Styles.container}>
<Divider style={Styles.divider}/>
<RightMenuDrawerItem onPress={this.props.actions.goToModalAccountSettings}>
<Icon
style={Styles.icon}
name='cog'
/>
<FormattedText
style={Styles.itemText}
id='sidebar_right_menu.accountSettings'
defaultMessage='Account Settings'
/>
</RightMenuDrawerItem>
<Divider style={Styles.divider}/>
<RightMenuDrawerItem onPress={this.props.actions.goToModalSelectTeam}>
<Icon

View file

@ -4,7 +4,7 @@
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {goToModalAccountSettings, goBack, goToModalSelectTeam} from 'app/actions/navigation';
import {goBack, goToModalSelectTeam} from 'app/actions/navigation';
import {clearErrors} from 'mattermost-redux/actions/errors';
import {logout} from 'mattermost-redux/actions/users';
@ -25,7 +25,6 @@ function mapStateToProps(state, ownProps) {
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
goToModalAccountSettings,
goBack,
goToModalSelectTeam,
clearErrors,