diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index a297e4bce..9916c6b89 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -38,10 +38,6 @@ import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences' import {getCurrentUserId, getUserIdsInChannels, getUsers} from 'mattermost-redux/selectors/entities/users'; import {getTeamByName} from 'mattermost-redux/selectors/entities/teams'; -import {getChannelReachable} from 'app/selectors/channel'; - -import telemetry from 'app/telemetry'; - import { getChannelByName, getDirectChannelName, @@ -55,6 +51,8 @@ import {getLastCreateAt} from 'mattermost-redux/utils/post_utils'; import {getPreferencesByCategory} from 'mattermost-redux/utils/preference_utils'; import {INSERT_TO_COMMENT, INSERT_TO_DRAFT} from 'app/constants/post_textbox'; +import {getChannelReachable} from 'app/selectors/channel'; +import telemetry from 'app/telemetry'; import {isDirectChannelVisible, isGroupChannelVisible, isDirectMessageVisible, isGroupMessageVisible, isDirectChannelAutoClosed} from 'app/utils/channels'; import {buildPreference} from 'app/utils/preferences'; @@ -349,21 +347,18 @@ export function selectDefaultChannel(teamId) { }; } -export function handleSelectChannel(channelId, fromPushNotification = false) { +export function handleSelectChannel(channelId) { return async (dispatch, getState) => { const dt = Date.now(); const state = getState(); - const {channels, myMembers} = state.entities.channels; + const {channels, currentChannelId, myMembers} = state.entities.channels; const {currentTeamId} = state.entities.teams; const channel = channels[channelId]; const member = myMembers[channelId]; - // If the app is open from push notification, we already fetched the posts. - if (!fromPushNotification) { - dispatch(loadPostsIfNecessaryWithRetry(channelId)); - } + dispatch(loadPostsIfNecessaryWithRetry(channelId)); - if (channel) { + if (channel && currentChannelId !== channelId) { dispatch({ type: ChannelTypes.SELECT_CHANNEL, data: channelId, @@ -373,9 +368,11 @@ export function handleSelectChannel(channelId, fromPushNotification = false) { teamId: channel.team_id || currentTeamId, }, }); + + dispatch(markChannelViewedAndRead(channelId, currentChannelId)); } - console.log('channel switch in', channel?.display_name, (Date.now() - dt), 'ms'); //eslint-disable-line + console.log('channel switch to', channel?.display_name, (Date.now() - dt), 'ms'); //eslint-disable-line }; } diff --git a/app/actions/views/channel.test.js b/app/actions/views/channel.test.js index de48887e8..fefeb4ab0 100644 --- a/app/actions/views/channel.test.js +++ b/app/actions/views/channel.test.js @@ -118,9 +118,11 @@ describe('Actions.Views.Channel', () => { currentChannelId, channels: { 'channel-id': {id: 'channel-id', display_name: 'Test Channel'}, + 'channel-id-2': {id: 'channel-id-2', display_name: 'Test Channel'}, }, myMembers: { 'channel-id': {channel_id: 'channel-id', user_id: currentUserId, mention_count: 0, msg_count: 0}, + 'channel-id-2': {channel_id: 'channel-id-2', user_id: currentUserId, mention_count: 0, msg_count: 0}, }, }, teams: { @@ -282,17 +284,17 @@ describe('Actions.Views.Channel', () => { }); const handleSelectChannelCases = [ - [currentChannelId, true], - [currentChannelId, false], - [`not-${currentChannelId}`, true], - [`not-${currentChannelId}`, false], + [currentChannelId], + [`${currentChannelId}-2`], + [`not-${currentChannelId}`], + [`not-${currentChannelId}-2`], ]; - test.each(handleSelectChannelCases)('handleSelectChannel dispatches selectChannelWithMember', async (channelId, fromPushNotification) => { + test.each(handleSelectChannelCases)('handleSelectChannel dispatches selectChannelWithMember', async (channelId) => { const testObj = {...storeObj}; testObj.entities.teams.currentTeamId = currentTeamId; store = mockStore(testObj); - await store.dispatch(handleSelectChannel(channelId, fromPushNotification)); + await store.dispatch(handleSelectChannel(channelId)); const storeActions = store.getActions(); const selectChannelWithMember = storeActions.find(({type}) => type === ChannelTypes.SELECT_CHANNEL); const viewedAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_VIEWED); @@ -315,7 +317,7 @@ describe('Actions.Views.Channel', () => { teamId: currentTeamId, }, }; - if (channelId.includes('not')) { + if (channelId.includes('not') || channelId === currentChannelId) { expect(selectChannelWithMember).toBe(undefined); } else { expect(selectChannelWithMember).toStrictEqual(expectedSelectChannelWithMember); diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 8778248c3..ba484c6ef 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -1,18 +1,21 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {GeneralTypes} from 'mattermost-redux/action_types'; +import {batchActions} from 'redux-batched-actions'; + +import {ChannelTypes, GeneralTypes, TeamTypes} from 'mattermost-redux/action_types'; import {Client4} from 'mattermost-redux/client'; import {General} from 'mattermost-redux/constants'; import {fetchMyChannelsAndMembers} from 'mattermost-redux/actions/channels'; import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from 'mattermost-redux/actions/general'; import {receivedNewPost} from 'mattermost-redux/actions/posts'; -import {getMyTeams, getMyTeamMembers, selectTeam} from 'mattermost-redux/actions/teams'; +import {getMyTeams, getMyTeamMembers} from 'mattermost-redux/actions/teams'; import {ViewTypes} from 'app/constants'; +import EphemeralStore from 'app/store/ephemeral_store'; import {recordTime} from 'app/utils/segment'; -import {handleSelectChannel} from 'app/actions/views/channel'; +import {markChannelViewedAndRead} from './channel'; export function startDataCleanup() { return async (dispatch, getState) => { @@ -79,12 +82,46 @@ export function loadFromPushNotification(notification) { await Promise.all(loading); } + dispatch(handleSelectTeamAndChannel(teamId, channelId)); + }; +} + +export function handleSelectTeamAndChannel(teamId, channelId) { + return async (dispatch, getState) => { + const dt = Date.now(); + const state = getState(); + const {channels, currentChannelId, myMembers} = state.entities.channels; + const {currentTeamId} = state.entities.teams; + const channel = channels[channelId]; + const member = myMembers[channelId]; + const actions = []; + // when the notification is from a team other than the current team if (teamId !== currentTeamId) { - dispatch(selectTeam({id: teamId})); + actions.push({type: TeamTypes.SELECT_TEAM, data: teamId}); } - dispatch(handleSelectChannel(channelId, true)); + if (channel && currentChannelId !== channelId) { + actions.push({ + type: ChannelTypes.SELECT_CHANNEL, + data: channelId, + extra: { + channel, + member, + teamId: channel.team_id || currentTeamId, + }, + }); + + dispatch(markChannelViewedAndRead(channelId)); + } + + if (actions.length) { + dispatch(batchActions(actions)); + } + + EphemeralStore.setStartFromNotification(false); + + console.log('channel switch from push notification to', channel?.display_name, (Date.now() - dt), 'ms'); //eslint-disable-line }; } diff --git a/app/push_notifications/push_notifications.ios.js b/app/push_notifications/push_notifications.ios.js index bd4ebffe8..f6b96b35f 100644 --- a/app/push_notifications/push_notifications.ios.js +++ b/app/push_notifications/push_notifications.ios.js @@ -11,7 +11,7 @@ import NotificationsIOS, { } from 'react-native-notifications'; import {getBadgeCount} from 'app/selectors/views'; -import ephemeralStore from 'app/store/ephemeral_store'; +import EphemeralStore from 'app/store/ephemeral_store'; import {getCurrentLocale} from 'app/selectors/i18n'; import {getLocalizedMessage} from 'app/i18n'; import {t} from 'app/utils/i18n'; @@ -58,7 +58,7 @@ class PushNotification { if (notification) { const data = notification.getData(); if (data) { - ephemeralStore.appStartedFromPushNotification = true; + EphemeralStore.setStartFromNotification(true); this.handleNotification(data, false, true); } } @@ -137,7 +137,7 @@ class PushNotification { // mark the app as started as soon as possible if (userInteraction) { - ephemeralStore.appStartedFromPushNotification = true; + EphemeralStore.setStartFromNotification(true); } const data = notification.getData(); diff --git a/app/screens/channel/channel_base.js b/app/screens/channel/channel_base.js index f7e908ecc..6a010e952 100644 --- a/app/screens/channel/channel_base.js +++ b/app/screens/channel/channel_base.js @@ -32,7 +32,6 @@ export default class ChannelBase extends PureComponent { static propTypes = { actions: PropTypes.shape({ loadChannelsForTeam: PropTypes.func.isRequired, - markChannelViewedAndRead: PropTypes.func.isRequired, selectDefaultTeam: PropTypes.func.isRequired, selectInitialChannel: PropTypes.func.isRequired, recordLoadTime: PropTypes.func.isRequired, @@ -87,7 +86,6 @@ export default class ChannelBase extends PureComponent { PushNotifications.clearChannelNotifications(this.props.currentChannelId); requestAnimationFrame(() => { this.props.actions.getChannelStats(this.props.currentChannelId); - this.props.actions.markChannelViewedAndRead(this.props.currentChannelId); }); } @@ -123,11 +121,9 @@ export default class ChannelBase extends PureComponent { } if (this.props.currentChannelId && this.props.currentChannelId !== prevProps.currentChannelId) { - const previousChannelId = EphemeralStore.appStartedFromPushNotification ? null : prevProps.currentChannelId; PushNotifications.clearChannelNotifications(this.props.currentChannelId); requestAnimationFrame(() => { - this.props.actions.markChannelViewedAndRead(this.props.currentChannelId, previousChannelId); this.props.actions.getChannelStats(this.props.currentChannelId); this.updateNativeScrollView(); }); @@ -207,19 +203,17 @@ export default class ChannelBase extends PureComponent { loadChannels = (teamId) => { const {loadChannelsForTeam, selectInitialChannel} = this.props.actions; - loadChannelsForTeam(teamId).then((result) => { - if (result?.error) { - this.setState({channelsRequestFailed: true}); - return; - } + if (!EphemeralStore.getStartFromNotification()) { + loadChannelsForTeam(teamId).then((result) => { + if (result?.error) { + this.setState({channelsRequestFailed: true}); + return; + } - if (EphemeralStore.appStartedFromPushNotification) { - EphemeralStore.appStartedFromPushNotification = false; - } else { this.setState({channelsRequestFailed: false}); selectInitialChannel(teamId); - } - }); + }); + } }; retryLoadChannels = () => { diff --git a/app/screens/channel/index.js b/app/screens/channel/index.js index 638da338e..a28d2aa67 100644 --- a/app/screens/channel/index.js +++ b/app/screens/channel/index.js @@ -14,7 +14,6 @@ import {getChannelStats} from 'mattermost-redux/actions/channels'; import { loadChannelsForTeam, selectInitialChannel, - markChannelViewedAndRead, } from 'app/actions/views/channel'; import {connection} from 'app/actions/device'; import {recordLoadTime} from 'app/actions/views/root'; @@ -41,7 +40,6 @@ function mapDispatchToProps(dispatch) { connection, loadChannelsForTeam, logout, - markChannelViewedAndRead, selectDefaultTeam, selectInitialChannel, recordLoadTime, diff --git a/app/store/ephemeral_store.js b/app/store/ephemeral_store.js index 3639ecdda..9e55e178a 100644 --- a/app/store/ephemeral_store.js +++ b/app/store/ephemeral_store.js @@ -45,6 +45,14 @@ class EphemeralStore { this.navigationComponentIdStack.splice(index, 1); } } + + getStartFromNotification = () => { + return this.appStartedFromPushNotification; + }; + + setStartFromNotification = (value) => { + this.appStartedFromPushNotification = value; + }; } export default new EphemeralStore(); diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index 33b5c23e4..67d83b668 100644 --- a/app/utils/push_notifications.js +++ b/app/utils/push_notifications.js @@ -47,7 +47,7 @@ class PushNotificationUtils { loadFromNotification = async (notification) => { // Set appStartedFromPushNotification to avoid channel screen to call selectInitialChannel - EphemeralStore.appStartedFromPushNotification = true; + EphemeralStore.setStartFromNotification(true); await this.store.dispatch(loadFromPushNotification(notification)); // if we have a componentId means that the app is already initialized @@ -80,11 +80,8 @@ class PushNotificationUtils { if (foreground) { EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification); } else if (userInteraction && !notification?.data?.localNotification) { - EventEmitter.emit(NavigationTypes.CLOSE_MAIN_SIDEBAR); if (getState().views.root.hydrationComplete) { //TODO: Replace when realm is ready - setTimeout(() => { - this.loadFromNotification(notification); - }, 0); + this.loadFromNotification(notification); } else { waitForHydration(this.store, () => { this.loadFromNotification(notification);