diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index f43a71571..291838a9d 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -184,34 +184,35 @@ export function loadThreadIfNecessary(rootId, channelId) { export function selectInitialChannel(teamId) { return async (dispatch, getState) => { const state = getState(); - const {channels, currentChannelId, myMembers} = state.entities.channels; + const {channels, myMembers} = state.entities.channels; const {currentUserId} = state.entities.users; - const currentChannel = channels[currentChannelId]; const {myPreferences} = state.entities.preferences; + const lastChannelId = state.views.team.lastChannelForTeam[teamId] || ''; + const lastChannel = channels[lastChannelId]; - const isDMVisible = currentChannel && currentChannel.type === General.DM_CHANNEL && - isDirectChannelVisible(currentUserId, myPreferences, currentChannel); + const isDMVisible = lastChannel && lastChannel.type === General.DM_CHANNEL && + isDirectChannelVisible(currentUserId, myPreferences, lastChannel); - const isGMVisible = currentChannel && currentChannel.type === General.GM_CHANNEL && - isGroupChannelVisible(myPreferences, currentChannel); + const isGMVisible = lastChannel && lastChannel.type === General.GM_CHANNEL && + isGroupChannelVisible(myPreferences, lastChannel); - if (currentChannel && myMembers[currentChannelId] && - (currentChannel.team_id === teamId || isDMVisible || isGMVisible)) { - await handleSelectChannel(currentChannelId)(dispatch, getState); + if (lastChannelId && myMembers[lastChannelId] && + (lastChannel.team_id === teamId || isDMVisible || isGMVisible)) { + handleSelectChannel(lastChannelId)(dispatch, getState); return; } const channel = Object.values(channels).find((c) => c.team_id === teamId && c.name === General.DEFAULT_CHANNEL); if (channel) { dispatch(setChannelDisplayName('')); - await handleSelectChannel(channel.id)(dispatch, getState); + handleSelectChannel(channel.id)(dispatch, getState); } else { // Handle case when the default channel cannot be found // so we need to get the first available channel of the team const channelsInTeam = Object.values(channels).filter((c) => c.team_id === teamId); const firstChannel = channelsInTeam.length ? channelsInTeam[0].id : {id: ''}; dispatch(setChannelDisplayName('')); - await handleSelectChannel(firstChannel.id)(dispatch, getState); + handleSelectChannel(firstChannel.id)(dispatch, getState); } }; } @@ -220,13 +221,15 @@ export function handleSelectChannel(channelId) { return async (dispatch, getState) => { const {currentTeamId} = getState().entities.teams; + selectChannel(channelId)(dispatch, getState); + dispatch(setChannelLoading(false)); + dispatch({ type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM, teamId: currentTeamId, channelId }); getChannelStats(channelId)(dispatch, getState); - selectChannel(channelId)(dispatch, getState); }; } diff --git a/app/actions/views/select_team.js b/app/actions/views/select_team.js index 00a71786b..a56391bf3 100644 --- a/app/actions/views/select_team.js +++ b/app/actions/views/select_team.js @@ -26,9 +26,9 @@ export function handleTeamChange(team, selectChannel = true) { ]; if (selectChannel) { - const lastChannelId = state.views.team.lastChannelForTeam[team.id] || ''; - actions.push({type: ChannelTypes.SELECT_CHANNEL, data: lastChannelId}); + actions.push({type: ChannelTypes.SELECT_CHANNEL, data: ''}); + const lastChannelId = state.views.team.lastChannelForTeam[team.id] || ''; const currentChannelId = getCurrentChannelId(state); viewChannel(lastChannelId, currentChannelId)(dispatch, getState); markChannelAsRead(lastChannelId, currentChannelId)(dispatch, getState); diff --git a/app/components/channel_drawer/channel_drawer.js b/app/components/channel_drawer/channel_drawer.js index 527790664..6695d64bd 100644 --- a/app/components/channel_drawer/channel_drawer.js +++ b/app/components/channel_drawer/channel_drawer.js @@ -74,6 +74,7 @@ export default class ChannelDrawer extends PureComponent { EventEmitter.on('close_channel_drawer', this.closeChannelDrawer); EventEmitter.on(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle); BackHandler.addEventListener('hardwareBackPress', this.handleAndroidBack); + this.mounted = true; } componentWillReceiveProps(nextProps) { @@ -94,6 +95,7 @@ export default class ChannelDrawer extends PureComponent { EventEmitter.off('close_channel_drawer', this.closeChannelDrawer); EventEmitter.off(WebsocketEvents.CHANNEL_UPDATED, this.handleUpdateTitle); BackHandler.removeEventListener('hardwareBackPress', this.handleAndroidBack); + this.mounted = false; } handleAndroidBack = () => { @@ -106,7 +108,9 @@ export default class ChannelDrawer extends PureComponent { }; closeChannelDrawer = () => { - this.setState({openDrawer: false}); + if (this.mounted) { + this.setState({openDrawer: false}); + } }; drawerSwiperRef = (ref) => { @@ -121,7 +125,7 @@ export default class ChannelDrawer extends PureComponent { this.closeLeftHandle = null; } - if (this.state.openDrawer) { + if (this.state.openDrawer && this.mounted) { // The state doesn't get updated if you swipe to close this.setState({ openDrawer: false @@ -133,7 +137,7 @@ export default class ChannelDrawer extends PureComponent { if (!this.closeLeftHandle) { this.closeLeftHandle = InteractionManager.createInteractionHandle(); } - } + }; handleDrawerOpen = () => { if (this.state.openDrawerOffset !== 0) { @@ -151,7 +155,7 @@ export default class ChannelDrawer extends PureComponent { this.openLeftHandle = InteractionManager.createInteractionHandle(); } - if (!this.state.openDrawer) { + if (!this.state.openDrawer && this.mounted) { // The state doesn't get updated if you swipe to open this.setState({ openDrawer: true @@ -188,9 +192,11 @@ export default class ChannelDrawer extends PureComponent { openChannelDrawer = () => { this.props.blurPostTextBox(); - this.setState({ - openDrawer: true - }); + if (this.mounted) { + this.setState({ + openDrawer: true + }); + } }; selectChannel = (channel) => { @@ -207,18 +213,17 @@ export default class ChannelDrawer extends PureComponent { viewChannel } = actions; - markChannelAsRead(channel.id, currentChannelId); - - if (channel.id !== currentChannelId) { - setChannelLoading(); - viewChannel(currentChannelId); - setChannelDisplayName(channel.display_name); - } + setChannelLoading(); + setChannelDisplayName(channel.display_name); this.closeChannelDrawer(); InteractionManager.runAfterInteractions(() => { handleSelectChannel(channel.id); + markChannelAsRead(channel.id, currentChannelId); + if (channel.id !== currentChannelId) { + viewChannel(currentChannelId); + } }); }; @@ -368,6 +373,7 @@ export default class ChannelDrawer extends PureComponent { onOpenStart={this.handleDrawerOpenStart} onOpen={this.handleDrawerOpen} onClose={this.handleDrawerClose} + onCloseStart={this.handleDrawerCloseStart} captureGestures='open' type='static' acceptTap={true} diff --git a/app/components/channel_drawer/channels_list/channel_item.js b/app/components/channel_drawer/channels_list/channel_item.js index 4443227e5..81cd3b33f 100644 --- a/app/components/channel_drawer/channels_list/channel_item.js +++ b/app/components/channel_drawer/channels_list/channel_item.js @@ -26,9 +26,9 @@ export default class ChannelItem extends PureComponent { onPress = () => { const {channel, onSelectChannel} = this.props; - setTimeout(() => { + requestAnimationFrame(() => { preventDoubleTap(onSelectChannel, this, channel); - }, 100); + }); }; render() { diff --git a/app/components/channel_drawer/channels_list/switch_teams/switch_teams.js b/app/components/channel_drawer/channels_list/switch_teams/switch_teams.js index 3b57431ad..b18cbbb73 100644 --- a/app/components/channel_drawer/channels_list/switch_teams/switch_teams.js +++ b/app/components/channel_drawer/channels_list/switch_teams/switch_teams.js @@ -16,7 +16,7 @@ import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; export default class SwitchTeams extends React.PureComponent { static propTypes = { - currentTeam: PropTypes.object.isRequired, + currentTeam: PropTypes.object, searching: PropTypes.bool.isRequired, showTeams: PropTypes.func.isRequired, teamMembers: PropTypes.object.isRequired, @@ -78,6 +78,10 @@ export default class SwitchTeams extends React.PureComponent { theme } = this.props; + if (!currentTeam) { + return null; + } + const { badgeCount } = this.state; diff --git a/app/components/channel_drawer/teams_list/teams_list.js b/app/components/channel_drawer/teams_list/teams_list.js index ad4b80e61..7f1c39fd2 100644 --- a/app/components/channel_drawer/teams_list/teams_list.js +++ b/app/components/channel_drawer/teams_list/teams_list.js @@ -45,14 +45,14 @@ class TeamsList extends PureComponent { } selectTeam = (team) => { - const {actions, closeChannelDrawer, currentTeamId} = this.props; - if (team.id === currentTeamId) { - closeChannelDrawer(); - } else { - actions.handleTeamChange(team); + requestAnimationFrame(() => { + const {actions, closeChannelDrawer, currentTeamId} = this.props; + if (team.id !== currentTeamId) { + actions.handleTeamChange(team); + } closeChannelDrawer(); - } + }); }; goToSelectTeam = () => { @@ -125,11 +125,7 @@ class TeamsList extends PureComponent { { - setTimeout(() => { - preventDoubleTap(this.selectTeam, this, item); - }, 100); - }} + onPress={() => preventDoubleTap(this.selectTeam, this, item)} > diff --git a/app/components/channel_loader.js b/app/components/channel_loader.js deleted file mode 100644 index 2702e25d8..000000000 --- a/app/components/channel_loader.js +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. -// See License.txt for license information. - -import React from 'react'; -import PropTypes from 'prop-types'; -import { - View -} from 'react-native'; -import LinearGradient from 'react-native-linear-gradient'; - -import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; - -const GRADIENT_START = 0.05; -const GRADIENT_MIDDLE = 0.1; -const GRADIENT_END = 0.01; - -function buildSections(key, style, theme, top) { - return ( - - - - - - - - - ); -} - -export default function channelLoader(props) { - const style = getStyleSheet(props.theme); - - return ( - - {Array(10).fill().map((item, index) => buildSections(index, style, props.theme, index === 0))} - - ); -} - -channelLoader.propTypes = { - theme: PropTypes.object.isRequired -}; - -const getStyleSheet = makeStyleSheetFromTheme((theme) => { - return { - avatar: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - borderRadius: 16, - height: 32, - width: 32 - }, - container: { - backgroundColor: theme.centerChannelBg, - flex: 1 - }, - messageText: { - backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), - height: 10, - marginBottom: 10 - }, - section: { - flexDirection: 'row', - paddingLeft: 12, - paddingRight: 20, - marginVertical: 10 - }, - sectionMessage: { - marginLeft: 12, - flex: 1 - } - }; -}); diff --git a/app/components/channel_loader/channel_loader.js b/app/components/channel_loader/channel_loader.js new file mode 100644 index 000000000..669291dd0 --- /dev/null +++ b/app/components/channel_loader/channel_loader.js @@ -0,0 +1,127 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; +import { + Platform, + View +} from 'react-native'; +import LinearGradient from 'react-native-linear-gradient'; + +import {changeOpacity, makeStyleSheetFromTheme} from 'app/utils/theme'; + +const GRADIENT_START = 0.05; +const GRADIENT_MIDDLE = 0.1; +const GRADIENT_END = 0.01; + +export default class ChannelLoader extends PureComponent { + static propTypes = { + channelIsLoading: PropTypes.bool.isRequired, + deviceWidth: PropTypes.number.isRequired, + theme: PropTypes.object.isRequired + }; + + buildSections(key, style, top) { + return ( + + + + + + + + + ); + } + + render() { + const {channelIsLoading, deviceWidth, theme} = this.props; + + if (!channelIsLoading) { + return null; + } + + const style = getStyleSheet(theme); + + return ( + + {Array(20).fill().map((item, index) => this.buildSections(index, style, index === 0))} + + ); + } +} + +const getStyleSheet = makeStyleSheetFromTheme((theme) => { + return { + container: { + backgroundColor: theme.centerChannelBg, + flex: 1, + position: 'absolute', + ...Platform.select({ + android: { + top: 0 + }, + ios: { + top: 15 + } + }) + }, + avatar: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), + borderRadius: 16, + height: 32, + width: 32 + }, + messageText: { + backgroundColor: changeOpacity(theme.centerChannelColor, 0.1), + height: 10, + marginBottom: 10 + }, + section: { + backgroundColor: theme.centerChannelBg, + flexDirection: 'row', + paddingLeft: 12, + paddingRight: 20, + marginVertical: 10 + }, + sectionMessage: { + marginLeft: 12, + flex: 1 + } + }; +}); + diff --git a/app/components/channel_loader/index.js b/app/components/channel_loader/index.js new file mode 100644 index 000000000..ec68326d9 --- /dev/null +++ b/app/components/channel_loader/index.js @@ -0,0 +1,19 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {connect} from 'react-redux'; +import {getTheme} from 'app/selectors/preferences'; + +import ChannelLoader from './channel_loader'; + +function mapStateToProps(state, ownProps) { + const {deviceWidth} = state.device.dimension; + return { + ...ownProps, + channelIsLoading: state.views.channel.loading, + deviceWidth, + theme: getTheme(state) + }; +} + +export default connect(mapStateToProps)(ChannelLoader); diff --git a/app/components/post_list/post_list.js b/app/components/post_list/post_list.js index e9927f483..7cd51c661 100644 --- a/app/components/post_list/post_list.js +++ b/app/components/post_list/post_list.js @@ -25,8 +25,7 @@ export default class PostList extends PureComponent { actions: PropTypes.shape({ refreshChannelWithRetry: PropTypes.func.isRequired }).isRequired, - channel: PropTypes.object, - channelIsLoading: PropTypes.bool.isRequired, + channelId: PropTypes.string, currentUserId: PropTypes.string, indicateNewMessages: PropTypes.bool, isLoadingMore: PropTypes.bool, @@ -44,11 +43,6 @@ export default class PostList extends PureComponent { theme: PropTypes.object.isRequired }; - static defaultProps = { - channel: {}, - channelIsLoading: false - }; - getPostsWithDates = () => { const {posts, indicateNewMessages, currentUserId, lastViewedAt, showLoadMore} = this.props; const list = addDatesToPostList(posts, {indicateNewMessages, currentUserId, lastViewedAt}); @@ -81,12 +75,12 @@ export default class PostList extends PureComponent { onRefresh = () => { const { actions, - channel, + channelId, onRefresh } = this.props; - if (Object.keys(channel).length) { - actions.refreshChannelWithRetry(channel.id); + if (channelId) { + actions.refreshChannelWithRetry(channelId); } if (onRefresh) { @@ -95,9 +89,9 @@ export default class PostList extends PureComponent { }; renderChannelIntro = () => { - const {channel, channelIsLoading, navigator, refreshing, showLoadMore} = this.props; + const {channelId, navigator, refreshing, showLoadMore} = this.props; - if (channel.hasOwnProperty('id') && !showLoadMore && !refreshing && !channelIsLoading) { + if (channelId && !showLoadMore && !refreshing) { return ( @@ -169,13 +163,13 @@ export default class PostList extends PureComponent { }; render() { - const {channel, refreshing, theme} = this.props; + const {channelId, refreshing, theme} = this.props; const refreshControl = { refreshing }; - if (Object.keys(channel).length) { + if (channelId) { refreshControl.onRefresh = this.onRefresh; } @@ -187,7 +181,7 @@ export default class PostList extends PureComponent { keyExtractor={this.keyExtractor} ListFooterComponent={this.renderChannelIntro} onEndReached={this.loadMorePosts} - onEndReachedThreshold={700} + onEndReachedThreshold={0} {...refreshControl} renderItem={this.renderItem} theme={theme} diff --git a/app/reducers/views/channel.js b/app/reducers/views/channel.js index 0312249d7..7b8b514f3 100644 --- a/app/reducers/views/channel.js +++ b/app/reducers/views/channel.js @@ -176,8 +176,6 @@ function drafts(state = {}, action) { function loading(state = false, action) { switch (action.type) { - case ChannelTypes.SELECT_CHANNEL: - return false; case ViewTypes.SET_CHANNEL_LOADER: return action.loading; default: diff --git a/app/screens/channel/channel.js b/app/screens/channel/channel.js index b67dbd5f6..759fda07d 100644 --- a/app/screens/channel/channel.js +++ b/app/screens/channel/channel.js @@ -14,6 +14,7 @@ import {RequestStatus} from 'mattermost-redux/constants'; import EventEmitter from 'mattermost-redux/utils/event_emitter'; import ChannelDrawer from 'app/components/channel_drawer'; +import ChannelLoader from 'app/components/channel_loader'; import KeyboardLayout from 'app/components/layout/keyboard_layout'; import Loading from 'app/components/loading'; import OfflineIndicator from 'app/components/offline_indicator'; @@ -227,6 +228,7 @@ class Channel extends PureComponent { navigator={navigator} /> + = props.postVisibility }; } componentDidMount() { - const {channel, posts, channelRefreshingFailed} = this.props; + const {channelId} = this.props; this.mounted = true; - this.loadPosts(this.props.channel.id); - this.shouldMarkChannelAsLoaded(posts.length, channel.total_msg_count === 0, channelRefreshingFailed); + this.loadPosts(channelId); } componentWillReceiveProps(nextProps) { - const {channel: currentChannel} = this.props; - const {channel: nextChannel, channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps; + const {channelId: currentChannelId} = this.props; + const {channelId: nextChannelId, channelRefreshingFailed: nextChannelRefreshingFailed, posts: nextPosts} = nextProps; - if (currentChannel.id !== nextChannel.id) { + if (currentChannelId !== nextChannelId) { // Load the posts when the channel actually changes - this.loadPosts(nextChannel.id); + this.loadPosts(nextChannelId); } - if (nextChannelRefreshingFailed && this.state.channelLoaded && nextPosts.length) { + if (nextChannelRefreshingFailed && nextPosts.length) { this.toggleRetryMessage(); } else if (!nextChannelRefreshingFailed || !nextPosts.length) { this.toggleRetryMessage(false); } - this.shouldMarkChannelAsLoaded(nextPosts.length, nextChannel.total_msg_count === 0, nextChannelRefreshingFailed); - const showLoadMore = nextProps.posts.length >= nextProps.postVisibility; - this.setState({ - showLoadMore - }); + let visiblePosts = this.state.visiblePosts; if (nextProps.posts !== this.props.posts || nextProps.postVisibility !== this.props.postVisibility) { - this.setState({ - visiblePosts: this.getVisiblePosts(nextProps) - }); + visiblePosts = this.getVisiblePosts(nextProps); } + + this.setState({ + showLoadMore, + visiblePosts + }); } componentWillUnmount() { @@ -98,17 +99,7 @@ class ChannelPostList extends PureComponent { } getVisiblePosts = (props) => { - return props.posts.slice(0, props.posts.postVisibility); - } - - shouldMarkChannelAsLoaded = (postsCount, channelHasMessages, channelRefreshingFailed) => { - if (postsCount || channelHasMessages || channelRefreshingFailed) { - this.channelLoaded(); - } - }; - - channelLoaded = () => { - this.setState({channelLoaded: true}); + return props.posts.slice(0, props.postVisibility); }; toggleRetryMessage = (show = true) => { @@ -120,19 +111,17 @@ class ChannelPostList extends PureComponent { }; goToThread = (post) => { - const {actions, channel, intl, navigator, theme} = this.props; - const channelId = post.channel_id; + const {actions, channelId, channelDisplayName, channelType, intl, navigator, theme} = this.props; const rootId = (post.root_id || post.id); actions.loadThreadIfNecessary(post.root_id, channelId); actions.selectPost(rootId); let title; - if (channel.type === General.DM_CHANNEL) { + if (channelType === General.DM_CHANNEL) { title = intl.formatMessage({id: 'mobile.routes.thread_dm', defaultMessage: 'Direct Message Thread'}); } else { - const channelName = channel.display_name; - title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName}); + title = intl.formatMessage({id: 'mobile.routes.thread', defaultMessage: '{channelName} Thread'}, {channelName: channelDisplayName}); } const options = { @@ -161,29 +150,28 @@ class ChannelPostList extends PureComponent { loadMorePosts = () => { if (this.state.showLoadMore) { - const {actions, channel} = this.props; - actions.increasePostVisibility(channel.id); + const {actions, channelId} = this.props; + actions.increasePostVisibility(channelId); } }; loadPosts = (channelId) => { - this.setState({channelLoaded: false}); this.props.actions.loadPostsIfNecessaryWithRetry(channelId); }; loadPostsRetry = () => { - this.loadPosts(this.props.channel.id); + this.loadPosts(this.props.channelId); }; render() { const { actions, - channel, - channelIsLoading, + channelId, channelIsRefreshing, channelRefreshingFailed, + currentUserId, + lastViewedAt, loadingPosts, - myMember, navigator, posts, theme @@ -203,8 +191,6 @@ class ChannelPostList extends PureComponent { theme={theme} /> ); - } else if (channelIsLoading) { - component = ; } else { component = ( ); } diff --git a/app/screens/channel/channel_post_list/index.js b/app/screens/channel/channel_post_list/index.js index e22ad7521..1536cbcc0 100644 --- a/app/screens/channel/channel_post_list/index.js +++ b/app/screens/channel/channel_post_list/index.js @@ -8,6 +8,7 @@ import {selectPost} from 'mattermost-redux/actions/posts'; import {RequestStatus} from 'mattermost-redux/constants'; import {makeGetPostsInChannel} from 'mattermost-redux/selectors/entities/posts'; import {getMyCurrentChannelMembership, makeGetChannel} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {loadPostsIfNecessaryWithRetry, loadThreadIfNecessary, increasePostVisibility, refreshChannelWithRetry} from 'app/actions/views/channel'; import {getConnection} from 'app/selectors/device'; import {getTheme} from 'app/selectors/preferences'; @@ -40,16 +41,21 @@ function makeMapStateToProps() { channelRefreshingFailed = false; } + const channel = getChannel(state, {id: channelId}); + return { - channel: getChannel(state, {id: channelId}), - channelIsLoading: state.views.channel.loading, + channelId, channelIsRefreshing, channelRefreshingFailed, + currentUserId: getCurrentUserId(state), + channelType: channel.type, + channelDisplayName: channel.display_name, posts, postVisibility: state.views.channel.postVisibility[channelId], loadingPosts: state.views.channel.loadingPosts[channelId], - myMember: getMyCurrentChannelMembership(state), + LastViewedAt: getMyCurrentChannelMembership(state).last_viewed_at, networkOnline, + totalMessageCount: channel.total_msg_count, theme: getTheme(state), ...ownProps }; diff --git a/app/screens/search/search.js b/app/screens/search/search.js index e6669acb9..b49aff655 100644 --- a/app/screens/search/search.js +++ b/app/screens/search/search.js @@ -6,6 +6,7 @@ import PropTypes from 'prop-types'; import {injectIntl, intlShape} from 'react-intl'; import { Keyboard, + InteractionManager, Platform, SectionList, StyleSheet, @@ -97,13 +98,13 @@ class Search extends Component { const recentLenght = recent.length; const shouldScroll = prevStatus !== status && (status === RequestStatus.SUCCESS || status === RequestStatus.STARTED); - if (shouldScroll && !this.state.isFocused) { - setTimeout(() => { + if (shouldScroll) { + requestAnimationFrame(() => { this.refs.list._wrapperListRef.getListRef().scrollToOffset({ //eslint-disable-line no-underscore-dangle animated: true, offset: SECTION_HEIGHT + (2 * MODIFIER_LABEL_HEIGHT) + (recentLenght * RECENT_LABEL_HEIGHT) + ((recentLenght + 1) * RECENT_SEPARATOR_HEIGHT) }); - }, 200); + }); } } @@ -430,6 +431,8 @@ class Search extends Component { viewChannel } = actions; + setChannelLoading(); + const channel = channels.find((c) => c.id === channelId); let displayName = ''; @@ -440,10 +443,11 @@ class Search extends Component { this.props.navigator.dismissModal({animationType: 'none'}); markChannelAsRead(channelId, currentChannelId); - setChannelLoading(); viewChannel(channelId, currentChannelId); setChannelDisplayName(displayName); - handleSelectChannel(channelId); + InteractionManager.runAfterInteractions(() => { + handleSelectChannel(channelId); + }); } }; diff --git a/app/selectors/views.js b/app/selectors/views.js index 8d3ff8c59..98f9efe9b 100644 --- a/app/selectors/views.js +++ b/app/selectors/views.js @@ -3,7 +3,7 @@ import {createSelector} from 'reselect'; -import {getCurrentChannel} from 'mattermost-redux/selectors/entities/channels'; +import {getCurrentChannelId} from 'mattermost-redux/selectors/entities/channels'; const emptyDraft = { draft: '', @@ -20,9 +20,9 @@ function getThreadDrafts(state) { export const getCurrentChannelDraft = createSelector( getChannelDrafts, - getCurrentChannel, - (drafts, currentChannel) => { - return drafts[currentChannel.id] || emptyDraft; + getCurrentChannelId, + (drafts, currentChannelId) => { + return drafts[currentChannelId] || emptyDraft; } ); diff --git a/app/store/index.js b/app/store/index.js index c22ed359d..876ca1e84 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -48,7 +48,7 @@ export default function configureAppStore(initialState) { ['typing'] ); - const channelViewBlackList = {loading: true, refreshing: true, tooltipVisible: true, postVisibility: true, loadingPosts: true}; + const channelViewBlackList = {loading: true, refreshing: true, tooltipVisible: true, loadingPosts: true}; const channelViewBlackListFilter = createTransform( (inboundState) => { const channel = {}; diff --git a/app/utils/why_did_you_update.js b/app/utils/why_did_you_update.js new file mode 100644 index 000000000..891a0aff7 --- /dev/null +++ b/app/utils/why_did_you_update.js @@ -0,0 +1,49 @@ +/* eslint-disable */ + +// Gist created by https://benchling.engineering/a-deep-dive-into-react-perf-debugging-fd2063f5a667 + +import _ from 'underscore'; + +function isRequiredUpdateObject(o) { + return Array.isArray(o) || (o && o.constructor === Object.prototype.constructor); +} + +function deepDiff(o1, o2, p) { + const notify = (status) => { + console.warn('Update %s', status); + console.log('%cbefore', 'font-weight: bold', o1); + console.log('%cafter ', 'font-weight: bold', o2); + }; + if (!_.isEqual(o1, o2)) { + console.group(p); + if ([o1, o2].every(_.isFunction)) { + notify('avoidable?'); + } else if (![o1, o2].every(isRequiredUpdateObject)) { + notify('required.'); + } else { + const keys = _.union(_.keys(o1), _.keys(o2)); + for (const key of keys) { + deepDiff(o1[key], o2[key], key); + } + } + console.groupEnd(); + } else if (o1 !== o2) { + console.group(p); + notify('avoidable!'); + if (_.isObject(o1) && _.isObject(o2)) { + const keys = _.union(_.keys(o1), _.keys(o2)); + for (const key of keys) { + deepDiff(o1[key], o2[key], key); + } + } + console.groupEnd(); + } +} + +function whyDidYouUpdate(prevProps, prevState) { + deepDiff({props: prevProps, state: prevState}, + {props: this.props, state: this.state}, + this.constructor.name); +} + +export default whyDidYouUpdate; diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock index c8b35a002..f2bfb378b 100644 --- a/fastlane/Gemfile.lock +++ b/fastlane/Gemfile.lock @@ -24,7 +24,7 @@ GEM faraday_middleware (0.12.2) faraday (>= 0.7.4, < 1.0) fastimage (2.1.0) - fastlane (2.58.0) + fastlane (2.59.0) CFPropertyList (>= 2.3, < 3.0.0) addressable (>= 2.3, < 3.0.0) babosa (>= 1.0.2, < 2.0.0) @@ -126,7 +126,7 @@ GEM unf_ext (0.0.7.4) unicode-display_width (1.3.0) word_wrap (1.0.0) - xcodeproj (1.5.1) + xcodeproj (1.5.2) CFPropertyList (~> 2.3.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) diff --git a/package.json b/package.json index adceefa9c..bd9ac5e6f 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,8 @@ "react-native-svg-mock": "1.0.2", "react-test-renderer": "16.0.0-alpha.12", "remote-redux-devtools": "0.5.12", - "remote-redux-devtools-on-debugger": "0.8.2" + "remote-redux-devtools-on-debugger": "0.8.2", + "underscore": "1.8.3" }, "scripts": { "test": "NODE_ENV=test nyc --reporter=text mocha --opts test/mocha.opts", diff --git a/yarn.lock b/yarn.lock index 6ab0a062d..364d558e8 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/850e7df06cd7dcae7e592f78ca7bbdd6ab2ec56a" + resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/85db14af9a6d8c402213b0eb3c372f1ac6adad34" dependencies: deep-equal "1.0.1" harmony-reflect "1.5.1" @@ -6411,6 +6411,10 @@ ultron@~1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.0.tgz#b07a2e6a541a815fc6a34ccd4533baec307ca864" +underscore@1.8.3: + version "1.8.3" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"