Merge branch 'master' into mark-as-unread

This commit is contained in:
Harrison Healey 2019-11-13 19:34:06 -05:00
commit ef162ce403
15 changed files with 175 additions and 55 deletions

View file

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

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),
];
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,
};
}

View file

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

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

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

View file

@ -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(
<NotificationSettings {...baseProps}/>
);
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,
},
});

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

View file

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

View file

@ -34,7 +34,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>243</string>
<string>245</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.25.0</string>
<key>CFBundleVersion</key>
<string>243</string>
<string>245</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>

View file

@ -19,6 +19,6 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>243</string>
<string>245</string>
</dict>
</plist>

View file

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.25.0</string>
<key>CFBundleVersion</key>
<string>243</string>
<string>245</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>

View file

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