From 98abbd6dca68e4755d0535002effa260ac4c775e Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Fri, 22 Sep 2017 14:48:30 -0400 Subject: [PATCH] RN-345 Additional improvements to channel switching (#926) * RN-345 Only show refreshing indicator when channel is actually refreshing * Added getCurrentLocale selector * Added SwitchTeams component to channel drawer to reduce rerenders * Reduced number of props passed from ChannelsList into children * Removed unused prop from FilteredList * Moved visible post calculation out of render function * Don't set the channel to loading when not switching channels * Added most of channel view state to blacklist --- app/actions/views/channel.js | 7 + .../channel_drawer/channel_drawer.js | 9 +- .../channels_list/channels_list.js | 150 +++------------- .../filtered_list/filtered_list.js | 4 +- .../channels_list/filtered_list/index.js | 13 +- .../channel_drawer/channels_list/index.js | 8 - .../channels_list/list/index.js | 6 + .../channel_drawer/channels_list/list/list.js | 26 ++- .../channels_list/switch_teams/index.js | 20 +++ .../switch_teams/switch_teams.js | 168 ++++++++++++++++++ app/components/post_list/post_list.js | 11 +- app/components/root/index.js | 11 +- app/components/search_bar/search_bar.ios.js | 4 +- app/constants/view.js | 1 + app/i18n/index.js | 2 +- app/reducers/views/channel.js | 19 +- .../channel_post_list/channel_post_list.js | 38 +++- .../channel/channel_post_list/index.js | 2 +- app/selectors/i18n.js | 31 ++++ app/store/index.js | 23 ++- 20 files changed, 380 insertions(+), 173 deletions(-) create mode 100644 app/components/channel_drawer/channels_list/switch_teams/index.js create mode 100644 app/components/channel_drawer/channels_list/switch_teams/switch_teams.js create mode 100644 app/selectors/i18n.js diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 4c2cd15a1..f43a71571 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -325,6 +325,13 @@ export function setChannelLoading(loading = true) { }; } +export function setChannelRefreshing(loading = true) { + return { + type: ViewTypes.SET_CHANNEL_REFRESHING, + loading + }; +} + export function setPostTooltipVisible(visible = true) { return { type: ViewTypes.POST_TOOLTIP_VISIBLE, diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/channel_drawer/channel_drawer.js index 438f2f657..d265c8a54 100644 --- a/app/components/channel_drawer/channel_drawer.js +++ b/app/components/channel_drawer/channel_drawer.js @@ -206,9 +206,12 @@ export default class ChannelDrawer extends PureComponent { } = actions; markChannelAsRead(channel.id, currentChannelId); - setChannelLoading(); - viewChannel(currentChannelId); - setChannelDisplayName(channel.display_name); + + if (channel.id !== currentChannelId) { + setChannelLoading(); + viewChannel(currentChannelId); + setChannelDisplayName(channel.display_name); + } this.closeChannelDrawer(); diff --git a/app/components/channel_drawer/channels_list/channels_list.js b/app/components/channel_drawer/channels_list/channels_list.js index bab3d24ce..fbd48eb0b 100644 --- a/app/components/channel_drawer/channels_list/channels_list.js +++ b/app/components/channel_drawer/channels_list/channels_list.js @@ -5,7 +5,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { Platform, - Text, TouchableHighlight, View } from 'react-native'; @@ -13,22 +12,17 @@ import {injectIntl, intlShape} from 'react-intl'; import AwesomeIcon from 'react-native-vector-icons/FontAwesome'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import Badge from 'app/components/badge'; import SearchBar from 'app/components/search_bar'; -import {preventDoubleTap} from 'app/utils/tap'; +import {wrapWithPreventDoubleTap} from 'app/utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; import FilteredList from './filtered_list'; import List from './list'; +import SwitchTeams from './switch_teams'; class ChannelsList extends React.PureComponent { static propTypes = { - channels: PropTypes.object.isRequired, - channelMembers: PropTypes.object, - currentChannel: PropTypes.object, - currentTeam: PropTypes.object.isRequired, intl: intlShape.isRequired, - myTeamMembers: PropTypes.object.isRequired, navigator: PropTypes.object, onJoinChannel: PropTypes.func.isRequired, onSearchEnds: PropTypes.func.isRequired, @@ -38,11 +32,6 @@ class ChannelsList extends React.PureComponent { theme: PropTypes.object.isRequired }; - static defaultProps = { - currentTeam: {}, - currentChannel: {} - }; - constructor(props) { super(props); this.firstUnreadChannel = null; @@ -66,7 +55,7 @@ class ChannelsList extends React.PureComponent { this.refs.search_bar.cancel(); }; - openSettingsModal = () => { + openSettingsModal = wrapWithPreventDoubleTap(() => { const {intl, navigator, theme} = this.props; navigator.showModal({ @@ -88,7 +77,7 @@ class ChannelsList extends React.PureComponent { }] } }); - }; + }); onSearch = (term) => { this.setState({term}); @@ -107,33 +96,30 @@ class ChannelsList extends React.PureComponent { render() { const { - currentChannel, - currentTeam, intl, - myTeamMembers, + navigator, onShowTeams, theme } = this.props; - if (!currentChannel) { - return {'Loading'}; - } - const {searching, term} = this.state; - const teamMembers = Object.values(myTeamMembers); - const showMembers = teamMembers.length > 1; const styles = getStyleSheet(theme); let settings; let list; if (searching) { - const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles, term}; - list = ; + list = ( + + ); } else { settings = ( preventDoubleTap(this.openSettingsModal)} + onPress={this.openSettingsModal} underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} > ); - const listProps = {...this.props, onSelectChannel: this.onSelectChannel, styles}; - list = ; + list = ( + + ); } const title = ( @@ -174,67 +165,18 @@ class ChannelsList extends React.PureComponent { ); - let badge; - let switcher; - if (showMembers && !searching) { - let mentionCount = 0; - let messageCount = 0; - teamMembers.forEach((m) => { - if (m.team_id !== currentTeam.id) { - mentionCount = mentionCount + (m.mention_count || 0); - messageCount = messageCount + (m.msg_count || 0); - } - }); - - let badgeCount = 0; - if (mentionCount) { - badgeCount = mentionCount; - } else if (messageCount) { - badgeCount = -1; - } - - if (badgeCount) { - badge = ( - - ); - } - - switcher = ( - preventDoubleTap(onShowTeams)} - underlayColor={changeOpacity(theme.sidebarHeaderBg, 0.5)} - > - - - - - {currentTeam.display_name.substr(0, 2).toUpperCase()} - - - - ); - } - return ( - {switcher} + {title} {settings} - {badge} {list} @@ -249,9 +191,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { backgroundColor: theme.sidebarBg, flex: 1 }, - extraPadding: { - paddingBottom: 5 - }, statusBar: { backgroundColor: theme.sidebarHeaderBg, ...Platform.select({ @@ -303,7 +242,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { fontSize: 18, fontWeight: '300' }, - titleContainer: { + titleContainer: { // These aren't used by this component, but they are passed down to the list component alignItems: 'center', flex: 1, flexDirection: 'row', @@ -330,43 +269,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { } }) }, - switcherContainer: { - alignItems: 'center', - backgroundColor: theme.sidebarHeaderTextColor, - borderRadius: 2, - flexDirection: 'row', - height: 32, - justifyContent: 'center', - marginLeft: 6, - marginRight: 10, - paddingHorizontal: 6 - }, - switcherDivider: { - backgroundColor: theme.sidebarHeaderBg, - height: 15, - marginHorizontal: 6, - width: 1 - }, - switcherTeam: { - color: theme.sidebarHeaderBg, - fontFamily: 'OpenSans', - fontSize: 14 - }, - badge: { - backgroundColor: theme.mentionBj, - borderColor: theme.sidebarHeaderBg, - borderRadius: 10, - borderWidth: 1, - flexDirection: 'row', - padding: 3, - position: 'absolute', - left: 5, - top: 0 - }, - mention: { - color: theme.mentionColor, - fontSize: 10 - }, divider: { backgroundColor: changeOpacity(theme.sidebarText, 0.1), height: 1 diff --git a/app/components/channel_drawer/channels_list/filtered_list/filtered_list.js b/app/components/channel_drawer/channels_list/filtered_list/filtered_list.js index faf776404..44dbd1dcb 100644 --- a/app/components/channel_drawer/channels_list/filtered_list/filtered_list.js +++ b/app/components/channel_drawer/channels_list/filtered_list/filtered_list.js @@ -13,7 +13,6 @@ import { import {injectIntl, intlShape} from 'react-intl'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; -import {preventDoubleTap} from 'app/utils/tap'; import {changeOpacity} from 'app/utils/theme'; import {General} from 'mattermost-redux/constants'; @@ -34,7 +33,6 @@ class FilteredList extends Component { currentTeam: PropTypes.object.isRequired, currentUserId: PropTypes.string, currentChannel: PropTypes.object, - groupChannels: PropTypes.array, groupChannelMemberDetails: PropTypes.object, intl: intlShape.isRequired, teammateNameDisplay: PropTypes.string, @@ -325,7 +323,7 @@ class FilteredList extends Component { return ( preventDoubleTap(action, this)} + onPress={action} underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)} > { + createPrivateChannel = wrapWithPreventDoubleTap(() => { const {intl, navigator, theme} = this.props; navigator.showModal({ @@ -171,7 +171,7 @@ class List extends Component { closeButton: this.closeButton } }); - }; + }); buildChannels = (props) => { const {canCreatePrivateChannels, styles} = props; @@ -233,7 +233,15 @@ class List extends Component { return data; }; - showDirectMessagesModal = () => { + scrollToTop = () => { + this.refs.list.scrollToOffset({ + x: 0, + y: 0, + animated: true + }); + } + + showDirectMessagesModal = wrapWithPreventDoubleTap(() => { const {intl, navigator, theme} = this.props; navigator.showModal({ @@ -255,9 +263,9 @@ class List extends Component { }] } }); - }; + }); - showMoreChannelsModal = () => { + showMoreChannelsModal = wrapWithPreventDoubleTap(() => { const {intl, navigator, theme} = this.props; navigator.showModal({ @@ -276,14 +284,14 @@ class List extends Component { closeButton: this.closeButton } }); - }; + }); renderSectionAction = (styles, action) => { const {theme} = this.props; return ( preventDoubleTap(action, this)} + onPress={action} underlayColor={changeOpacity(theme.sidebarTextHoverBg, 0.5)} > this.refs.list.scrollToOffset({x: 0, y: 0, animated: true})} + onPress={this.scrollToTop} text={( { + 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(); + }); + + render() { + const { + currentTeam, + searching, + teamMembers, + theme + } = this.props; + + const { + badgeCount + } = this.state; + + if (searching || teamMembers.length < 2) { + return null; + } + + const styles = getStyleSheet(theme); + + let badge; + if (badgeCount) { + badge = ( + + ); + } + + return ( + + + + + + + {currentTeam.display_name.substr(0, 2).toUpperCase()} + + + + {badge} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + switcherContainer: { + alignItems: 'center', + backgroundColor: theme.sidebarHeaderTextColor, + borderRadius: 2, + flexDirection: 'row', + height: 32, + justifyContent: 'center', + marginLeft: 6, + marginRight: 10, + paddingHorizontal: 6 + }, + switcherDivider: { + backgroundColor: theme.sidebarHeaderBg, + height: 15, + marginHorizontal: 6, + width: 1 + }, + switcherTeam: { + color: theme.sidebarHeaderBg, + fontFamily: 'OpenSans', + fontSize: 14 + }, + badge: { + backgroundColor: theme.mentionBj, + borderColor: theme.sidebarHeaderBg, + borderRadius: 10, + borderWidth: 1, + flexDirection: 'row', + padding: 3, + position: 'absolute', + left: -5, + top: -5 + }, + mention: { + color: theme.mentionColor, + fontSize: 10 + } + }; +}); diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index c34c8720c..e9927f483 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -35,6 +35,7 @@ export default class PostList extends PureComponent { loadMore: PropTypes.func, navigator: PropTypes.object, onPostPress: PropTypes.func, + onRefresh: PropTypes.func, posts: PropTypes.array.isRequired, refreshing: PropTypes.bool, renderReplies: PropTypes.bool, @@ -78,11 +79,19 @@ export default class PostList extends PureComponent { }; onRefresh = () => { - const {actions, channel} = this.props; + const { + actions, + channel, + onRefresh + } = this.props; if (Object.keys(channel).length) { actions.refreshChannelWithRetry(channel.id); } + + if (onRefresh) { + onRefresh(); + } }; renderChannelIntro = () => { diff --git a/app/components/root/index.js b/app/components/root/index.js index 65967dab6..68509e281 100644 --- a/app/components/root/index.js +++ b/app/components/root/index.js @@ -2,26 +2,19 @@ // See License.txt for license information. import {connect} from 'react-redux'; -import DeviceInfo from 'react-native-device-info'; import {Client, Client4} from 'mattermost-redux/client'; import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; import {getCurrentUrl} from 'mattermost-redux/selectors/entities/general'; +import {getCurrentLocale} from 'app/selectors/i18n'; import {getTheme} from 'app/selectors/preferences'; import {removeProtocol} from 'app/utils/url'; import Root from './root'; function mapStateToProps(state, ownProps) { - const users = state.entities.users; - const {currentUserId} = users; - - let locale = DeviceInfo.getDeviceLocale().split('-')[0]; - if (currentUserId && users.profiles[currentUserId]) { - locale = users.profiles[currentUserId].locale; - } - + const locale = getCurrentLocale(state); Client.setLocale(locale); Client4.setAcceptLanguage(locale); diff --git a/app/components/search_bar/search_bar.ios.js b/app/components/search_bar/search_bar.ios.js index 4536f0ec2..32cc4d841 100644 --- a/app/components/search_bar/search_bar.ios.js +++ b/app/components/search_bar/search_bar.ios.js @@ -1,12 +1,12 @@ // Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. // See License.txt for license information. -import React, {Component} from 'react'; +import React, {PureComponent} from 'react'; import {InteractionManager, Keyboard} from 'react-native'; import PropTypes from 'prop-types'; import Search from './search_box'; -export default class SearchBarIos extends Component { +export default class SearchBarIos extends PureComponent { static propTypes = { onCancelButtonPress: PropTypes.func, onChangeText: PropTypes.func, diff --git a/app/constants/view.js b/app/constants/view.js index 14bf61412..799e13467 100644 --- a/app/constants/view.js +++ b/app/constants/view.js @@ -34,6 +34,7 @@ const ViewTypes = keyMirror({ ADD_FILE_TO_FETCH_CACHE: null, SET_CHANNEL_LOADER: null, + SET_CHANNEL_REFRESHING: null, SET_CHANNEL_DISPLAY_NAME: null, POST_TOOLTIP_VISIBLE: null, diff --git a/app/i18n/index.js b/app/i18n/index.js index 44620e4f3..9289829ef 100644 --- a/app/i18n/index.js +++ b/app/i18n/index.js @@ -32,7 +32,7 @@ import ru from 'assets/i18n/ru.json'; import zhCN from 'assets/i18n/zh-CN.json'; import zhTW from 'assets/i18n/zh-TW.json'; -const DEFAULT_LOCALE = 'en'; +export const DEFAULT_LOCALE = 'en'; const TRANSLATIONS = { de, diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index be6bb1b0c..0312249d7 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -2,7 +2,11 @@ // See License.txt for license information. import {combineReducers} from 'redux'; -import {ChannelTypes, FileTypes, PostTypes} from 'mattermost-redux/action_types'; +import { + ChannelTypes, + FileTypes, + PostTypes +} from 'mattermost-redux/action_types'; import {ViewTypes} from 'app/constants'; @@ -181,6 +185,18 @@ function loading(state = false, action) { } } +function refreshing(state = false, action) { + switch (action.type) { + case PostTypes.GET_POSTS_SUCCESS: + case PostTypes.GET_POSTS_FAILURE: + return false; + case ViewTypes.SET_CHANNEL_REFRESHING: + return action.loading; + default: + return state; + } +} + function tooltipVisible(state = false, action) { switch (action.type) { case ViewTypes.POST_TOOLTIP_VISIBLE: @@ -236,6 +252,7 @@ export default combineReducers({ displayName, drafts, loading, + refreshing, tooltipVisible, postVisibility, loadingPosts diff --git a/app/screens/channel/channel_post_list/channel_post_list.js b/app/screens/channel/channel_post_list/channel_post_list.js index 485ced103..0b51841ab 100644 --- a/app/screens/channel/channel_post_list/channel_post_list.js +++ b/app/screens/channel/channel_post_list/channel_post_list.js @@ -51,7 +51,9 @@ class ChannelPostList extends PureComponent { super(props); this.state = { - retryMessageHeight: new Animated.Value(0) + retryMessageHeight: new Animated.Value(0), + visiblePosts: this.getVisiblePosts(props), + showLoadMore: false }; } @@ -83,12 +85,22 @@ class ChannelPostList extends PureComponent { this.setState({ showLoadMore }); + + if (nextProps.posts !== this.props.posts || nextProps.postVisibility !== this.props.postVisibility) { + this.setState({ + visiblePosts: this.getVisiblePosts(nextProps) + }); + } } componentWillUnmount() { this.mounted = false; } + getVisiblePosts = (props) => { + return props.posts.slice(0, props.posts.postVisibility); + } + shouldMarkChannelAsLoaded = (postsCount, channelHasMessages, channelRefreshingFailed) => { if (postsCount || channelHasMessages || channelRefreshingFailed) { this.channelLoaded(); @@ -159,8 +171,13 @@ class ChannelPostList extends PureComponent { this.props.actions.loadPostsIfNecessaryWithRetry(channelId); }; + loadPostsRetry = () => { + this.loadPosts(this.props.channel.id); + }; + render() { const { + actions, channel, channelIsLoading, channelIsRefreshing, @@ -169,17 +186,20 @@ class ChannelPostList extends PureComponent { myMember, navigator, posts, - postVisibility, theme } = this.props; - const {retryMessageHeight} = this.state; + const { + retryMessageHeight, + showLoadMore, + visiblePosts + } = this.state; let component; if (!posts.length && channelRefreshingFailed) { component = ( this.loadPosts(channel.id)} + retry={this.loadPostsRetry} theme={theme} /> ); @@ -188,11 +208,12 @@ class ChannelPostList extends PureComponent { } else { component = ( + {component} { + return currentUser ? currentUser.locale : ''; + } +); + +// Not a proper selector since the device locale isn't in the redux store +export function getCurrentLocale(state) { + const userLocale = getCurrentUserLocale(state); + if (userLocale) { + return userLocale; + } + + const deviceLocale = DeviceInfo.getDeviceLocale().split('-')[0]; + if (deviceLocale) { + return deviceLocale; + } + + return DEFAULT_LOCALE; +} diff --git a/app/store/index.js b/app/store/index.js index 7c45644cc..c22ed359d 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -48,6 +48,26 @@ export default function configureAppStore(initialState) { ['typing'] ); + const channelViewBlackList = {loading: true, refreshing: true, tooltipVisible: true, postVisibility: true, loadingPosts: true}; + const channelViewBlackListFilter = createTransform( + (inboundState) => { + const channel = {}; + + for (const channelKey of Object.keys(inboundState.channel)) { + if (channelViewBlackList[channelKey]) { + channel[channelKey] = inboundState.channel[channelKey]; + } + } + + return { + ...inboundState, + channel + }; + }, + null, + {whitelist: ['views']} // Only run this filter the views state (or any other entry that ends up being named views) + ); + const setTransformer = createTransform( (inboundState, key) => { if (key === 'entities') { @@ -165,7 +185,8 @@ export default function configureAppStore(initialState) { transforms: [ setTransformer, viewsBlackListFilter, - typingBlackListFilter + typingBlackListFilter, + channelViewBlackListFilter ] } };