[MM-19968] Ensure SELECT_CHANNEL_WITH_MEMBER is dispatched when app opened from a push notification (#3533) (#3547)

* Dispatch SELECT_CHANNEL_WITH_MEMBER

* Call handleSelectChannel with true when from any push notification

* Always dispatch SELECT_CHANNEL_WITH_MEMBER with passed channelId

* Add missing comma

* Update unit tests

* Fix makrChannelViewedAndRead call
This commit is contained in:
Miguel Alatzar 2019-11-12 16:03:35 -07:00 committed by GitHub
parent 15c34ea486
commit 5b5e057bb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 143 additions and 44 deletions

View file

@ -379,45 +379,23 @@ export function handleSelectChannel(channelId, fromPushNotification = false) {
dispatch(loadPostsIfNecessaryWithRetry(channelId));
}
let previousChannelId;
if (!fromPushNotification && !sameChannel) {
previousChannelId = currentChannelId;
}
const actions = [
selectChannel(channelId),
getChannelStats(channelId),
setChannelDisplayName(channel.display_name),
{
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId,
},
setInitialPostVisibility(channelId),
setChannelLoading(false),
{
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId: currentTeamId,
channelId,
},
setLastChannelForTeam(currentTeamId, channelId),
selectChannelWithMember(channelId, channel, member),
markChannelViewedAndRead(channelId, previousChannelId),
];
let markPreviousChannelId;
if (!fromPushNotification && !sameChannel) {
markPreviousChannelId = currentChannelId;
actions.push({
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: currentChannelId,
channel: getChannel(state, currentChannelId),
member: getMyChannelMember(state, currentChannelId),
});
}
if (!fromPushNotification) {
actions.push({
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
channel,
member,
});
}
dispatch(batchActions(actions));
dispatch(markChannelViewedAndRead(channelId, markPreviousChannelId));
};
}
@ -686,3 +664,27 @@ function setLoadMorePostsVisible(visible) {
data: visible,
};
}
function setInitialPostVisibility(channelId) {
return {
type: ViewTypes.SET_INITIAL_POST_VISIBILITY,
data: channelId,
};
}
function setLastChannelForTeam(teamId, channelId) {
return {
type: ViewTypes.SET_LAST_CHANNEL_FOR_TEAM,
teamId,
channelId,
};
}
function selectChannelWithMember(channelId, channel, member) {
return {
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
channel,
member,
};
}

View file

@ -5,27 +5,24 @@ import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import initialState from 'app/initial_state';
import {ViewTypes} from 'app/constants';
import testHelper from 'test/test_helper';
import {
import * as ChannelActions from 'app/actions/views/channel';
const {
handleSelectChannel,
handleSelectChannelByName,
loadPostsIfNecessaryWithRetry,
} from 'app/actions/views/channel';
} = ChannelActions;
import postReducer from 'mattermost-redux/reducers/entities/posts';
jest.mock('mattermost-redux/selectors/entities/channels', () => ({
getChannel: () => ({data: 'received-channel-id'}),
getCurrentChannelId: () => 'current-channel-id',
getMyChannelMember: () => ({data: {member: {}}}),
}));
jest.mock('mattermost-redux/actions/channels', () => {
const channelActions = require.requireActual('mattermost-redux/actions/channels');
return {
...channelActions,
markChannelAsRead: jest.fn(),
markChannelAsViewed: jest.fn(),
markChannelAsRead: jest.fn().mockReturnValue({type: ''}),
markChannelAsViewed: jest.fn().mockReturnValue({type: ''}),
};
});
@ -130,6 +127,11 @@ describe('Actions.Views.Channel', () => {
},
};
const channelSelectors = require('mattermost-redux/selectors/entities/channels');
channelSelectors.getChannel = jest.fn((state, channelId) => ({data: channelId}));
channelSelectors.getCurrentChannelId = jest.fn(() => currentChannelId);
channelSelectors.getMyChannelMember = jest.fn(() => ({data: {member: {}}}));
test('handleSelectChannelByName success', async () => {
store = mockStore(storeObj);
@ -238,4 +240,33 @@ describe('Actions.Views.Channel', () => {
expect(postActions.getPostsSince).toHaveBeenCalledWith(currentChannelId, store.getState().views.channel.lastGetPosts[currentChannelId]);
expect(receivedPostsSince).not.toBe(null);
});
const handleSelectChannelCases = [
[currentChannelId, true],
[currentChannelId, false],
[`not-${currentChannelId}`, true],
[`not-${currentChannelId}`, false],
];
test.each(handleSelectChannelCases)('handleSelectChannel dispatches selectChannelWithMember', async (channelId, fromPushNotification) => {
store = mockStore({...storeObj});
await store.dispatch(handleSelectChannel(channelId, fromPushNotification));
const storeBatchActions = store.getActions().find(({type}) => type === 'BATCHING_REDUCER.BATCH');
const selectChannelWithMember = storeBatchActions.payload.find(({type}) => type === ViewTypes.SELECT_CHANNEL_WITH_MEMBER);
const expectedSelectChannelWithMember = {
type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER,
data: channelId,
channel: {
data: channelId,
},
member: {
data: {
member: {},
},
},
};
expect(selectChannelWithMember).toStrictEqual(expectedSelectChannelWithMember);
});
});

View file

@ -47,7 +47,7 @@ export function loadConfigAndLicense() {
};
}
export function loadFromPushNotification(notification, startAppFromPushNotification) {
export function loadFromPushNotification(notification) {
return async (dispatch, getState) => {
const state = getState();
const {data} = notification;
@ -84,7 +84,7 @@ export function loadFromPushNotification(notification, startAppFromPushNotificat
dispatch(selectTeam({id: teamId}));
}
dispatch(handleSelectChannel(channelId, startAppFromPushNotification));
dispatch(handleSelectChannel(channelId, true));
};
}

View file

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Notification should match snapshot 1`] = `null`;

View file

@ -0,0 +1,63 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {InteractionManager} from 'react-native';
import Preferences from 'mattermost-redux/constants/preferences';
import Notification from './notification.js';
jest.mock('react-native-navigation', () => ({
Navigation: {
events: jest.fn(() => ({
registerComponentDidDisappearListener: jest.fn(),
})),
},
}));
InteractionManager.runAfterInteractions = jest.fn((cb) => cb());
describe('Notification', () => {
const baseProps = {
actions: {
loadFromPushNotification: jest.fn(),
},
componentId: 'component-id',
deviceWidth: 100,
notification: {},
theme: Preferences.THEMES.default,
};
test('should match snapshot', () => {
const wrapper = shallow(
<Notification {...baseProps}/>,
);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should call loadFromPushNotification on notification tap for non-local notification', () => {
const props = {
...baseProps,
notification: {
localNotification: true,
},
};
const wrapper = shallow(
<Notification {...props}/>,
);
const instance = wrapper.instance();
instance.notificationTapped();
expect(baseProps.actions.loadFromPushNotification).not.toHaveBeenCalled();
const notification = {
localNotification: false,
};
wrapper.setProps({notification});
instance.notificationTapped();
expect(baseProps.actions.loadFromPushNotification).toHaveBeenCalledWith(notification);
});
});

View file

@ -47,7 +47,7 @@ class PushNotificationUtils {
loadFromNotification = async (notification) => {
// Set appStartedFromPushNotification to avoid channel screen to call selectInitialChannel
EphemeralStore.appStartedFromPushNotification = true;
await this.store.dispatch(loadFromPushNotification(notification, true));
await this.store.dispatch(loadFromPushNotification(notification));
// if we have a componentId means that the app is already initialized
const componentId = EphemeralStore.getNavigationTopComponentId();