From cc966702fd12c99c3ba0dccc6caf0ef809d63778 Mon Sep 17 00:00:00 2001 From: enahum Date: Tue, 17 Oct 2017 14:06:28 -0300 Subject: [PATCH] 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 --- Makefile | 1 + .../mattermost/rnbeta/MainApplication.java | 2 +- android/settings.gradle | 4 +- app/actions/views/select_team.js | 10 +- app/components/badge.js | 4 + .../channel_drawer/channel_drawer.js | 41 +- .../{ => channel_item}/channel_item.js | 89 ++-- .../channels_list/channel_item/index.js | 32 ++ .../channels_list/channels_list.js | 25 +- .../channel_drawer/channels_list/index.js | 3 +- .../channels_list/list/index.js | 37 +- .../channel_drawer/channels_list/list/list.js | 411 ++++++++---------- .../channels_list/switch_teams/index.js | 20 - .../switch_teams_button/index.js | 23 + .../switch_teams_button.js} | 92 +--- .../channels_list/unread_indicator.js | 49 --- .../unread_indicator/above_icon.js | 48 ++ .../channels_list/unread_indicator/index.js | 80 ++++ .../drawer_swipper/drawer_swiper.js | 26 +- .../channel_drawer/drawer_swipper/index.js | 9 +- app/components/channel_drawer/index.js | 6 +- .../channel_drawer/teams_list/index.js | 16 +- .../channel_drawer/teams_list/teams_list.js | 24 +- .../teams_list/teams_list_item/index.js | 12 +- .../teams_list_item/teams_list_item.js | 50 +-- app/components/channel_icon.js | 8 +- app/screens/channel_info/index.js | 7 +- app/screens/load_team/load_team.js | 2 +- app/screens/select_team/select_team.js | 2 +- app/selectors/teams.js | 23 - package.json | 3 +- yarn.lock | 13 +- 32 files changed, 616 insertions(+), 556 deletions(-) rename app/components/channel_drawer/channels_list/{ => channel_item}/channel_item.js (68%) create mode 100644 app/components/channel_drawer/channels_list/channel_item/index.js delete mode 100644 app/components/channel_drawer/channels_list/switch_teams/index.js create mode 100644 app/components/channel_drawer/channels_list/switch_teams_button/index.js rename app/components/channel_drawer/channels_list/{switch_teams/switch_teams.js => switch_teams_button/switch_teams_button.js} (58%) delete mode 100644 app/components/channel_drawer/channels_list/unread_indicator.js create mode 100644 app/components/channel_drawer/channels_list/unread_indicator/above_icon.js create mode 100644 app/components/channel_drawer/channels_list/unread_indicator/index.js delete mode 100644 app/selectors/teams.js diff --git a/Makefile b/Makefile index 71c7d5cb9..a5bcfb5f0 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index ee6ca2d17..5aed4c115 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -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; diff --git a/android/settings.gradle b/android/settings.gradle index c85e18ae2..6325f0c0d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -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') diff --git a/app/actions/views/select_team.js b/app/actions/views/select_team.js index a56391bf3..c0357bb5d 100644 --- a/app/actions/views/select_team.js +++ b/app/actions/views/select_team.js @@ -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); } diff --git a/app/components/badge.js b/app/components/badge.js index 2efa43ced..3cc7613a1 100644 --- a/app/components/badge.js +++ b/app/components/badge.js @@ -118,6 +118,10 @@ export default class Badge extends PureComponent { }; render() { + if (!this.props.count) { + return null; + } + return ( { + 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} /> ); @@ -354,7 +366,6 @@ export default class ChannelDrawer extends PureComponent { onPageSelected={this.onPageSelected} openDrawerOffset={openDrawerOffset} showTeams={showTeams} - theme={theme} > {lists} diff --git a/app/components/channel_drawer/channels_list/channel_item.js b/app/components/channel_drawer/channels_list/channel_item/channel_item.js similarity index 68% rename from app/components/channel_drawer/channels_list/channel_item.js rename to app/components/channel_drawer/channels_list/channel_item/channel_item.js index 81cd3b33f..2dac318f9 100644 --- a/app/components/channel_drawer/channels_list/channel_item.js +++ b/app/components/channel_drawer/channels_list/channel_item/channel_item.js @@ -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 = ( + + ); + } else if (isUnread) { + extraTextStyle = style.textUnread; + } - let activeBorder; let badge; - - if (mentions && !isActive) { + if (mentions) { badge = ( - ); - } - const icon = ( - ); @@ -92,15 +97,15 @@ export default class ChannelItem extends PureComponent { onPress={this.onPress} > - {activeBorder} - + {extraBorder} + {icon} - {channel.display_name} + {displayName} {badge} diff --git a/app/components/channel_drawer/channels_list/channel_item/index.js b/app/components/channel_drawer/channels_list/channel_item/index.js new file mode 100644 index 000000000..775a75bf3 --- /dev/null +++ b/app/components/channel_drawer/channels_list/channel_item/index.js @@ -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); diff --git a/app/components/channel_drawer/channels_list/channels_list.js b/app/components/channel_drawer/channels_list/channels_list.js index fbd48eb0b..e220574fb 100644 --- a/app/components/channel_drawer/channels_list/channels_list.js +++ b/app/components/channel_drawer/channels_list/channels_list.js @@ -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 { > - {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' } }; }); diff --git a/app/components/channel_drawer/channels_list/index.js b/app/components/channel_drawer/channels_list/index.js index 8e5849af6..65f1cb47b 100644 --- a/app/components/channel_drawer/channels_list/index.js +++ b/app/components/channel_drawer/channels_list/index.js @@ -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) }; } diff --git a/app/components/channel_drawer/channels_list/list/index.js b/app/components/channel_drawer/channels_list/list/index.js index 791badbbf..23b2d160a 100644 --- a/app/components/channel_drawer/channels_list/list/index.js +++ b/app/components/channel_drawer/channels_list/list/index.js @@ -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); diff --git a/app/components/channel_drawer/channels_list/list/list.js b/app/components/channel_drawer/channels_list/list/list.js index c639455e2..632ebd211 100644 --- a/app/components/channel_drawer/channels_list/list/list.js +++ b/app/components/channel_drawer/channels_list/list/list.js @@ -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 ( - - ); - }; - - 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 ( - + ); }; renderItem = ({item}) => { - if (!item.isTitle) { - return this.createChannelElement(item); - } - return item.title; + return ( + + ); }; - renderTitle = (styles, id, defaultMessage, action, topDivider, bottomDivider) => { - const {formatMessage} = this.props.intl; + renderUnreadItem = ({item}) => { + return ( + + ); + }; - return { + renderSectionHeader = ({section}) => { + const {intl, styles} = this.props; + const { + action, + bottomSeparator, + defaultMessage, id, - isTitle: true, - title: ( - - {topDivider && this.renderDivider(styles, 0)} - - - {formatMessage({id, defaultMessage}).toUpperCase()} - - {action && this.renderSectionAction(styles, action)} - - {bottomDivider && this.renderDivider(styles, 0)} + topSeparator + } = section; + + return ( + + {topSeparator && this.renderSectionSeparator()} + + + {intl.formatMessage({id, defaultMessage}).toUpperCase()} + + {action && this.renderSectionAction(styles, action)} - ) - }; + {bottomSeparator && this.renderSectionSeparator()} + + ); + }; + + 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 = ( - - )} - /> - ); - } + const {styles, theme} = this.props; + const {sections, width, showIndicator} = this.state; return ( - item.id} + renderSectionHeader={this.renderSectionHeader} + keyExtractor={this.keyExtractor} onViewableItemsChanged={this.updateUnreadIndicators} keyboardDismissMode='on-drag' maxToRenderPerBatch={10} + stickySectionHeadersEnabled={false} viewabilityConfig={{ viewAreaCoveragePercentThreshold: 3, waitForInteraction: false }} /> - {above} + ); } diff --git a/app/components/channel_drawer/channels_list/switch_teams/index.js b/app/components/channel_drawer/channels_list/switch_teams/index.js deleted file mode 100644 index 86ee07591..000000000 --- a/app/components/channel_drawer/channels_list/switch_teams/index.js +++ /dev/null @@ -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); diff --git a/app/components/channel_drawer/channels_list/switch_teams_button/index.js b/app/components/channel_drawer/channels_list/switch_teams_button/index.js new file mode 100644 index 000000000..d50262ce0 --- /dev/null +++ b/app/components/channel_drawer/channels_list/switch_teams_button/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 {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); diff --git a/app/components/channel_drawer/channels_list/switch_teams/switch_teams.js b/app/components/channel_drawer/channels_list/switch_teams_button/switch_teams_button.js similarity index 58% rename from app/components/channel_drawer/channels_list/switch_teams/switch_teams.js rename to app/components/channel_drawer/channels_list/switch_teams_button/switch_teams_button.js index 73c54e9dd..bdca1daee 100644 --- a/app/components/channel_drawer/channels_list/switch_teams/switch_teams.js +++ b/app/components/channel_drawer/channels_list/switch_teams_button/switch_teams_button.js @@ -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 = ( - - ); - } + const badge = ( + + ); return ( @@ -119,7 +73,7 @@ export default class SwitchTeams extends React.PureComponent { /> - {currentTeam.display_name.substr(0, 2).toUpperCase()} + {displayName.substr(0, 2).toUpperCase()} diff --git a/app/components/channel_drawer/channels_list/unread_indicator.js b/app/components/channel_drawer/channels_list/unread_indicator.js deleted file mode 100644 index 7fccee0a1..000000000 --- a/app/components/channel_drawer/channels_list/unread_indicator.js +++ /dev/null @@ -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 ( - - - {this.props.text} - - - ); - } -} - -const Styles = StyleSheet.create({ - container: { - justifyContent: 'center', - alignItems: 'center', - flexDirection: 'row', - position: 'absolute', - borderRadius: 15, - marginHorizontal: 15, - height: 25 - } -}); diff --git a/app/components/channel_drawer/channels_list/unread_indicator/above_icon.js b/app/components/channel_drawer/channels_list/unread_indicator/above_icon.js new file mode 100644 index 000000000..976795785 --- /dev/null +++ b/app/components/channel_drawer/channels_list/unread_indicator/above_icon.js @@ -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 ( + + + + + + + + + + ); + } +} + +const style = StyleSheet.create({ + container: { + alignItems: 'flex-start', + transform: [{rotate: '180deg'}] + } +}); diff --git a/app/components/channel_drawer/channels_list/unread_indicator/index.js b/app/components/channel_drawer/channels_list/unread_indicator/index.js new file mode 100644 index 000000000..1361ad436 --- /dev/null +++ b/app/components/channel_drawer/channels_list/unread_indicator/index.js @@ -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 ( + + + + + + + ); + } +} + +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' + } + }; +}); diff --git a/app/components/channel_drawer/drawer_swipper/drawer_swiper.js b/app/components/channel_drawer/drawer_swipper/drawer_swiper.js index bef5db83d..9b25ecc9f 100644 --- a/app/components/channel_drawer/drawer_swipper/drawer_swiper.js +++ b/app/components/channel_drawer/drawer_swipper/drawer_swiper.js @@ -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() { diff --git a/app/components/channel_drawer/drawer_swipper/index.js b/app/components/channel_drawer/drawer_swipper/index.js index 9b2bc08d2..f637d5c97 100644 --- a/app/components/channel_drawer/drawer_swipper/index.js +++ b/app/components/channel_drawer/drawer_swipper/index.js @@ -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) }; } diff --git a/app/components/channel_drawer/index.js b/app/components/channel_drawer/index.js index cd4ab1d66..4d2114f7f 100644 --- a/app/components/channel_drawer/index.js +++ b/app/components/channel_drawer/index.js @@ -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) }; } diff --git a/app/components/channel_drawer/teams_list/index.js b/app/components/channel_drawer/teams_list/index.js index debb190ab..570905e35 100644 --- a/app/components/channel_drawer/teams_list/index.js +++ b/app/components/channel_drawer/teams_list/index.js @@ -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) }; } diff --git a/app/components/channel_drawer/teams_list/teams_list.js b/app/components/channel_drawer/teams_list/teams_list.js index a8953ee81..216550bc3 100644 --- a/app/components/channel_drawer/teams_list/teams_list.js +++ b/app/components/channel_drawer/teams_list/teams_list.js @@ -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 ( ); }; 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 = ( { - 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 = ( - ); - } + const badge = ( + + ); return ( @@ -79,7 +73,7 @@ export default class TeamsListItem extends React.PureComponent { - {team.display_name.substr(0, 2).toUpperCase()} + {displayName.substr(0, 2).toUpperCase()} @@ -88,14 +82,14 @@ export default class TeamsListItem extends React.PureComponent { ellipsizeMode='tail' style={styles.teamName} > - {team.display_name} + {displayName} - {`${currentUrl}/${team.name}`} + {`${currentUrl}/${name}`} {current} diff --git a/app/components/channel_icon.js b/app/components/channel_icon.js index da45db496..b85473c21 100644 --- a/app/components/channel_icon.js +++ b/app/components/channel_icon.js @@ -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; diff --git a/app/screens/channel_info/index.js b/app/screens/channel_info/index.js index 193d72660..917e82fdf 100644 --- a/app/screens/channel_info/index.js +++ b/app/screens/channel_info/index.js @@ -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, diff --git a/app/screens/load_team/load_team.js b/app/screens/load_team/load_team.js index f0546dfbf..2489e2085 100644 --- a/app/screens/load_team/load_team.js +++ b/app/screens/load_team/load_team.js @@ -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 = () => { diff --git a/app/screens/select_team/select_team.js b/app/screens/select_team/select_team.js index 32e970542..891369a61 100644 --- a/app/screens/select_team/select_team.js +++ b/app/screens/select_team/select_team.js @@ -134,7 +134,7 @@ export default class SelectTeam extends PureComponent { return; } - handleTeamChange(team); + handleTeamChange(team.id); if (userWithoutTeams) { this.goToChannelView(); diff --git a/app/selectors/teams.js b/app/selectors/teams.js deleted file mode 100644 index a10d78d43..000000000 --- a/app/selectors/teams.js +++ /dev/null @@ -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}); - }); - } -); diff --git a/package.json b/package.json index bd9ac5e6f..175a3effb 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index a530489ca..95da0c295 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"