Improve performance of the channel drawer (#1023)

* Improve performance of the channel drawer

* Feedback review

* Fix makefile

* Feedback review 3

* Fix areStatesEqual

* Fix Android SVG
This commit is contained in:
enahum 2017-10-17 14:06:28 -03:00 committed by Jarred Witt
parent 2378ad627b
commit cc966702fd
32 changed files with 616 additions and 556 deletions

View file

@ -121,6 +121,7 @@ post-install:
@sed -i'' -e 's|"./lib/locales": false|"./lib/locales": "./lib/locales"|g' node_modules/intl-relativeformat/package.json
@sed -i'' -e 's|"./locale-data/complete.js": false|"./locale-data/complete.js": "./locale-data/complete.js"|g' node_modules/intl/package.json
@sed -i'' -e 's|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);|auto("auto", Configuration.ORIENTATION_UNDEFINED, ActivityInfo.SCREEN_ORIENTATION_FULL_USER);|g' node_modules/react-native-navigation/android/app/src/main/java/com/reactnativenavigation/params/Orientation.java
@cd ./node_modules/react-native-svg/ios && rm -rf PerformanceBezier && git clone https://github.com/adamwulf/PerformanceBezier.git
start-packager:
@if [ $(shell ps -e | grep -i "cli.js start" | grep -civ grep) -eq 0 ]; then \

View file

@ -6,6 +6,7 @@ import android.content.Context;
import android.os.Bundle;
import com.facebook.react.ReactApplication;
import com.horcrux.svg.SvgPackage;
import com.inprogress.reactnativeyoutube.ReactNativeYouTube;
import io.sentry.RNSentryPackage;
import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage;
@ -23,7 +24,6 @@ import com.gnet.bottomsheet.RNBottomSheetPackage;
import com.learnium.RNDeviceInfo.RNDeviceInfo;
import com.psykar.cookiemanager.CookieManagerPackage;
import com.oblador.vectoricons.VectorIconsPackage;
import com.horcrux.svg.SvgPackage;
import com.BV.LinearGradient.LinearGradientPackage;
import com.github.yamill.orientation.OrientationPackage;
import com.reactnativenavigation.NavigationApplication;

View file

@ -27,9 +27,9 @@ include ':reactnativenotifications'
project(':reactnativenotifications').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-notifications/android')
include ':app'
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')
include ':react-native-orientation'
project(':react-native-orientation').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-orientation/android')
include ':react-native-linear-gradient'
project(':react-native-linear-gradient').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-linear-gradient/android')
include ':react-native-svg'
project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android')

View file

@ -12,23 +12,23 @@ import {NavigationTypes} from 'app/constants';
import {setChannelDisplayName} from './channel';
export function handleTeamChange(team, selectChannel = true) {
export function handleTeamChange(teamId, selectChannel = true) {
return async (dispatch, getState) => {
const state = getState();
const {currentTeamId} = state.entities.teams;
if (currentTeamId === team.id) {
if (currentTeamId === teamId) {
return;
}
const actions = [
setChannelDisplayName(''),
{type: TeamTypes.SELECT_TEAM, data: team.id}
{type: TeamTypes.SELECT_TEAM, data: teamId}
];
if (selectChannel) {
actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''});
const lastChannelId = state.views.team.lastChannelForTeam[team.id] || '';
const lastChannelId = state.views.team.lastChannelForTeam[teamId] || '';
const currentChannelId = getCurrentChannelId(state);
viewChannel(lastChannelId, currentChannelId)(dispatch, getState);
markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState);
@ -45,7 +45,7 @@ export function selectFirstAvailableTeam() {
const firstTeam = Object.values(teams).sort((a, b) => a.display_name.localeCompare(b.display_name))[0];
if (firstTeam) {
handleTeamChange(firstTeam)(dispatch, getState);
handleTeamChange(firstTeam.id)(dispatch, getState);
} else {
EventEmitter.emit(NavigationTypes.NAVIGATION_NO_TEAMS);
}

View file

@ -118,6 +118,10 @@ export default class Badge extends PureComponent {
};
render() {
if (!this.props.count) {
return null;
}
return (
<TouchableWithoutFeedback
{...this.panResponder.panHandlers}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {
BackHandler,
@ -24,7 +24,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter';
const DRAWER_INITIAL_OFFSET = 40;
const DRAWER_LANDSCAPE_OFFSET = 150;
export default class ChannelDrawer extends PureComponent {
export default class ChannelDrawer extends Component {
static propTypes = {
actions: PropTypes.shape({
getTeams: PropTypes.func.isRequired,
@ -37,7 +37,6 @@ export default class ChannelDrawer extends PureComponent {
}).isRequired,
blurPostTextBox: PropTypes.func.isRequired,
children: PropTypes.node,
currentChannelId: PropTypes.string.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUserId: PropTypes.string.isRequired,
isLandscape: PropTypes.bool.isRequired,
@ -76,11 +75,11 @@ export default class ChannelDrawer extends PureComponent {
}
componentWillReceiveProps(nextProps) {
const {isLandscape, isTablet} = this.props;
if (nextProps.isLandscape !== isLandscape || nextProps.isTablet || isTablet) {
const {isLandscape} = this.props;
if (nextProps.isLandscape !== isLandscape) {
if (this.state.openDrawerOffset !== 0) {
let openDrawerOffset = DRAWER_INITIAL_OFFSET;
if (nextProps.isLandscape || nextProps.isTablet) {
if (nextProps.isLandscape || this.props.isTablet) {
openDrawerOffset = DRAWER_LANDSCAPE_OFFSET;
}
this.setState({openDrawerOffset});
@ -88,6 +87,19 @@ export default class ChannelDrawer extends PureComponent {
}
}
shouldComponentUpdate(nextProps, nextState) {
const {currentTeamId, isLandscape, teamsCount} = this.props;
const {openDrawerOffset} = this.state;
if (nextState.openDrawerOffset !== openDrawerOffset) {
return true;
}
return nextProps.currentTeamId !== currentTeamId ||
nextProps.isLandscape !== isLandscape ||
nextProps.teamsCount !== teamsCount;
}
componentWillUnmount() {
EventEmitter.off('open_channel_drawer', this.openChannelDrawer);
EventEmitter.off('close_channel_drawer', this.closeChannelDrawer);
@ -180,10 +192,9 @@ export default class ChannelDrawer extends PureComponent {
}
};
selectChannel = (channel) => {
selectChannel = (channel, currentChannelId) => {
const {
actions,
currentChannelId
actions
} = this.props;
const {
@ -194,7 +205,7 @@ export default class ChannelDrawer extends PureComponent {
viewChannel
} = actions;
setChannelLoading();
setChannelLoading(channel.id !== currentChannelId);
setChannelDisplayName(channel.display_name);
this.closeChannelDrawer();
@ -211,7 +222,7 @@ export default class ChannelDrawer extends PureComponent {
});
};
joinChannel = async (channel) => {
joinChannel = async (channel, currentChannelId) => {
const {
actions,
currentTeamId,
@ -253,7 +264,7 @@ export default class ChannelDrawer extends PureComponent {
return;
}
this.selectChannel(result.data);
this.selectChannel(result.data, currentChannelId);
};
onPageSelected = (index) => {
@ -284,13 +295,13 @@ export default class ChannelDrawer extends PureComponent {
};
showTeams = () => {
if (this.swiperIndex === 1 && this.props.teamsCount > 1) {
if (this.drawerSwiper && this.swiperIndex === 1 && this.props.teamsCount > 1) {
this.drawerSwiper.getWrappedInstance().showTeamsPage();
}
};
resetDrawer = () => {
if (this.swiperIndex !== 1) {
if (this.drawerSwiper && this.swiperIndex !== 1) {
this.drawerSwiper.getWrappedInstance().resetPage();
}
};
@ -344,6 +355,7 @@ export default class ChannelDrawer extends PureComponent {
onShowTeams={this.showTeams}
onSearchStart={this.onSearchStart}
onSearchEnds={this.onSearchEnds}
theme={theme}
/>
</View>
);
@ -354,7 +366,6 @@ export default class ChannelDrawer extends PureComponent {
onPageSelected={this.onPageSelected}
openDrawerOffset={openDrawerOffset}
showTeams={showTeams}
theme={theme}
>
{lists}
</DrawerSwiper>

View file

@ -10,45 +10,62 @@ import {
} from 'react-native';
import Badge from 'app/components/badge';
import ChanneIcon from 'app/components/channel_icon';
import {preventDoubleTap} from 'app/utils/tap';
import ChannelIcon from 'app/components/channel_icon';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class ChannelItem extends PureComponent {
static propTypes = {
channel: PropTypes.object.isRequired,
onSelectChannel: PropTypes.func.isRequired,
isActive: PropTypes.bool.isRequired,
hasUnread: PropTypes.bool.isRequired,
channelId: PropTypes.string.isRequired,
currentChannelId: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
isUnread: PropTypes.bool,
mentions: PropTypes.number.isRequired,
onSelectChannel: PropTypes.func.isRequired,
status: PropTypes.string,
type: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
onPress = () => {
const {channel, onSelectChannel} = this.props;
onPress = wrapWithPreventDoubleTap(() => {
const {channelId, currentChannelId, displayName, onSelectChannel} = this.props;
requestAnimationFrame(() => {
preventDoubleTap(onSelectChannel, this, channel);
onSelectChannel({id: channelId, display_name: displayName}, currentChannelId);
});
};
});
render() {
const {
channel,
theme,
channelId,
currentChannelId,
displayName,
isUnread,
mentions,
hasUnread,
isActive
status,
theme,
type
} = this.props;
const style = getStyleSheet(theme);
let activeItem;
let activeText;
let unreadText;
const isActive = channelId === currentChannelId;
let extraItemStyle;
let extraTextStyle;
let extraBorder;
if (isActive) {
extraItemStyle = style.itemActive;
extraTextStyle = style.textActive;
extraBorder = (
<View style={style.borderActive}/>
);
} else if (isUnread) {
extraTextStyle = style.textUnread;
}
let activeBorder;
let badge;
if (mentions && !isActive) {
if (mentions) {
badge = (
<Badge
style={style.badge}
@ -61,28 +78,16 @@ export default class ChannelItem extends PureComponent {
);
}
if (hasUnread) {
unreadText = style.textUnread;
}
if (isActive) {
activeItem = style.itemActive;
activeText = style.textActive;
activeBorder = (
<View style={style.borderActive}/>
);
}
const icon = (
<ChanneIcon
<ChannelIcon
isActive={isActive}
hasUnread={hasUnread}
membersCount={channel.display_name.split(',').length}
channelId={channelId}
isUnread={isUnread}
membersCount={displayName.split(',').length}
size={16}
status={channel.status}
status={status}
theme={theme}
type={channel.type}
type={type}
/>
);
@ -92,15 +97,15 @@ export default class ChannelItem extends PureComponent {
onPress={this.onPress}
>
<View style={style.container}>
{activeBorder}
<View style={[style.item, activeItem]}>
{extraBorder}
<View style={[style.item, extraItemStyle]}>
{icon}
<Text
style={[style.text, unreadText, activeText]}
style={[style.text, extraTextStyle]}
ellipsizeMode='tail'
numberOfLines={1}
>
{channel.display_name}
{displayName}
</Text>
{badge}
</View>

View file

@ -0,0 +1,32 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {connect} from 'react-redux';
import {getCurrentChannelId, makeGetChannel, getMyChannelMember} from 'mattermost-redux/selectors/entities/channels';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelItem from './channel_item';
function makeMapStateToProps() {
const getChannel = makeGetChannel();
return (state, ownProps) => {
const channel = getChannel(state, {id: ownProps.channelId});
let member;
if (ownProps.isUnread) {
member = getMyChannelMember(state, ownProps.channelId);
}
return {
currentChannelId: getCurrentChannelId(state),
displayName: channel.display_name,
mentions: member ? member.mention_count : 0,
status: channel.status,
theme: getTheme(state),
type: channel.type
};
};
}
export default connect(makeMapStateToProps)(ChannelItem);

View file

@ -18,7 +18,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
import FilteredList from './filtered_list';
import List from './list';
import SwitchTeams from './switch_teams';
import SwitchTeamsButton from './switch_teams_button';
class ChannelsList extends React.PureComponent {
static propTypes = {
@ -45,14 +45,16 @@ class ChannelsList extends React.PureComponent {
});
}
onSelectChannel = (channel) => {
onSelectChannel = (channel, currentChannelId) => {
if (channel.fake) {
this.props.onJoinChannel(channel);
this.props.onJoinChannel(channel, currentChannelId);
} else {
this.props.onSelectChannel(channel);
this.props.onSelectChannel(channel, currentChannelId);
}
this.refs.search_bar.cancel();
if (this.refs.search_bar) {
this.refs.search_bar.cancel();
}
};
openSettingsModal = wrapWithPreventDoubleTap(() => {
@ -171,9 +173,9 @@ class ChannelsList extends React.PureComponent {
>
<View style={styles.statusBar}>
<View style={styles.headerContainer}>
<SwitchTeams
<SwitchTeamsButton
searching={searching}
showTeams={onShowTeams}
onShowTeams={onShowTeams}
/>
{title}
{settings}
@ -288,15 +290,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => {
above: {
backgroundColor: theme.mentionBj,
top: 9
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
};
});

View file

@ -7,9 +7,8 @@ import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import ChannelsList from './channels_list';
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
return {
...ownProps,
theme: getTheme(state)
};
}

View file

@ -4,7 +4,13 @@
import {connect} from 'react-redux';
import {General} from 'mattermost-redux/constants';
import {getChannelsWithUnreadSection, getCurrentChannel, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
import {
getSortedUnreadChannelIds,
getSortedFavoriteChannelIds,
getSortedPublicChannelIds,
getSortedPrivateChannelIds,
getSortedDirectChannelIds
} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {showCreateOption} from 'mattermost-redux/utils/channel_utils';
@ -12,18 +18,33 @@ import {isAdmin, isSystemAdmin} from 'mattermost-redux/utils/user_utils';
import List from './list';
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
const {config, license} = state.entities.general;
const roles = getCurrentUserId(state) ? getCurrentUserRoles(state) : '';
const unreadChannelIds = getSortedUnreadChannelIds(state);
const favoriteChannelIds = getSortedFavoriteChannelIds(state);
const publicChannelIds = getSortedPublicChannelIds(state);
const privateChannelIds = getSortedPrivateChannelIds(state);
const directChannelIds = getSortedDirectChannelIds(state);
return {
canCreatePrivateChannels: showCreateOption(config, license, General.PRIVATE_CHANNEL, isAdmin(roles), isSystemAdmin(roles)),
channelMembers: getMyChannelMemberships(state),
channels: getChannelsWithUnreadSection(state),
currentChannel: getCurrentChannel(state),
theme: getTheme(state),
...ownProps
unreadChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
directChannelIds,
theme: getTheme(state)
};
}
export default connect(mapStateToProps, null)(List);
function areStatesEqual(next, prev) {
const equalRoles = getCurrentUserRoles(prev) === getCurrentUserRoles(next);
const equalChannels = next.entities.channels === prev.entities.channels;
const equalConfig = next.entities.general.config === prev.entities.general.config;
const equalUsers = next.entities.users.profiles === prev.entities.users.profiles;
return equalChannels && equalConfig && equalRoles && equalUsers;
}
export default connect(mapStateToProps, null, null, {pure: true, areStatesEqual})(List);

View file

@ -1,11 +1,10 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import deepEqual from 'deep-equal';
import React, {Component} from 'react';
import React, {PureComponent} from 'react';
import PropTypes from 'prop-types';
import {
FlatList,
SectionList,
Text,
TouchableHighlight,
View
@ -13,38 +12,36 @@ import {
import {injectIntl, intlShape} from 'react-intl';
import MaterialIcon from 'react-native-vector-icons/MaterialIcons';
import FormattedText from 'app/components/formatted_text';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
import {General} from 'mattermost-redux/constants';
import {debounce} from 'mattermost-redux/actions/helpers';
import ChannelItem from 'app/components/channel_drawer/channels_list/channel_item';
import UnreadIndicator from 'app/components/channel_drawer/channels_list/unread_indicator';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity} from 'app/utils/theme';
class List extends Component {
class List extends PureComponent {
static propTypes = {
canCreatePrivateChannels: PropTypes.bool.isRequired,
channels: PropTypes.object.isRequired,
channelMembers: PropTypes.object,
currentChannel: PropTypes.object,
directChannelIds: PropTypes.array.isRequired,
favoriteChannelIds: PropTypes.array.isRequired,
intl: intlShape.isRequired,
navigator: PropTypes.object,
onSelectChannel: PropTypes.func.isRequired,
publicChannelIds: PropTypes.array.isRequired,
privateChannelIds: PropTypes.array.isRequired,
styles: PropTypes.object.isRequired,
theme: PropTypes.object.isRequired
};
static defaultProps = {
currentChannel: {}
theme: PropTypes.object.isRequired,
unreadChannelIds: PropTypes.array.isRequired
};
constructor(props) {
super(props);
this.firstUnreadChannel = null;
this.state = {
dataSource: this.buildData(props),
showAbove: false
sections: this.buildSections(props),
showIndicator: false,
width: 0
};
MaterialIcon.getImageSource('close', 20, this.props.theme.sidebarHeaderTextColor).then((source) => {
@ -52,106 +49,96 @@ class List extends Component {
});
}
shouldComponentUpdate(nextProps, nextState) {
return !deepEqual(this.props, nextProps, {strict: true}) || !deepEqual(this.state, nextState, {strict: true});
}
componentWillReceiveProps(nextProps) {
this.setState({
dataSource: this.buildData(nextProps)
}, () => {
if (this.refs.list) {
this.refs.list.recordInteraction();
this.updateUnreadIndicators({
viewableItems: Array.from(this.refs.list._listRef._viewabilityHelper._viewableItems.values()) //eslint-disable-line
});
}
});
const {
canCreatePrivateChannels,
directChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds
} = this.props;
if (nextProps.canCreatePrivateChannels !== canCreatePrivateChannels ||
nextProps.directChannelIds !== directChannelIds || nextProps.favoriteChannelIds !== favoriteChannelIds ||
nextProps.publicChannelIds !== publicChannelIds || nextProps.privateChannelIds !== privateChannelIds ||
nextProps.unreadChannelIds !== unreadChannelIds) {
this.setState({sections: this.buildSections(nextProps)});
}
}
updateUnreadIndicators = ({viewableItems}) => {
let showAbove = false;
const visibleIndexes = viewableItems.map((v) => v.index);
if (visibleIndexes.length) {
const {dataSource} = this.state;
const firstVisible = parseInt(visibleIndexes[0], 10);
if (this.firstUnreadChannel) {
const index = dataSource.findIndex((item) => {
return item.display_name === this.firstUnreadChannel;
});
showAbove = index < firstVisible;
}
this.setState({
showAbove
componentDidUpdate(prevProps, prevState) {
if (prevState.sections !== this.state.sections && this.refs.list) {
this.refs.list.recordInteraction();
this.updateUnreadIndicators({
viewableItems: Array.from(this.refs.list._wrapperListRef._listRef._viewabilityHelper._viewableItems.values()) //eslint-disable-line
});
}
};
}
onSelectChannel = (channel) => {
this.props.onSelectChannel(channel);
};
buildSections = (props) => {
const {
canCreatePrivateChannels,
directChannelIds,
favoriteChannelIds,
publicChannelIds,
privateChannelIds,
unreadChannelIds
} = props;
const sections = [];
onLayout = (event) => {
const {width} = event.nativeEvent.layout;
this.width = width;
};
getUnreadMessages = (channel) => {
const member = this.props.channelMembers[channel.id];
let mentions = 0;
let unreadCount = 0;
if (member && channel) {
mentions = member.mention_count;
unreadCount = channel.total_msg_count - member.msg_count;
if (member.notify_props && member.notify_props.mark_unread === General.MENTION) {
unreadCount = 0;
}
if (unreadChannelIds.length) {
sections.push({
id: 'mobile.channel_list.unreads',
defaultMessage: 'UNREADS',
data: unreadChannelIds,
renderItem: this.renderUnreadItem,
topSeparator: false,
bottomSeparator: true
});
}
return {
mentions,
unreadCount
};
};
if (favoriteChannelIds.length) {
sections.push({
id: 'sidebar.favorite',
defaultMessage: 'FAVORITES',
data: favoriteChannelIds,
topSeparator: unreadChannelIds.length > 0,
bottomSeparator: true
});
}
findUnreadChannels = (data) => {
data.forEach((c) => {
if (c.id) {
const {mentions, unreadCount} = this.getUnreadMessages(c);
const unread = (mentions + unreadCount) > 0;
if (unread && c.id !== this.props.currentChannel.id) {
if (!this.firstUnreadChannel) {
this.firstUnreadChannel = c.display_name;
}
}
}
sections.push({
action: this.goToMoreChannels,
id: 'sidebar.channels',
defaultMessage: 'PUBLIC CHANNELS',
data: publicChannelIds,
topSeparator: favoriteChannelIds.length > 0 || unreadChannelIds.length > 0,
bottomSeparator: publicChannelIds.length > 0
});
sections.push({
action: canCreatePrivateChannels ? this.goToCreatePrivateChannel : null,
id: 'sidebar.pg',
defaultMessage: 'PRIVATE CHANNELS',
data: privateChannelIds,
topSeparator: true,
bottomSeparator: privateChannelIds.length > 0
});
sections.push({
action: this.goToDirectMessages,
id: 'sidebar.direct',
defaultMessage: 'DIRECT MESSAGES',
data: directChannelIds,
topSeparator: true,
bottomSeparator: false
});
return sections;
};
createChannelElement = (channel) => {
const {mentions, unreadCount} = this.getUnreadMessages(channel);
const msgCount = mentions + unreadCount;
const unread = msgCount > 0;
return (
<ChannelItem
ref={channel.id}
channel={channel}
hasUnread={unread}
mentions={mentions}
onSelectChannel={this.onSelectChannel}
isActive={channel.isCurrent}
theme={this.props.theme}
/>
);
};
createPrivateChannel = wrapWithPreventDoubleTap(() => {
goToCreatePrivateChannel = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@ -173,75 +160,7 @@ class List extends Component {
});
});
buildChannels = (props) => {
const {canCreatePrivateChannels, styles} = props;
const {
unreadChannels,
favoriteChannels,
publicChannels,
privateChannels,
directAndGroupChannels
} = props.channels;
const data = [];
if (unreadChannels.length) {
data.push(
this.renderTitle(styles, 'mobile.channel_list.unreads', 'UNREADS', null, false, true),
...unreadChannels
);
}
if (favoriteChannels.length) {
data.push(
this.renderTitle(styles, 'sidebar.favorite', 'FAVORITES', null, unreadChannels.length > 0, true),
...favoriteChannels
);
}
data.push(
this.renderTitle(styles, 'sidebar.channels', 'CHANNELS', this.showMoreChannelsModal, favoriteChannels.length > 0, publicChannels.length > 0),
...publicChannels
);
let createPrivateChannel;
if (canCreatePrivateChannels) {
createPrivateChannel = this.createPrivateChannel;
}
data.push(
this.renderTitle(styles, 'sidebar.pg', 'PRIVATE CHANNELS', createPrivateChannel, true, privateChannels.length > 0),
...privateChannels
);
data.push(
this.renderTitle(styles, 'sidebar.direct', 'DIRECT MESSAGES', this.showDirectMessagesModal, true, directAndGroupChannels.length > 0),
...directAndGroupChannels
);
return data;
};
buildData = (props) => {
if (!props.currentChannel) {
return null;
}
const data = this.buildChannels(props);
this.firstUnreadChannel = null;
this.findUnreadChannels(data);
return data;
};
scrollToTop = () => {
this.refs.list.scrollToOffset({
x: 0,
y: 0,
animated: true
});
}
showDirectMessagesModal = wrapWithPreventDoubleTap(() => {
goToDirectMessages = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@ -265,7 +184,7 @@ class List extends Component {
});
});
showMoreChannelsModal = wrapWithPreventDoubleTap(() => {
goToMoreChannels = wrapWithPreventDoubleTap(() => {
const {intl, navigator, theme} = this.props;
navigator.showModal({
@ -286,6 +205,18 @@ class List extends Component {
});
});
keyExtractor = (item) => item.id || item;
onSelectChannel = (channel, currentChannelId) => {
const {onSelectChannel} = this.props;
onSelectChannel(channel, currentChannelId);
};
onLayout = (event) => {
const {width} = event.nativeEvent.layout;
this.setState({width: width - 40});
};
renderSectionAction = (styles, action) => {
const {theme} = this.props;
return (
@ -302,83 +233,115 @@ class List extends Component {
);
};
renderDivider = (styles, marginLeft) => {
renderSectionSeparator = () => {
const {styles} = this.props;
return (
<View
style={[styles.divider, {marginLeft}]}
/>
<View style={[styles.divider]}/>
);
};
renderItem = ({item}) => {
if (!item.isTitle) {
return this.createChannelElement(item);
}
return item.title;
return (
<ChannelItem
channelId={item}
onSelectChannel={this.onSelectChannel}
/>
);
};
renderTitle = (styles, id, defaultMessage, action, topDivider, bottomDivider) => {
const {formatMessage} = this.props.intl;
renderUnreadItem = ({item}) => {
return (
<ChannelItem
channelId={item}
isUnread={true}
onSelectChannel={this.onSelectChannel}
/>
);
};
return {
renderSectionHeader = ({section}) => {
const {intl, styles} = this.props;
const {
action,
bottomSeparator,
defaultMessage,
id,
isTitle: true,
title: (
<View>
{topDivider && this.renderDivider(styles, 0)}
<View style={styles.titleContainer}>
<Text style={styles.title}>
{formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
{action && this.renderSectionAction(styles, action)}
</View>
{bottomDivider && this.renderDivider(styles, 0)}
topSeparator
} = section;
return (
<View>
{topSeparator && this.renderSectionSeparator()}
<View style={styles.titleContainer}>
<Text style={styles.title}>
{intl.formatMessage({id, defaultMessage}).toUpperCase()}
</Text>
{action && this.renderSectionAction(styles, action)}
</View>
)
};
{bottomSeparator && this.renderSectionSeparator()}
</View>
);
};
scrollToTop = () => {
if (this.refs.list) {
this.refs.list.scrollToOffset({
x: 0,
y: 0,
animated: true
});
}
};
emitUnreadIndicatorChange = debounce((showIndicator) => {
this.setState({showIndicator});
}, 100);
updateUnreadIndicators = ({viewableItems}) => {
const {unreadChannelIds} = this.props;
const firstUnread = unreadChannelIds[0];
if (firstUnread) {
const isVisible = viewableItems.find((v) => v.item === firstUnread);
if (isVisible) {
return this.emitUnreadIndicatorChange(false);
}
return this.emitUnreadIndicatorChange(true);
}
return this.emitUnreadIndicatorChange(false);
};
render() {
const {styles} = this.props;
const {dataSource, showAbove} = this.state;
let above;
if (showAbove) {
above = (
<UnreadIndicator
style={[styles.above, {width: (this.width - 40)}]}
onPress={this.scrollToTop}
text={(
<FormattedText
style={styles.indicatorText}
id='sidebar.unreadAbove'
defaultMessage='Unread post(s) above'
/>
)}
/>
);
}
const {styles, theme} = this.props;
const {sections, width, showIndicator} = this.state;
return (
<View
style={styles.container}
onLayout={this.onLayout}
>
<FlatList
<SectionList
ref='list'
data={dataSource}
sections={sections}
renderItem={this.renderItem}
keyExtractor={(item) => item.id}
renderSectionHeader={this.renderSectionHeader}
keyExtractor={this.keyExtractor}
onViewableItemsChanged={this.updateUnreadIndicators}
keyboardDismissMode='on-drag'
maxToRenderPerBatch={10}
stickySectionHeadersEnabled={false}
viewabilityConfig={{
viewAreaCoveragePercentThreshold: 3,
waitForInteraction: false
}}
/>
{above}
<UnreadIndicator
show={showIndicator}
style={[styles.above, {width}]}
onPress={this.scrollToTop}
theme={theme}
/>
</View>
);
}

View file

@ -1,20 +0,0 @@
// 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 {getCurrentTeam, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeams from './switch_teams';
function mapStateToProps(state, ownProps) {
return {
currentTeam: getCurrentTeam(state),
teamMembers: getTeamMemberships(state),
theme: getTheme(state),
...ownProps
};
}
export default connect(mapStateToProps)(SwitchTeams);

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 {getCurrentTeam, getMyTeamsCount, getChannelDrawerBadgeCount} from 'mattermost-redux/selectors/entities/teams';
import SwitchTeamsButton from './switch_teams_button';
function mapStateToProps(state) {
const team = getCurrentTeam(state);
return {
currentTeamId: team.id,
displayName: team.display_name,
mentionCount: getChannelDrawerBadgeCount(state),
teamsCount: getMyTeamsCount(state),
theme: getTheme(state)
};
}
export default connect(mapStateToProps)(SwitchTeamsButton);

View file

@ -14,96 +14,50 @@ import Badge from 'app/components/badge';
import {wrapWithPreventDoubleTap} from 'app/utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme';
export default class SwitchTeams extends React.PureComponent {
export default class SwitchTeamsButton extends React.PureComponent {
static propTypes = {
currentTeam: PropTypes.object,
currentTeamId: PropTypes.string,
displayName: PropTypes.string,
searching: PropTypes.bool.isRequired,
showTeams: PropTypes.func.isRequired,
teamMembers: PropTypes.object.isRequired,
onShowTeams: PropTypes.func.isRequired,
mentionCount: PropTypes.number.isRequired,
teamsCount: PropTypes.number.isRequired,
theme: PropTypes.object.isRequired
};
constructor(props) {
super(props);
this.state = {
badgeCount: this.getBadgeCount(props)
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.currentTeam !== this.props.currentTeam || nextProps.teamMembers !== this.props.teamMembers) {
this.setState({
badgeCount: this.getBadgeCount(nextProps)
});
}
}
getBadgeCount = (props) => {
const {
currentTeam,
teamMembers
} = props;
let mentionCount = 0;
let messageCount = 0;
Object.values(teamMembers).forEach((m) => {
if (m.team_id !== currentTeam.id) {
mentionCount = mentionCount + (m.mention_count || 0);
messageCount = messageCount + (m.msg_count || 0);
}
});
let badgeCount;
if (mentionCount) {
badgeCount = mentionCount;
} else if (messageCount) {
badgeCount = -1;
} else {
badgeCount = 0;
}
return badgeCount;
};
showTeams = wrapWithPreventDoubleTap(() => {
this.props.showTeams();
this.props.onShowTeams();
});
render() {
const {
currentTeam,
currentTeamId,
displayName,
mentionCount,
searching,
teamMembers,
teamsCount,
theme
} = this.props;
if (!currentTeam) {
if (!currentTeamId) {
return null;
}
const {
badgeCount
} = this.state;
if (searching || Object.keys(teamMembers).length < 2) {
if (searching || teamsCount < 2) {
return null;
}
const styles = getStyleSheet(theme);
let badge;
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
const badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={mentionCount}
minHeight={20}
minWidth={20}
/>
);
return (
<View>
@ -119,7 +73,7 @@ export default class SwitchTeams extends React.PureComponent {
/>
<View style={styles.switcherDivider}/>
<Text style={styles.switcherTeam}>
{currentTeam.display_name.substr(0, 2).toUpperCase()}
{displayName.substr(0, 2).toUpperCase()}
</Text>
</View>
</TouchableHighlight>

View file

@ -1,49 +0,0 @@
// 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 {
StyleSheet,
Text,
TouchableWithoutFeedback,
View,
ViewPropTypes
} from 'react-native';
export default class UnreadIndicator extends PureComponent {
static propTypes = {
style: ViewPropTypes.style,
textStyle: Text.propTypes.style,
text: PropTypes.node.isRequired,
onPress: PropTypes.func
};
static defaultProps = {
onPress: () => true
};
render() {
return (
<TouchableWithoutFeedback onPress={this.props.onPress}>
<View
style={[Styles.container, this.props.style]}
>
{this.props.text}
</View>
</TouchableWithoutFeedback>
);
}
}
const Styles = StyleSheet.create({
container: {
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
position: 'absolute',
borderRadius: 15,
marginHorizontal: 15,
height: 25
}
});

View file

@ -0,0 +1,48 @@
// 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 {StyleSheet, View} from 'react-native';
import Svg, {
G,
Path
} from 'react-native-svg';
export default class AboveIcon extends PureComponent {
static propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
color: PropTypes.string.isRequired
};
render() {
const {color, height, width} = this.props;
return (
<View style={[style.container, {height, width}]}>
<Svg
width={width}
height={height}
viewBox='0 0 10 10'
>
<G transform='matrix(1,0,0,1,-20,-18)'>
<G transform='matrix(0.0330723,0,0,0.0322634,15.8132,12.3164)'>
<Path
d='M245.803,377.493C245.803,377.493 204.794,336.485 179.398,311.088C168.55,300.24 150.962,300.24 140.114,311.088C138.327,312.875 136.517,314.686 134.73,316.473C123.882,327.321 123.882,344.908 134.73,355.756C167.972,388.998 233.949,454.975 256.949,477.975C262.158,483.184 269.223,486.111 276.591,486.111C277.38,486.111 278.176,486.111 278.965,486.111C286.332,486.111 293.397,483.184 298.607,477.975C321.607,454.975 387.584,388.998 420.826,355.756C431.674,344.908 431.674,327.321 420.826,316.473C419.039,314.686 417.228,312.875 415.441,311.088C404.593,300.24 387.005,300.24 376.158,311.088C350.761,336.485 309.753,377.493 309.753,377.493C309.753,377.493 309.753,279.687 309.753,203.94C309.753,196.573 306.826,189.508 301.617,184.298C296.408,179.089 289.342,176.162 281.975,176.162C279.191,176.162 276.364,176.162 273.58,176.162C266.213,176.162 259.148,179.089 253.939,184.298C248.729,189.508 245.803,196.573 245.803,203.94L245.803,377.493Z'
fill={color}
/>
</G>
</G>
</Svg>
</View>
);
}
}
const style = StyleSheet.create({
container: {
alignItems: 'flex-start',
transform: [{rotate: '180deg'}]
}
});

View file

@ -0,0 +1,80 @@
// 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 {
TouchableWithoutFeedback,
View,
ViewPropTypes
} from 'react-native';
import FormattedText from 'app/components/formatted_text';
import {makeStyleSheetFromTheme} from 'app/utils/theme';
import AboveIcon from './above_icon';
export default class UnreadIndicator extends PureComponent {
static propTypes = {
show: PropTypes.bool,
style: ViewPropTypes.style,
onPress: PropTypes.func,
theme: PropTypes.object.isRequired
};
static defaultProps = {
onPress: () => true
};
render() {
const {onPress, show, theme} = this.props;
if (!show) {
return null;
}
const style = getStyleSheet(theme);
return (
<TouchableWithoutFeedback onPress={onPress}>
<View
style={[style.container, this.props.style]}
>
<FormattedText
style={style.indicatorText}
id='sidebar.unreads'
defaultMessage='More unreads'
/>
<AboveIcon
width={12}
height={12}
color={theme.mentionColor}
/>
</View>
</TouchableWithoutFeedback>
);
}
}
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
container: {
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'row',
position: 'absolute',
borderRadius: 15,
marginHorizontal: 15,
height: 25
},
indicatorText: {
backgroundColor: 'transparent',
color: theme.mentionColor,
fontSize: 14,
paddingVertical: 2,
paddingHorizontal: 4,
textAlign: 'center',
textAlignVertical: 'center'
}
};
});

View file

@ -1,7 +1,7 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import React, {PureComponent} from 'react';
import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {StyleSheet} from 'react-native';
@ -9,7 +9,7 @@ import {changeOpacity} from 'app/utils/theme';
import Swiper from 'app/components/swiper';
export default class DrawerSwiper extends PureComponent {
export default class DrawerSwiper extends Component {
static propTypes = {
children: PropTypes.node.isRequired,
deviceWidth: PropTypes.number.isRequired,
@ -24,16 +24,28 @@ export default class DrawerSwiper extends PureComponent {
openDrawerOffset: 0
};
shouldComponentUpdate(nextProps) {
const {deviceWidth, showTeams, theme} = this.props;
return nextProps.deviceWidth !== deviceWidth ||
nextProps.showTeams !== showTeams || nextProps.theme !== theme;
}
runOnLayout = (shouldRun = true) => {
this.refs.swiper.runOnLayout = shouldRun;
if (this.refs.swiper) {
this.refs.swiper.runOnLayout = shouldRun;
}
};
resetPage = () => {
this.refs.swiper.scrollToIndex(1, false);
if (this.refs.swiper) {
this.refs.swiper.scrollToIndex(1, false);
}
};
scrollToStart = () => {
this.refs.swiper.scrollToStart();
if (this.refs.swiper) {
this.refs.swiper.scrollToStart();
}
};
swiperPageSelected = (index) => {
@ -41,7 +53,9 @@ export default class DrawerSwiper extends PureComponent {
};
showTeamsPage = () => {
this.refs.swiper.scrollToIndex(0, true);
if (this.refs.swiper) {
this.refs.swiper.scrollToIndex(0, true);
}
};
render() {

View file

@ -3,15 +3,16 @@
import {connect} from 'react-redux';
import {getDimensions, isLandscape} from 'app/selectors/device';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getDimensions} from 'app/selectors/device';
import DraweSwiper from './drawer_swiper';
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
return {
...ownProps,
...getDimensions(state),
isLandscape: isLandscape(state)
theme: getTheme(state)
};
}

View file

@ -6,8 +6,7 @@ import {connect} from 'react-redux';
import {joinChannel, viewChannel, markChannelAsRead} from 'mattermost-redux/actions/channels';
import {getTeams} from 'mattermost-redux/actions/teams';
import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getMyTeamsCount} from 'mattermost-redux/selectors/entities/teams';
import {handleSelectChannel, setChannelDisplayName, setChannelLoading} from 'app/actions/views/channel';
import {makeDirectChannel} from 'app/actions/views/more_dms';
@ -21,11 +20,10 @@ function mapStateToProps(state) {
return {
currentTeamId: getCurrentTeamId(state),
currentChannelId: getCurrentChannelId(state),
currentUserId,
isLandscape: isLandscape(state),
isTablet: isTablet(state),
teamsCount: Object.keys(getTeamMemberships(state)).length,
teamsCount: getMyTeamsCount(state),
theme: getTheme(state)
};
}

View file

@ -5,23 +5,25 @@ import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getCurrentTeamId, getJoinableTeams} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getJoinableTeamIds, getMySortedTeamIds} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
import {handleTeamChange} from 'app/actions/views/select_team';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getMySortedTeams} from 'app/selectors/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsList from './teams_list';
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
const currentUser = getCurrentUser(state);
const locale = currentUser ? currentUser.locale : 'en';
return {
joinableTeams: getJoinableTeams(state),
canJoinOtherTeams: getJoinableTeamIds(state).length > 0,
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
teams: getMySortedTeams(state),
theme: getTheme(state),
...ownProps
teamIds: getMySortedTeamIds(state, locale),
theme: getTheme(state)
};
}

View file

@ -24,13 +24,13 @@ class TeamsList extends PureComponent {
actions: PropTypes.shape({
handleTeamChange: PropTypes.func.isRequired
}).isRequired,
canJoinOtherTeams: PropTypes.bool.isRequired,
closeChannelDrawer: PropTypes.func.isRequired,
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
intl: intlShape.isRequired,
joinableTeams: PropTypes.object.isRequired,
navigator: PropTypes.object.isRequired,
teams: PropTypes.array.isRequired,
teamIds: PropTypes.array.isRequired,
theme: PropTypes.object.isRequired
};
@ -42,11 +42,11 @@ class TeamsList extends PureComponent {
});
}
selectTeam = (team) => {
selectTeam = (teamId) => {
requestAnimationFrame(() => {
const {actions, closeChannelDrawer, currentTeamId} = this.props;
if (team.id !== currentTeamId) {
actions.handleTeamChange(team);
if (teamId !== currentTeamId) {
actions.handleTeamChange(teamId);
}
closeChannelDrawer();
@ -81,25 +81,25 @@ class TeamsList extends PureComponent {
});
});
keyExtractor = (team) => {
return team.id;
}
keyExtractor = (item) => {
return item;
};
renderItem = ({item}) => {
return (
<TeamsListItem
selectTeam={this.selectTeam}
team={item}
teamId={item}
/>
);
};
render() {
const {joinableTeams, teams, theme} = this.props;
const {canJoinOtherTeams, teamIds, theme} = this.props;
const styles = getStyleSheet(theme);
let moreAction;
if (Object.keys(joinableTeams).length) {
if (canJoinOtherTeams) {
moreAction = (
<TouchableHighlight
style={styles.moreActionContainer}
@ -128,7 +128,7 @@ class TeamsList extends PureComponent {
</View>
</View>
<FlatList
data={teams}
data={teamIds}
renderItem={this.renderItem}
keyExtractor={this.keyExtractor}
viewabilityConfig={{

View file

@ -5,19 +5,23 @@ import {connect} from 'react-redux';
import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general';
import {getTheme} from 'mattermost-redux/selectors/entities/preferences';
import {getCurrentTeamId, getTeamMemberships} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentTeamId, getTeam, makeGetBadgeCountForTeamId} from 'mattermost-redux/selectors/entities/teams';
import {removeProtocol} from 'app/utils/url';
import TeamsListItem from './teams_list_item.js';
function mapStateToProps(state, ownProps) {
const team = getTeam(state, ownProps.teamId);
const getMentionCount = makeGetBadgeCountForTeamId();
return {
currentTeamId: getCurrentTeamId(state),
currentUrl: removeProtocol(getCurrentUrl(state)),
teamMember: getTeamMemberships(state)[ownProps.team.id],
theme: getTheme(state),
...ownProps
displayName: team.display_name,
mentionCount: getMentionCount(state, ownProps.teamId),
name: team.name,
theme: getTheme(state)
};
}

View file

@ -18,29 +18,32 @@ export default class TeamsListItem extends React.PureComponent {
static propTypes = {
currentTeamId: PropTypes.string.isRequired,
currentUrl: PropTypes.string.isRequired,
displayName: PropTypes.string.isRequired,
mentionCount: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
selectTeam: PropTypes.func.isRequired,
team: PropTypes.object.isRequired,
teamMember: PropTypes.object.isRequired,
teamId: PropTypes.string.isRequired,
theme: PropTypes.object.isRequired
};
selectTeam = wrapWithPreventDoubleTap(() => {
this.props.selectTeam(this.props.team);
this.props.selectTeam(this.props.teamId);
});
render() {
const {
currentTeamId,
currentUrl,
team,
teamMember,
displayName,
mentionCount,
name,
teamId,
theme
} = this.props;
const styles = getStyleSheet(theme);
let current;
let badge;
if (team.id === currentTeamId) {
if (teamId === currentTeamId) {
current = (
<View style={styles.checkmarkContainer}>
<IonIcon
@ -51,24 +54,15 @@ export default class TeamsListItem extends React.PureComponent {
);
}
let badgeCount = 0;
if (teamMember.mention_count) {
badgeCount = teamMember.mention_count;
} else if (teamMember.msg_count) {
badgeCount = -1;
}
if (badgeCount) {
badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={badgeCount}
minHeight={20}
minWidth={20}
/>
);
}
const badge = (
<Badge
style={styles.badge}
countStyle={styles.mention}
count={mentionCount}
minHeight={20}
minWidth={20}
/>
);
return (
<View style={styles.teamWrapper}>
@ -79,7 +73,7 @@ export default class TeamsListItem extends React.PureComponent {
<View style={styles.teamContainer}>
<View style={styles.teamIconContainer}>
<Text style={styles.teamIcon}>
{team.display_name.substr(0, 2).toUpperCase()}
{displayName.substr(0, 2).toUpperCase()}
</Text>
</View>
<View style={styles.teamNameContainer}>
@ -88,14 +82,14 @@ export default class TeamsListItem extends React.PureComponent {
ellipsizeMode='tail'
style={styles.teamName}
>
{team.display_name}
{displayName}
</Text>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.teamUrl}
>
{`${currentUrl}/${team.name}`}
{`${currentUrl}/${name}`}
</Text>
</View>
{current}

View file

@ -19,7 +19,7 @@ export default class ChannelIcon extends React.PureComponent {
static propTypes = {
isActive: PropTypes.bool,
isInfo: PropTypes.bool,
hasUnread: PropTypes.bool,
isUnread: PropTypes.bool,
membersCount: PropTypes.number,
size: PropTypes.number,
status: PropTypes.string,
@ -30,12 +30,12 @@ export default class ChannelIcon extends React.PureComponent {
static defaultProps = {
isActive: false,
isInfo: false,
hasUnread: false,
isUnread: false,
size: 12
};
render() {
const {isActive, hasUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const {isActive, isUnread, isInfo, membersCount, size, status, theme, type} = this.props;
const style = getStyleSheet(theme);
let activeIcon;
@ -46,7 +46,7 @@ export default class ChannelIcon extends React.PureComponent {
let unreadGroup;
let offlineColor = changeOpacity(theme.sidebarText, 0.5);
if (hasUnread) {
if (isUnread) {
unreadIcon = style.iconUnread;
unreadGroupBox = style.groupBoxUnread;
unreadGroup = style.groupUnread;

View file

@ -16,7 +16,7 @@ import {General} from 'mattermost-redux/constants';
import {
getCurrentChannel,
getCurrentChannelStats,
getChannelsByCategory,
getSortedFavoriteChannelIds,
canManageChannelMembers
} from 'mattermost-redux/selectors/entities/channels';
import {getCurrentUserId, getUser, getStatusForUserId, getCurrentUserRoles} from 'mattermost-redux/selectors/entities/users';
@ -25,7 +25,7 @@ import {isAdmin, isChannelAdmin, isSystemAdmin} from 'mattermost-redux/utils/use
import ChannelInfo from './channel_info';
function mapStateToProps(state, ownProps) {
function mapStateToProps(state) {
const {config, license} = state.entities.general;
const currentChannel = getCurrentChannel(state) || {};
const currentChannelCreator = getUser(state, currentChannel.creator_id);
@ -33,7 +33,7 @@ function mapStateToProps(state, ownProps) {
const currentChannelStats = getCurrentChannelStats(state);
const currentChannelMemberCount = currentChannelStats && currentChannelStats.member_count;
const currentUserId = getCurrentUserId(state);
const favoriteChannels = getChannelsByCategory(state).favoriteChannels.map((f) => f.id);
const favoriteChannels = getSortedFavoriteChannelIds(state);
const isCurrent = currentChannel.id === state.entities.channels.currentChannelId;
const isFavorite = favoriteChannels.indexOf(currentChannel.id) > -1;
const roles = getCurrentUserRoles(state);
@ -46,7 +46,6 @@ function mapStateToProps(state, ownProps) {
}
return {
...ownProps,
canDeleteChannel: showDeleteOption(config, license, currentChannel, isAdmin(roles), isSystemAdmin(roles), isChannelAdmin(roles)),
currentChannel,
currentChannelCreatorName,

View file

@ -58,7 +58,7 @@ export default class LoadTeam extends PureComponent {
onSelectTeam(team) {
const {handleTeamChange} = this.props.actions;
handleTeamChange(team).then(this.goToChannelView);
handleTeamChange(team.id).then(this.goToChannelView);
}
goToChannelView = () => {

View file

@ -134,7 +134,7 @@ export default class SelectTeam extends PureComponent {
return;
}
handleTeamChange(team);
handleTeamChange(team.id);
if (userWithoutTeams) {
this.goToChannelView();

View file

@ -1,23 +0,0 @@
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {createSelector} from 'reselect';
import {getMyTeams} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUser} from 'mattermost-redux/selectors/entities/users';
export const getMySortedTeams = createSelector(
getMyTeams,
getCurrentUser,
(teams, currentUser) => {
const locale = currentUser.locale;
return teams.sort((a, b) => {
if (a.display_name !== b.display_name) {
return a.display_name.toLowerCase().localeCompare(b.display_name.toLowerCase(), locale, {numeric: true});
}
return a.name.toLowerCase().localeCompare(b.name.toLowerCase(), locale, {numeric: true});
});
}
);

View file

@ -39,7 +39,7 @@
"react-native-passcode-status": "1.1.0",
"react-native-sentry": "0.15.1",
"react-native-status-bar-size": "0.3.2",
"react-native-svg": "5.4.1",
"react-native-svg": "react-native-community/react-native-svg#be8797be5239085d5d09eec90a926ae8da7a8583",
"react-native-tooltip": "enahum/react-native-tooltip",
"react-native-vector-icons": "4.3.0",
"react-native-youtube": "1.0.0-beta.3",
@ -51,6 +51,7 @@
"redux-thunk": "2.2.0",
"reselect": "3.0.1",
"semver": "5.4.1",
"shallow-equals": "1.0.0",
"socketcluster": "5.0.4",
"tinycolor2": "1.4.1",
"url-parse": "1.1.9",

View file

@ -3878,7 +3878,7 @@ makeerror@1.0.x:
mattermost-redux@mattermost/mattermost-redux#master:
version "0.0.1"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/9042b154c85f65b4e91b30bf2bc612048e3d194b"
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/1f64d99a0df42cf6513c12be6af965065eb1374f"
dependencies:
deep-equal "1.0.1"
harmony-reflect "1.5.1"
@ -3892,6 +3892,7 @@ mattermost-redux@mattermost/mattermost-redux#master:
redux-thunk "2.2.0"
reselect "3.0.1"
serialize-error "2.1.0"
shallow-equals "1.0.0"
md5-hex@^1.2.0:
version "1.3.0"
@ -5004,9 +5005,9 @@ react-native-svg-mock@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/react-native-svg-mock/-/react-native-svg-mock-1.0.2.tgz#2dc35fb97ca1e0ea393ece6d23782ba4f58e178f"
react-native-svg@5.4.1:
version "5.4.1"
resolved "https://registry.yarnpkg.com/react-native-svg/-/react-native-svg-5.4.1.tgz#c46191c786adbe9d5007342b4279efd153db8839"
react-native-svg@react-native-community/react-native-svg#be8797be5239085d5d09eec90a926ae8da7a8583:
version "6.0.0-rc1"
resolved "https://codeload.github.com/react-native-community/react-native-svg/tar.gz/be8797be5239085d5d09eec90a926ae8da7a8583"
dependencies:
color "^0.11.1"
lodash "^4.16.6"
@ -5819,6 +5820,10 @@ setprototypeof@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04"
shallow-equals@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/shallow-equals/-/shallow-equals-1.0.0.tgz#24b74bf1c634c11ed4c7182a6df6fb300dce4390"
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"