diff --git a/android/app/build.gradle b/android/app/build.gradle index 35cd1ccb1..2f38a30ed 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -140,7 +140,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion missingDimensionStrategy "RNN.reactNativeVersion", "reactNative60" - versionCode 243 + versionCode 245 versionName "1.25.0" multiDexEnabled = true ndk { diff --git a/app/actions/views/channel.js b/app/actions/views/channel.js index 122901c02..4d89dcd77 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), ]; - 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)); + dispatch(markChannelViewedAndRead(channelId, previousChannelId)); }; } @@ -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..de26b10c7 100644 --- a/app/actions/views/channel.test.js +++ b/app/actions/views/channel.test.js @@ -5,27 +5,27 @@ 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: {}}}), -})); +const MOCK_CHANNEL_MARK_AS_READ = 'MOCK_CHANNEL_MARK_AS_READ'; +const MOCK_CHANNEL_MARK_AS_VIEWED = 'MOCK_CHANNEL_MARK_AS_VIEWED'; 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: 'MOCK_CHANNEL_MARK_AS_READ'}), + markChannelAsViewed: jest.fn().mockReturnValue({type: 'MOCK_CHANNEL_MARK_AS_VIEWED'}), }; }); @@ -130,6 +130,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 +243,38 @@ 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 storeActions = store.getActions(); + const storeBatchActions = storeActions.find(({type}) => type === 'BATCHING_REDUCER.BATCH'); + const selectChannelWithMember = storeBatchActions.payload.find(({type}) => type === ViewTypes.SELECT_CHANNEL_WITH_MEMBER); + const viewedAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_VIEWED); + const readAction = storeActions.find(({type}) => type === MOCK_CHANNEL_MARK_AS_READ); + + const expectedSelectChannelWithMember = { + type: ViewTypes.SELECT_CHANNEL_WITH_MEMBER, + data: channelId, + channel: { + data: channelId, + }, + member: { + data: { + member: {}, + }, + }, + + }; + expect(selectChannelWithMember).toStrictEqual(expectedSelectChannelWithMember); + expect(viewedAction).not.toBe(null); + expect(readAction).not.toBe(null); + }); }); 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..b89a5c4c1 --- /dev/null +++ b/app/screens/notification/notification.test.js @@ -0,0 +1,60 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React from 'react'; +import {shallow} from 'enzyme'; + +import Preferences from 'mattermost-redux/constants/preferences'; + +import Notification from './notification.js'; + +jest.mock('react-native-navigation', () => ({ + Navigation: { + events: jest.fn(() => ({ + registerComponentDidDisappearListener: jest.fn(), + })), + }, +})); + +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/screens/settings/notification_settings/notification_settings.js b/app/screens/settings/notification_settings/notification_settings.js index 9c832cd07..a878b43ea 100644 --- a/app/screens/settings/notification_settings/notification_settings.js +++ b/app/screens/settings/notification_settings/notification_settings.js @@ -134,12 +134,13 @@ export default class NotificationSettings extends PureComponent { saveNotificationProps = (notifyProps) => { const {currentUser} = this.props; + const prevProps = getNotificationProps(currentUser); const updatedProps = { - ...getNotificationProps(currentUser), + ...prevProps, ...notifyProps, }; - if (!deepEqual(updatedProps, notifyProps)) { + if (!deepEqual(prevProps, notifyProps)) { this.props.actions.updateMe({notify_props: updatedProps}); } }; diff --git a/app/screens/settings/notification_settings/notification_settings.test.js b/app/screens/settings/notification_settings/notification_settings.test.js index 96cacca59..863a966f6 100644 --- a/app/screens/settings/notification_settings/notification_settings.test.js +++ b/app/screens/settings/notification_settings/notification_settings.test.js @@ -9,13 +9,16 @@ import {shallowWithIntl} from 'test/intl-test-helper'; import NotificationSettings from './notification_settings.js'; +import {getNotificationProps} from 'app/utils/notify_props'; + describe('NotificationSettings', () => { + const currentUser = {id: 'current_user_id'}; const baseProps = { actions: { updateMe: jest.fn(), }, componentId: 'component-id', - currentUser: {id: 'current_user_id'}, + currentUser, theme: Preferences.THEMES.default, updateMeRequest: {}, currentUserStatus: 'status', @@ -32,17 +35,22 @@ describe('NotificationSettings', () => { }); test('should include previous notification props when saving new ones', () => { - baseProps.currentUser.notify_props = {previous: 'previous'}; const wrapper = shallowWithIntl( ); const instance = wrapper.instance(); + + const defaultNotifyProps = getNotificationProps(currentUser); + instance.saveNotificationProps(defaultNotifyProps); + expect(baseProps.actions.updateMe).toHaveBeenCalledTimes(0); + const newProps = {new: 'new'}; instance.saveNotificationProps(newProps); + expect(baseProps.actions.updateMe).toHaveBeenCalledTimes(1); expect(baseProps.actions.updateMe).toHaveBeenCalledWith({ notify_props: { - ...baseProps.currentUser.notify_props, + ...defaultNotifyProps, ...newProps, }, }); diff --git a/app/utils/push_notifications.js b/app/utils/push_notifications.js index 94f1dd674..6d8b480ea 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(); diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index 29bc87906..5b90b036a 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -1060,7 +1060,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 243; + CURRENT_PROJECT_VERSION = 245; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; @@ -1098,7 +1098,7 @@ CODE_SIGN_ENTITLEMENTS = Mattermost/Mattermost.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 243; + CURRENT_PROJECT_VERSION = 245; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = UQ8HT4Q2XM; ENABLE_BITCODE = NO; diff --git a/ios/Mattermost/Info.plist b/ios/Mattermost/Info.plist index 4fd49fdc3..f15b3e80a 100644 --- a/ios/Mattermost/Info.plist +++ b/ios/Mattermost/Info.plist @@ -34,7 +34,7 @@ CFBundleVersion - 243 + 245 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/ios/MattermostShare/Info.plist b/ios/MattermostShare/Info.plist index 2aa2a57c8..15f3ed814 100644 --- a/ios/MattermostShare/Info.plist +++ b/ios/MattermostShare/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.25.0 CFBundleVersion - 243 + 245 NSAppTransportSecurity NSAllowsArbitraryLoads diff --git a/ios/MattermostTests/Info.plist b/ios/MattermostTests/Info.plist index ae0cd033f..fe65b8357 100644 --- a/ios/MattermostTests/Info.plist +++ b/ios/MattermostTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 243 + 245 diff --git a/ios/NotificationService/Info.plist b/ios/NotificationService/Info.plist index 47766f21b..541cacf20 100644 --- a/ios/NotificationService/Info.plist +++ b/ios/NotificationService/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.25.0 CFBundleVersion - 243 + 245 NSExtension NSExtensionPointIdentifier diff --git a/test/setup.js b/test/setup.js index 73bb310d6..1833f6182 100644 --- a/test/setup.js +++ b/test/setup.js @@ -23,6 +23,7 @@ jest.doMock('react-native', () => { ImagePickerManager, requireNativeComponent, Alert: RNAlert, + InteractionManager: RNInteractionManager, NativeModules: RNNativeModules, } = ReactNative; @@ -31,6 +32,11 @@ jest.doMock('react-native', () => { alert: jest.fn(), }; + const InteractionManager = { + ...RNInteractionManager, + runAfterInteractions: jest.fn((cb) => cb()), + }; + const NativeModules = { ...RNNativeModules, UIManager: { @@ -87,6 +93,7 @@ jest.doMock('react-native', () => { ImagePickerManager, requireNativeComponent, Alert, + InteractionManager, NativeModules, }, ReactNative); });