From 5b5e057bb7c29de27dbed83e6a01f6b40b09ac8e Mon Sep 17 00:00:00 2001 From: Miguel Alatzar Date: Tue, 12 Nov 2019 16:03:35 -0700 Subject: [PATCH] [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 --- app/actions/views/channel.js | 64 ++++++++++--------- app/actions/views/channel.test.js | 51 ++++++++++++--- app/actions/views/root.js | 4 +- .../__snapshots__/notification.test.js.snap | 3 + app/screens/notification/notification.test.js | 63 ++++++++++++++++++ app/utils/push_notifications.js | 2 +- 6 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 app/screens/notification/__snapshots__/notification.test.js.snap create mode 100644 app/screens/notification/notification.test.js diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 122901c02..de59441e3 100644 --- a/app/actions/views/channel.js +++ b/app/actions/views/channel.js @@ -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, + }; +} diff --git a/app/actions/views/channel.test.js b/app/actions/views/channel.test.js index 3af66604b..350e02b40 100644 --- a/app/actions/views/channel.test.js +++ b/app/actions/views/channel.test.js @@ -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); + }); }); diff --git a/app/actions/views/root.js b/app/actions/views/root.js index 8d3077c3c..6bed1877d 100644 --- a/app/actions/views/root.js +++ b/app/actions/views/root.js @@ -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)); }; } diff --git a/app/screens/notification/__snapshots__/notification.test.js.snap b/app/screens/notification/__snapshots__/notification.test.js.snap new file mode 100644 index 000000000..87b26a683 --- /dev/null +++ b/app/screens/notification/__snapshots__/notification.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Notification should match snapshot 1`] = `null`; diff --git a/app/screens/notification/notification.test.js b/app/screens/notification/notification.test.js new file mode 100644 index 000000000..002c2037d --- /dev/null +++ b/app/screens/notification/notification.test.js @@ -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( + , + ); + + expect(wrapper.getElement()).toMatchSnapshot(); + }); + + test('should call loadFromPushNotification on notification tap for non-local notification', () => { + const props = { + ...baseProps, + notification: { + localNotification: true, + }, + }; + const wrapper = shallow( + , + ); + 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); + }); +}); diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index 29e4dc274..f296c139c 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; - 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();