Fix app hanging when switching to a team after opening push notification (#4015)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Miguel Alatzar
2020-03-05 09:56:02 -07:00
committed by GitHub
parent 62adde0ad0
commit 732b301f0d
8 changed files with 81 additions and 48 deletions

View File

@@ -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
};
}

View File

@@ -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);

View File

@@ -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
};
}

View File

@@ -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();

View File

@@ -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 = () => {

View File

@@ -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,

View File

@@ -45,6 +45,14 @@ class EphemeralStore {
this.navigationComponentIdStack.splice(index, 1);
}
}
getStartFromNotification = () => {
return this.appStartedFromPushNotification;
};
setStartFromNotification = (value) => {
this.appStartedFromPushNotification = value;
};
}
export default new EphemeralStore();

View File

@@ -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);