[MM-23698] [MM-19559] Remove redux-offline and updated redux-persist (#4120)

* Remove redux-offline and configure redux-persist

* Fix typo

* Fix configure store

* Upgrade to redux-persist 6.0.0

* Add migration from redux-persist v4 to v6

* Replace AsyncStorage with MMKVStorage to boost storage speed

* Mock RNFastStorage

* Fix reactions test

* Fix clearing the store on logout

* Remove the need for LOGOUT_SUCCESS

* No need to pass persistConfig to middlewares()

* Remove unused imports

* Export connection

Accidentally removed this export.

* Add batch action name

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>

* Add batch action name

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>

* Add batch action name

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>

* Add batch action name

Co-Authored-By: Elias Nahum <nahumhbl@gmail.com>

* Fix delete post

* Fix leave channel

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Miguel Alatzar 2020-04-08 13:44:54 -07:00 committed by GitHub
parent 8bb43ca4c2
commit 7afa9aed01
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 876 additions and 1454 deletions

1
.gitignore vendored
View file

@ -87,6 +87,7 @@ ios/sentry.properties
# Testing
.nyc_output
coverage
.tmp
# Bundle artifact
*.jsbundle

View file

@ -2410,41 +2410,6 @@ SOFTWARE.
---
## redux-offline
This product contains 'redux-offline' by redux-offline.
Build Offline-First Apps for Web and React Native
* HOMEPAGE:
* https://github.com/redux-offline/redux-offline
* LICENSE: MIT
MIT License
Copyright (c) 2017 Jani Eväkallio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
## redux-persist
This product contains 'redux-persist' by Zack Story.

View file

@ -1,21 +1,16 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {networkStatusChangedAction} from 'redux-offline';
import {batchActions} from 'redux-batched-actions';
import {DeviceTypes} from 'app/constants';
export function connection(isOnline) {
return async (dispatch, getState) => {
const state = getState();
if (isOnline !== undefined && isOnline !== state.device.connection) { //eslint-disable-line no-undefined
dispatch(batchActions([
networkStatusChangedAction(isOnline), {
type: DeviceTypes.CONNECTION_CHANGED,
data: isOnline,
},
], 'BATCH_CONNECTION_CHANGED'));
dispatch({
type: DeviceTypes.CONNECTION_CHANGED,
data: isOnline,
});
}
};
}

View file

@ -4,6 +4,7 @@
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {Client4} from '@mm-redux/client';
import * as GeneralActions from '@mm-redux/actions/general';
import {handleSuccessfulLogin} from 'app/actions/views/login';
@ -46,8 +47,8 @@ describe('Actions.Views.Login', () => {
});
test('handleSuccessfulLogin gets config and license ', async () => {
const getClientConfig = jest.spyOn(GeneralActions, 'getClientConfig');
const getLicenseConfig = jest.spyOn(GeneralActions, 'getLicenseConfig');
const getClientConfig = jest.spyOn(Client4, 'getClientConfigOld');
const getLicenseConfig = jest.spyOn(Client4, 'getClientLicenseOld');
await store.dispatch(handleSuccessfulLogin());
expect(getClientConfig).toHaveBeenCalled();

View file

@ -4,15 +4,19 @@
import {batchActions} from 'redux-batched-actions';
import {ChannelTypes, GeneralTypes, TeamTypes} from '@mm-redux/action_types';
import {Client4} from '@mm-redux/client';
import {General} from '@mm-redux/constants';
import {fetchMyChannelsAndMembers} from '@mm-redux/actions/channels';
import {getClientConfig, getDataRetentionPolicy, getLicenseConfig} from '@mm-redux/actions/general';
import {receivedNewPost} from '@mm-redux/actions/posts';
import {getMyTeams, getMyTeamMembers} from '@mm-redux/actions/teams';
import {Client4} from '@mm-redux/client';
import {General} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {ViewTypes} from 'app/constants';
import {NavigationTypes, ViewTypes} from '@constants';
import initialState from 'app/initial_state';
import {persistor} from 'app/store';
import EphemeralStore from 'app/store/ephemeral_store';
import {getStateForReset} from 'app/store/utils';
import {recordTime} from 'app/utils/segment';
import {markChannelViewedAndRead} from './channel';
@ -29,24 +33,36 @@ export function startDataCleanup() {
export function loadConfigAndLicense() {
return async (dispatch, getState) => {
const {currentUserId} = getState().entities.users;
const [configData, licenseData] = await Promise.all([
getClientConfig()(dispatch, getState),
getLicenseConfig()(dispatch, getState),
]);
const config = configData.data || {};
const license = licenseData.data || {};
try {
const [config, license] = await Promise.all([
Client4.getClientConfigOld(),
Client4.getClientLicenseOld(),
]);
if (currentUserId) {
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
dispatch(getDataRetentionPolicy());
} else {
dispatch({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
const actions = [{
type: GeneralTypes.CLIENT_CONFIG_RECEIVED,
data: config,
}, {
type: GeneralTypes.CLIENT_LICENSE_RECEIVED,
data: license,
}];
if (currentUserId) {
if (config.DataRetentionEnableMessageDeletion && config.DataRetentionEnableMessageDeletion === 'true' &&
license.IsLicensed === 'true' && license.DataRetention === 'true') {
dispatch(getDataRetentionPolicy());
} else {
actions.push({type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, data: {}});
}
}
}
return {config, license};
dispatch(batchActions(actions, 'BATCH_LOAD_CONFIG_AND_LICENSE'));
return {config, license};
} catch (error) {
return {error};
}
};
}
@ -126,7 +142,18 @@ export function handleSelectTeamAndChannel(teamId, channelId) {
}
export function purgeOfflineStore() {
return {type: General.OFFLINE_STORE_PURGE};
return (dispatch, getState) => {
const currentState = getState();
persistor.purge();
dispatch({
type: General.OFFLINE_STORE_PURGE,
state: getStateForReset(initialState, currentState),
});
persistor.flush();
persistor.persist();
EventEmitter.emit(NavigationTypes.RESTART_APP);
};
}
// A non-optimistic version of the createPost action in app/mm-redux with the file handling

View file

@ -3,12 +3,14 @@
import {batchActions} from 'redux-batched-actions';
import {NavigationTypes} from 'app/constants';
import {GeneralTypes, RoleTypes, UserTypes} from '@mm-redux/action_types';
import {getDataRetentionPolicy} from '@mm-redux/actions/general';
import * as HelperActions from '@mm-redux/actions/helpers';
import {autoUpdateTimezone} from '@mm-redux/actions/timezone';
import {Client4} from '@mm-redux/client';
import {General} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {getConfig, getLicense} from '@mm-redux/selectors/entities/general';
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
import {getCurrentUserId, getStatusForUserId} from '@mm-redux/selectors/entities/users';
@ -194,7 +196,7 @@ export function ssoLogin(token) {
}
export function logout(skipServerLogout = false) {
return async (dispatch) => {
return async () => {
if (!skipServerLogout) {
try {
Client4.logout();
@ -203,7 +205,8 @@ export function logout(skipServerLogout = false) {
}
}
dispatch({type: UserTypes.LOGOUT_SUCCESS});
EventEmitter.emit(NavigationTypes.NAVIGATION_RESET);
return {data: true};
};
}

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React from 'react';
import {shallow} from 'enzyme';
import {shallowWithIntl} from 'test/intl-test-helper.js';
import Reactions from './reactions';
@ -31,12 +31,12 @@ describe('Reactions', () => {
};
test('should match snapshot', () => {
const wrapper = shallow(<Reactions {...baseProps}/>);
const wrapper = shallowWithIntl(<Reactions {...baseProps}/>);
expect(wrapper.getElement()).toMatchSnapshot();
});
test('should match snapshot with canAddReaction = false', () => {
const wrapper = shallow(
const wrapper = shallowWithIntl(
<Reactions
{...baseProps}
canAddReaction={false}

View file

@ -78,7 +78,7 @@ function makeMapStateToProps() {
isArchived,
isChannelMuted: isChannelMuted(member),
currentUserId,
hasDraft: Boolean(channelDraft.draft.trim() || channelDraft.files.length),
hasDraft: Boolean(channelDraft.draft.trim() || channelDraft?.files?.length),
mentions: member ? member.mention_count : 0,
shouldHideChannel,
showUnreadForMsgs,

View file

@ -15,27 +15,27 @@ import {close as closeWebSocket} from '@actions/websocket';
import {GeneralTypes} from '@mm-redux/action_types';
import {Client4} from '@mm-redux/client';
import {General} from '@mm-redux/constants';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {getCurrentChannelId} from '@mm-redux/selectors/entities/channels';
import {getCurrentUserId, getUser} from '@mm-redux/selectors/entities/users';
import {isTimezoneEnabled} from '@mm-redux/selectors/entities/timezone';
import EventEmitter from '@mm-redux/utils/event_emitter';
import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from '@actions/device';
import {selectDefaultChannel} from '@actions/views/channel';
import {showOverlay} from '@actions/navigation';
import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from '@actions/views/root';
import {loadMe, logout} from '@actions/views/user';
import {NavigationTypes, ViewTypes} from '@constants';
import {getTranslations, resetMomentLocale} from '@i18n';
import PushNotifications from 'app/push_notifications';
import {getCurrentLocale} from '@selectors/i18n';
import {t} from '@utils/i18n';
import {deleteFileCache} from '@utils/file';
import {getDeviceTimezoneAsync} from '@utils/timezone';
import {setDeviceDimensions, setDeviceOrientation, setDeviceAsTablet, setStatusBarHeight} from 'app/actions/device';
import {selectDefaultChannel} from 'app/actions/views/channel';
import {showOverlay} from 'app/actions/navigation';
import {loadConfigAndLicense, setDeepLinkURL, startDataCleanup} from 'app/actions/views/root';
import {loadMe, logout} from 'app/actions/views/user';
import {NavigationTypes, ViewTypes} from 'app/constants';
import {getTranslations, resetMomentLocale} from 'app/i18n';
import initialState from 'app/initial_state';
import mattermostBucket from 'app/mattermost_bucket';
import mattermostManaged from 'app/mattermost_managed';
import PushNotifications from 'app/push_notifications';
import {getCurrentLocale} from 'app/selectors/i18n';
import {t} from 'app/utils/i18n';
import {deleteFileCache} from 'app/utils/file';
import {getDeviceTimezoneAsync} from 'app/utils/timezone';
import LocalConfig from 'assets/config';
import {getAppCredentials, removeAppCredentials} from './credentials';
@ -274,23 +274,32 @@ class GlobalEventHandler {
try {
await AsyncStorage.clear();
const state = this.store.getState();
this.store.dispatch(batchActions([
{
type: General.OFFLINE_STORE_RESET,
data: initialState,
const newState = {
...initialState,
entities: {
...initialState.entities,
general: {
...initialState.entities.general,
deviceToken: state.entities.general.deviceToken,
},
},
{
type: General.STORE_REHYDRATION_COMPLETE,
views: {
i18n: {
locale: DeviceInfo.getDeviceLocale().split('-')[0],
},
root: {
hydrationComplete: true,
},
selectServer: {
serverUrl: state.views.selectServer.serverUrl,
},
},
{
type: ViewTypes.SERVER_URL_CHANGED,
serverUrl: state.entities.general.credentials.url || state.views.selectServer.serverUrl,
},
{
type: GeneralTypes.RECEIVED_APP_DEVICE_TOKEN,
data: state.entities.general.deviceToken,
},
], 'BATCH_RESET_STATE'));
};
this.store.dispatch({
type: General.OFFLINE_STORE_PURGE,
state: newState,
});
} catch (e) {
// clear error
}

View file

@ -4,6 +4,10 @@
import Config from 'assets/config.json';
const state = {
app: {
version: '',
build: '',
},
entities: {
general: {
appState: false,
@ -111,10 +115,6 @@ const state = {
status: 'not_started',
error: null,
},
logout: {
status: 'not_started',
error: null,
},
autocompleteUsers: {
status: 'not_started',
error: null,
@ -128,7 +128,6 @@ const state = {
device: {
connection: true,
},
navigation: '',
views: {
channel: {
drafts: {},

View file

@ -18,7 +18,7 @@ import 'app/init/device';
import 'app/init/fetch';
import globalEventHandler from 'app/init/global_event_handler';
import {registerScreens} from 'app/screens';
import store from 'app/store';
import store, {persistor} from 'app/store';
import {waitForHydration} from 'app/store/utils';
import EphemeralStore from 'app/store/ephemeral_store';
import telemetry from 'app/telemetry';

View file

@ -13,7 +13,6 @@ export default keyMirror({
LOGIN_FAILURE: null,
LOGOUT_REQUEST: null,
LOGOUT_SUCCESS: null,
LOGOUT_FAILURE: null,
REVOKE_ALL_USER_SESSIONS_SUCCESS: null,

View file

@ -130,23 +130,4 @@ describe('Actions.Bots', () => {
const botsResult = state.entities.bots.accounts[bot.user_id];
assert.equal(bot.owner_id, botsResult.owner_id);
});
it('logout', async () => {
// Fill redux store with somthing
const bot = TestHelper.fakeBot();
nock(Client4.getBaseRoute()).
post('/bots').
reply(200, bot);
await store.dispatch(BotActions.createBot(bot));
// Should be cleared by logout
nock(Client4.getUsersRoute()).
post('/logout').
reply(200, {status: 'OK'});
await store.dispatch(UserActions.logout());
// Check is clear
const state = store.getState();
assert.equal(0, Object.keys(state.entities.bots.accounts).length);
});
});

View file

@ -23,7 +23,14 @@ describe('Actions.Channels', () => {
});
beforeEach(async () => {
store = await configureStore();
const initialState = {
entities: {
users: {
currentUserId: TestHelper.basicUser.id,
},
},
};
store = await configureStore(initialState);
});
afterAll(async () => {
@ -1954,52 +1961,38 @@ describe('Actions.Channels', () => {
assert.deepEqual(channel.purpose, purpose);
});
it('leaveChannel', (done) => {
async function test() {
TestHelper.mockLogin();
await store.dispatch(login(TestHelper.basicUser.email, 'password1'));
nock(Client4.getBaseRoute()).
get(`/channels/${TestHelper.basicChannel.id}`).
reply(200, TestHelper.basicChannel);
it('leaveChannel', async () => {
TestHelper.mockLogin();
await store.dispatch(login(TestHelper.basicUser.email, 'password1'));
nock(Client4.getBaseRoute()).
get(`/channels/${TestHelper.basicChannel.id}`).
reply(200, TestHelper.basicChannel);
nock(Client4.getBaseRoute()).
post(`/channels/${TestHelper.basicChannel.id}/members`).
reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: TestHelper.basicUser.id});
nock(Client4.getBaseRoute()).
post(`/channels/${TestHelper.basicChannel.id}/members`).
reply(201, {channel_id: TestHelper.basicChannel.id, roles: 'channel_user', user_id: TestHelper.basicUser.id});
await store.dispatch(Actions.joinChannel(TestHelper.basicUser.id, TestHelper.basicTeam.id, TestHelper.basicChannel.id));
await store.dispatch(Actions.joinChannel(TestHelper.basicUser.id, TestHelper.basicTeam.id, TestHelper.basicChannel.id));
const {channels, myMembers} = store.getState().entities.channels;
assert.ok(channels[TestHelper.basicChannel.id]);
assert.ok(myMembers[TestHelper.basicChannel.id]);
let channelsState = store.getState().entities.channels;
assert.ok(channelsState.channels[TestHelper.basicChannel.id]);
assert.ok(channelsState.myMembers[TestHelper.basicChannel.id]);
nock(Client4.getBaseRoute()).
delete(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}`).
reply(400, {});
nock(Client4.getBaseRoute()).
delete(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}`).
reply(200, OK_RESPONSE);
nock(Client4.getBaseRoute()).
delete(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}`).
reply(200, OK_RESPONSE);
await store.dispatch(Actions.leaveChannel(TestHelper.basicChannel.id));
// This action will retry after 1000ms
await store.dispatch(Actions.leaveChannel(TestHelper.basicChannel.id));
channelsState = store.getState().entities.channels;
setTimeout(test2, 300);
}
async function test2() {
// retry will have completed and should have left the channel successfully
const {channels, myMembers} = store.getState().entities.channels;
assert.ok(channels[TestHelper.basicChannel.id]);
assert.ifError(myMembers[TestHelper.basicChannel.id]);
done();
}
test();
assert.ok(channelsState.channels[TestHelper.basicChannel.id]);
assert.ifError(channelsState.myMembers[TestHelper.basicChannel.id]);
});
it('leave private channel', async () => {
const newChannel = {
...TestHelper.fakeChannelWithId(TestHelper.basicChannel.id),
team_id: TestHelper.basicTeam.id,
name: 'redux-test-private',
display_name: 'Redux Test',
@ -2010,14 +2003,14 @@ describe('Actions.Channels', () => {
nock(Client4.getBaseRoute()).
post('/channels').
reply(201, {...TestHelper.fakeChannelWithId(TestHelper.basicTeam.id), ...newChannel});
reply(201, newChannel);
const {data: channel} = await store.dispatch(Actions.createChannel(newChannel, TestHelper.basicUser.id));
let channels = store.getState().entities.channels.channels;
assert.ok(channels[channel.id]);
nock(Client4.getBaseRoute()).
delete(`/channels/${TestHelper.basicChannel.id}/members/${TestHelper.basicUser.id}`).
delete(`/channels/${channel.id}/members/${TestHelper.basicUser.id}`).
reply(200, OK_RESPONSE);
await store.dispatch(Actions.leaveChannel(channel.id));

View file

@ -636,26 +636,23 @@ export function leaveChannel(channelId: string): ActionFunc {
team_id: channel.team_id,
type: channel.type,
},
meta: {
offline: {
effect: () => Client4.removeFromChannel(currentUserId, channelId),
commit: {type: 'do_nothing'}, // redux-offline always needs to dispatch something on commit
rollback: () => {
dispatch(batchActions([
{
type: ChannelTypes.RECEIVED_CHANNEL,
data: channel,
},
{
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: member,
},
]));
},
},
},
});
try {
await Client4.removeFromChannel(currentUserId, channelId);
} catch (error) {
dispatch(batchActions([
{
type: ChannelTypes.RECEIVED_CHANNEL,
data: channel,
},
{
type: ChannelTypes.RECEIVED_MY_CHANNEL_MEMBER,
data: member,
},
]));
}
return {data: true};
};
}
@ -1399,7 +1396,7 @@ export function favoriteChannel(channelId: string): ActionFunc {
Client4.trackEvent('action', 'action_channels_favorite');
return savePreferences(currentUserId, [preference])(dispatch);
return dispatch(savePreferences(currentUserId, [preference]));
};
}

View file

@ -60,28 +60,6 @@ describe('Actions.Helpers', () => {
assert.deepEqual(dispatch.actions, []);
});
it('should trigger logout when passed a 401 server error', async () => {
const store = await configureStore({
entities: {
users: {
currentUserId: 'user',
},
},
});
const dispatch = mockDispatch(store.dispatch);
const error = new ClientError(Client4.getUrl(), {
message: 'Failed to do something',
status_code: 401,
url: '/api/v4/foo/bar',
});
forceLogoutIfNecessary(error, dispatch, store.getState);
assert.notEqual(Client4.token, token);
assert.deepEqual(dispatch.actions, [{type: UserTypes.LOGOUT_SUCCESS, data: {}}]);
});
it('should do nothing when failing to log in', async () => {
const store = await configureStore({
entities: {

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from '@mm-redux/client';
import {UserTypes} from '@mm-redux/action_types';
import {logout} from '@actions/views/user';
import {Client4Error} from '@mm-redux/types/client4';
import {batchActions, Action, ActionFunc, GenericAction, DispatchFunc, GetStateFunc} from '@mm-redux/types/actions';
@ -13,8 +13,7 @@ export function forceLogoutIfNecessary(err: Client4Error, dispatch: DispatchFunc
const {currentUserId} = getState().entities.users;
if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err.url && err.url.indexOf('/login') === -1 && currentUserId) {
Client4.setToken('');
dispatch({type: UserTypes.LOGOUT_SUCCESS, data: {}});
dispatch(logout(false));
}
}

View file

@ -206,6 +206,10 @@ describe('Actions.Posts', () => {
await Actions.createPost(TestHelper.fakePost(channelId))(store.dispatch, store.getState);
const initialPosts = store.getState().entities.posts;
const postId = Object.keys(initialPosts.posts)[0];
nock(Client4.getBaseRoute()).
delete(`/posts/${postId}`).
reply(200, {});
await Actions.deletePost(initialPosts.posts[postId])(store.dispatch, store.getState);
const state = store.getState();
@ -243,6 +247,9 @@ describe('Actions.Posts', () => {
assert.ok(reactions[post1.id]);
assert.ok(reactions[post1.id][TestHelper.basicUser.id + '-' + emojiName]);
nock(Client4.getBaseRoute()).
delete(`/posts/${post1.id}`).
reply(200, {});
await Actions.deletePost(post1)(store.dispatch, store.getState);
reactions = store.getState().entities.posts.reactions;

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {Client4, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE} from '@mm-redux/client';
import {General, Preferences, Posts} from '../constants';
import {General, Preferences, Posts} from '@mm-redux/constants';
import {WebsocketEvents} from '@constants';
import {PostTypes, ChannelTypes, FileTypes, IntegrationTypes} from '@mm-redux/action_types';
@ -33,7 +33,6 @@ import {Action, ActionResult, batchActions, DispatchFunc, GetStateFunc, GenericA
import {ChannelUnread} from '@mm-redux/types/channels';
import {GlobalState} from '@mm-redux/types/store';
import {Post} from '@mm-redux/types/posts';
import {Error} from '@mm-redux/types/errors';
import {Reaction} from '@mm-redux/types/reactions';
import {UserProfile} from '@mm-redux/types/users';
import {Dictionary} from '@mm-redux/types/utilities';
@ -169,9 +168,9 @@ export function createPost(post: Post, files: any[] = []) {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const currentUserId = state.entities.users.currentUserId;
const timestamp = Date.now();
const pendingPostId = post.pending_post_id || `${currentUserId}:${timestamp}`;
let actions: Array<Action> = [];
if (Selectors.isPostIdSending(state, pendingPostId)) {
return {data: true};
@ -197,79 +196,78 @@ export function createPost(post: Post, files: any[] = []) {
file_ids: fileIds,
};
dispatch({
actions.push({
type: FileTypes.RECEIVED_FILES_FOR_POST,
postId: pendingPostId,
data: files,
});
}
dispatch({
actions.push({
type: PostTypes.RECEIVED_NEW_POST,
data: {
id: pendingPostId,
...newPost,
},
meta: {
offline: {
effect: () => Client4.createPost({...newPost, create_at: 0}),
commit: (result: any, payload: any) => {
const actions: Action[] = [
receivedPost(payload),
{
type: PostTypes.CREATE_POST_SUCCESS,
},
{
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
channelId: newPost.channel_id,
amount: 1,
},
},
{
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
data: {
channelId: newPost.channel_id,
amount: 1,
},
},
];
});
if (files) {
actions.push({
type: FileTypes.RECEIVED_FILES_FOR_POST,
postId: payload.id,
data: files,
});
}
dispatch(batchActions(actions, 'BATCH_CREATE_POST_INIT'));
dispatch(batchActions(actions));
},
maxRetry: 0,
offlineRollback: true,
rollback: (result: any, error: Error) => {
const data = {
...newPost,
id: pendingPostId,
failed: true,
update_at: Date.now(),
};
dispatch({type: PostTypes.CREATE_POST_FAILURE, error});
try {
const created = await Client4.createPost({...newPost, create_at: 0});
// If the failure was because: the root post was deleted or
// TownSquareIsReadOnly=true then remove the post
if (error.server_error_id === 'api.post.create_post.root_id.app_error' ||
error.server_error_id === 'api.post.create_post.town_square_read_only' ||
error.server_error_id === 'plugin.message_will_be_posted.dismiss_post'
) {
dispatch(removePost(data) as any);
} else {
dispatch(receivedPost(data));
}
actions = [
receivedPost(created),
{
type: PostTypes.CREATE_POST_SUCCESS,
},
{
type: ChannelTypes.INCREMENT_TOTAL_MSG_COUNT,
data: {
channelId: newPost.channel_id,
amount: 1,
},
},
},
});
{
type: ChannelTypes.DECREMENT_UNREAD_MSG_COUNT,
data: {
channelId: newPost.channel_id,
amount: 1,
},
},
];
if (files) {
actions.push({
type: FileTypes.RECEIVED_FILES_FOR_POST,
postId: created.id,
data: files,
});
}
dispatch(batchActions(actions, 'BATCH_CREATE_POST'));
} catch (error) {
const data = {
...newPost,
id: pendingPostId,
failed: true,
update_at: Date.now(),
};
actions = [{type: PostTypes.CREATE_POST_FAILURE, error}];
// If the failure was because: the root post was deleted or
// TownSquareIsReadOnly=true then remove the post
if (error.server_error_id === 'api.post.create_post.root_id.app_error' ||
error.server_error_id === 'api.post.create_post.town_square_read_only' ||
error.server_error_id === 'plugin.message_will_be_posted.dismiss_post'
) {
actions.push(removePost(data) as any);
} else {
actions.push(receivedPost(data));
}
dispatch(batchActions(actions, 'BATCH_CREATE_POST_FAILED'));
}
return {data: true};
};
@ -363,11 +361,10 @@ export function resetCreatePostRequest() {
}
export function deletePost(post: ExtendedPost) {
return (dispatch: DispatchFunc, getState: GetStateFunc) => {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const delPost = {...post};
if (delPost.type === Posts.POST_TYPES.COMBINED_USER_ACTIVITY && delPost.system_post_ids) {
delPost.system_post_ids.forEach((systemPostId) => {
if (post.type === Posts.POST_TYPES.COMBINED_USER_ACTIVITY && post.system_post_ids) {
post.system_post_ids.forEach((systemPostId) => {
const systemPost = Selectors.getPost(state, systemPostId);
if (systemPost) {
dispatch(deletePost(systemPost));
@ -376,15 +373,14 @@ export function deletePost(post: ExtendedPost) {
} else {
dispatch({
type: PostTypes.POST_DELETED,
data: delPost,
meta: {
offline: {
effect: () => Client4.deletePost(post.id),
commit: {type: 'do_nothing'}, // redux-offline always needs to dispatch something on commit
rollback: receivedPost(delPost),
},
},
data: post,
});
try {
await Client4.deletePost(post.id);
} catch (error) {
dispatch(receivedPost(post));
}
}
return {data: true};

View file

@ -21,7 +21,14 @@ describe('Actions.Preferences', () => {
});
beforeEach(async () => {
store = await configureStore();
const initialState = {
entities: {
users: {
currentUserId: TestHelper.basicUser.id,
},
},
};
store = await configureStore(initialState);
});
afterAll(async () => {
@ -241,7 +248,7 @@ describe('Actions.Preferences', () => {
nock(Client4.getUsersRoute()).
get('/me/preferences').
reply(200, existingPreferences);
await Actions.getMyPreferences()(store.dispatch, store.getState);
await store.dispatch(Actions.getMyPreferences());
const newTheme = {
type: 'Mattermost Dark',
@ -249,7 +256,7 @@ describe('Actions.Preferences', () => {
nock(Client4.getUsersRoute()).
put(`/${TestHelper.basicUser.id}/preferences`).
reply(200, OK_RESPONSE);
await Actions.saveTheme(team.id, newTheme)(store.dispatch, store.getState);
await store.dispatch(Actions.saveTheme(team.id, newTheme));
const state = store.getState();
const {myPreferences} = state.entities.preferences;

View file

@ -14,28 +14,26 @@ import {PreferenceType} from '@mm-redux/types/preferences';
import {bindClientFunc} from './helpers';
import {getProfilesByIds, getProfilesInChannel} from './users';
import {getChannelAndMyMember, getMyChannelMember} from './channels';
export function deletePreferences(userId: string, preferences: Array<PreferenceType>): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
const state = getState();
const myPreferences = getMyPreferencesSelector(state);
const currentPreferences = preferences.map((pref) => myPreferences[getPreferenceKey(pref.category, pref.name)]);
dispatch({
type: PreferenceTypes.DELETED_PREFERENCES,
data: preferences,
meta: {
offline: {
effect: () => Client4.deletePreferences(userId, preferences),
commit: {
type: PreferenceTypes.DELETED_PREFERENCES,
},
rollback: {
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: currentPreferences,
},
},
},
});
try {
dispatch({
type: PreferenceTypes.DELETED_PREFERENCES,
data: preferences,
});
await Client4.deletePreferences(userId, preferences);
} catch {
dispatch({
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: currentPreferences,
});
}
return {data: true};
};
@ -104,22 +102,21 @@ export function makeGroupMessageVisibleIfNecessary(channelId: string): ActionFun
export function savePreferences(userId: string, preferences: Array<PreferenceType>) {
return async (dispatch: DispatchFunc) => {
dispatch({
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: preferences,
meta: {
offline: {
effect: () => Client4.savePreferences(userId, preferences),
commit: {
type: PreferenceTypes.RECEIVED_PREFERENCES,
},
rollback: {
type: PreferenceTypes.DELETED_PREFERENCES,
data: preferences,
},
},
},
});
try {
// Optimistic action
dispatch({
type: PreferenceTypes.RECEIVED_PREFERENCES,
data: preferences,
});
await Client4.savePreferences(userId, preferences);
} catch (error) {
dispatch({
type: PreferenceTypes.DELETED_PREFERENCES,
data: preferences,
});
return {error};
}
return {data: true};
};

View file

@ -5,14 +5,20 @@ import assert from 'assert';
import nock from 'nock';
import fs from 'fs';
import {NavigationTypes} from '@constants';
import {logout} from '@actions/views/user';
import * as Actions from '@mm-redux/actions/users';
import {Client4} from '@mm-redux/client';
import {RequestStatus} from '../constants';
import TestHelper from 'test/test_helper';
import configureStore from 'test/test_store';
import deepFreeze from '@mm-redux/utils/deep_freeze';
import EventEmitter from '@mm-redux/utils/event_emitter';
import initialState from '@mm-redux/store/initial_state';
const OK_RESPONSE = {status: 'OK'};
const UNAUTHORIZED = {status_code: 401};
describe('Actions.Users', () => {
let store;
@ -21,7 +27,17 @@ describe('Actions.Users', () => {
});
beforeEach(async () => {
store = await configureStore();
const initial = {
...initialState,
entities: {
...initialState.entities,
users: {
...initialState.entities.users,
currentUserId: 'current-user-id',
},
},
};
store = await configureStore(initial);
});
afterAll(async () => {
@ -171,50 +187,13 @@ describe('Actions.Users', () => {
});
it('logout', async () => {
const emit = jest.spyOn(EventEmitter, 'emit');
nock(Client4.getBaseRoute()).
post('/users/logout').
reply(200, OK_RESPONSE);
await Actions.logout()(store.dispatch, store.getState);
const state = store.getState();
const logoutRequest = state.requests.users.logout;
const general = state.entities.general;
const users = state.entities.users;
const teams = state.entities.teams;
const channels = state.entities.channels;
const posts = state.entities.posts;
const preferences = state.entities.preferences;
if (logoutRequest.status === RequestStatus.FAILURE) {
throw new Error(JSON.stringify(logoutRequest.error));
}
assert.deepStrictEqual(general.config, {}, 'config not empty');
assert.deepStrictEqual(general.license, {}, 'license not empty');
assert.strictEqual(users.currentUserId, '', 'current user id not empty');
assert.deepStrictEqual(users.mySessions, [], 'user sessions not empty');
assert.deepStrictEqual(users.myAudits, [], 'user audits not empty');
assert.deepStrictEqual(users.profiles, {}, 'user profiles not empty');
assert.deepStrictEqual(users.profilesInTeam, {}, 'users profiles in team not empty');
assert.deepStrictEqual(users.profilesInChannel, {}, 'users profiles in channel not empty');
assert.deepStrictEqual(users.profilesNotInChannel, {}, 'users profiles NOT in channel not empty');
assert.deepStrictEqual(users.statuses, {}, 'users statuses not empty');
assert.strictEqual(teams.currentTeamId, '', 'current team id is not empty');
assert.deepStrictEqual(teams.teams, {}, 'teams is not empty');
assert.deepStrictEqual(teams.myMembers, {}, 'team members is not empty');
assert.deepStrictEqual(teams.membersInTeam, {}, 'members in team is not empty');
assert.deepStrictEqual(teams.stats, {}, 'team stats is not empty');
assert.strictEqual(channels.currentChannelId, '', 'current channel id is not empty');
assert.deepStrictEqual(channels.channels, {}, 'channels is not empty');
assert.deepStrictEqual(channels.channelsInTeam, {}, 'channelsInTeam is not empty');
assert.deepStrictEqual(channels.myMembers, {}, 'channel members is not empty');
assert.deepStrictEqual(channels.stats, {}, 'channel stats is not empty');
assert.strictEqual(posts.selectedPostId, '', 'selected post id is not empty');
assert.strictEqual(posts.currentFocusedPostId, '', 'current focused post id is not empty');
assert.deepStrictEqual(posts.posts, {}, 'posts is not empty');
assert.deepStrictEqual(posts.postsInChannel, {}, 'posts by channel is not empty');
assert.deepStrictEqual(preferences.myPreferences, {}, 'user preferences not empty');
await store.dispatch(logout(false));
expect(emit).toHaveBeenCalledWith(NavigationTypes.NAVIGATION_RESET);
nock(Client4.getBaseRoute()).
post('/users/login').
@ -625,7 +604,7 @@ describe('Actions.Users', () => {
assert.strictEqual(sessions.length, 0);
TestHelper.mockLogin();
await Actions.loginById(user.id, 'password1')(store.dispatch, store.getState);
await store.dispatch(Actions.loginById(user.id, 'password1'));
nock(Client4.getBaseRoute()).
post('/users/login').
@ -635,7 +614,7 @@ describe('Actions.Users', () => {
nock(Client4.getBaseRoute()).
get(`/users/${user.id}/sessions`).
reply(200, [{id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}, {id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}]);
await Actions.getSessions(user.id)(store.dispatch, store.getState);
await store.dispatch(Actions.getSessions(user.id));
sessions = store.getState().entities.users.mySessions;
assert.ok(sessions.length > 1);
@ -643,23 +622,14 @@ describe('Actions.Users', () => {
nock(Client4.getBaseRoute()).
post(`/users/${user.id}/sessions/revoke/all`).
reply(200, OK_RESPONSE);
const {data} = await Actions.revokeAllSessionsForUser(user.id)(store.dispatch, store.getState);
const {data} = await store.dispatch(Actions.revokeAllSessionsForUser(user.id));
assert.deepEqual(data, true);
nock(Client4.getBaseRoute()).
get('/users').
query(true).
reply(401, {});
await Actions.getProfiles(0)(store.dispatch, store.getState);
const logoutRequest = store.getState().requests.users.logout;
if (logoutRequest.status === RequestStatus.FAILURE) {
throw new Error(JSON.stringify(logoutRequest.error));
}
sessions = store.getState().entities.users.mySessions;
assert.strictEqual(sessions.length, 0);
reply(401, UNAUTHORIZED);
await store.dispatch(Actions.getProfiles(0));
nock(Client4.getBaseRoute()).
post('/users/login').
@ -669,6 +639,11 @@ describe('Actions.Users', () => {
it('revokeSessionsForAllUsers', async () => {
const user = TestHelper.basicUser;
nock(Client4.getBaseRoute()).
post('/roles/names').
reply(200, []);
nock(Client4.getBaseRoute()).
post('/users/logout').
reply(200, OK_RESPONSE);
@ -678,7 +653,7 @@ describe('Actions.Users', () => {
assert.strictEqual(sessions.length, 0);
TestHelper.mockLogin();
await Actions.loginById(user.id, 'password1')(store.dispatch, store.getState);
await store.dispatch(Actions.loginById(user.id, 'password1'));
nock(Client4.getBaseRoute()).
post('/users/login').
@ -688,7 +663,7 @@ describe('Actions.Users', () => {
nock(Client4.getBaseRoute()).
get(`/users/${user.id}/sessions`).
reply(200, [{id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}, {id: TestHelper.generateId(), create_at: 1507756921338, expires_at: 1510348921338, last_activity_at: 1507821125630, user_id: TestHelper.basicUser.id, device_id: '', roles: 'system_admin system_user'}]);
await Actions.getSessions(user.id)(store.dispatch, store.getState);
await store.dispatch(Actions.getSessions(user.id));
sessions = store.getState().entities.users.mySessions;
assert.ok(sessions.length > 1);
@ -696,24 +671,9 @@ describe('Actions.Users', () => {
nock(Client4.getBaseRoute()).
post('/users/sessions/revoke/all').
reply(200, OK_RESPONSE);
const {data} = await Actions.revokeSessionsForAllUsers(user.id)(store.dispatch, store.getState);
const {data} = await store.dispatch(Actions.revokeSessionsForAllUsers(user.id));
assert.deepEqual(data, true);
nock(Client4.getBaseRoute()).
get('/users').
query(true).
reply(401, {});
await Actions.getProfiles(0)(store.dispatch, store.getState);
const logoutRequest = store.getState().requests.users.logout;
if (logoutRequest.status === RequestStatus.FAILURE) {
throw new Error(JSON.stringify(logoutRequest.error));
}
sessions = store.getState().entities.users.mySessions;
assert.strictEqual(sessions.length, 0);
nock(Client4.getBaseRoute()).
post('/users/login').
reply(200, TestHelper.basicUser);

View file

@ -242,22 +242,6 @@ export function loadMe(): ActionFunc {
};
}
export function logout(): ActionFunc {
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
dispatch({type: UserTypes.LOGOUT_REQUEST, data: null});
try {
await Client4.logout();
} catch (error) {
// nothing to do here
}
dispatch({type: UserTypes.LOGOUT_SUCCESS, data: null});
return {data: true};
};
}
export function getTotalUsersStats(): ActionFunc {
return bindClientFunc({
clientFunc: Client4.getTotalUsersStats,
@ -1421,7 +1405,6 @@ export default {
checkMfa,
generateMfaSecret,
login,
logout,
getProfiles,
getProfilesByIds,
getProfilesInTeam,

View file

@ -51,7 +51,7 @@ export default {
GM_CHANNEL: 'G',
PUSH_NOTIFY_APPLE_REACT_NATIVE: 'apple_rn',
PUSH_NOTIFY_ANDROID_REACT_NATIVE: 'android_rn',
STORE_REHYDRATION_COMPLETE: 'store_hydation_complete',
STORE_REHYDRATION_COMPLETE: 'store_hydration_complete',
OFFLINE_STORE_RESET: 'offline_store_reset',
OFFLINE_STORE_PURGE: 'offline_store_purge',
TEAMMATE_NAME_DISPLAY: {
@ -68,4 +68,5 @@ export default {
DISABLED: 'disabled',
DEFAULT_ON: 'default_on',
DEFAULT_OFF: 'default_off',
REHYDRATED: 'app/REHYDRATED',
};

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {BotTypes, UserTypes} from '@mm-redux/action_types';
import {BotTypes} from '@mm-redux/action_types';
import {GenericAction} from '@mm-redux/types/actions';
import {Dictionary} from '@mm-redux/types/utilities';
import {Bot} from '@mm-redux/types/bots';
@ -22,8 +22,6 @@ function accounts(state: Dictionary<Bot> = {}, action: GenericAction) {
nextState[bot.user_id] = bot;
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}

View file

@ -5,7 +5,7 @@ import {combineReducers} from 'redux';
import {CategoryTypes} from '../../constants/channel_categories';
import {ChannelCategoryTypes, TeamTypes, UserTypes} from '@mm-redux/action_types';
import {ChannelCategoryTypes, TeamTypes} from '@mm-redux/action_types';
import {GenericAction} from '@mm-redux/types/actions';
import {ChannelCategory} from '@mm-redux/types/channel_categories';
@ -79,8 +79,6 @@ export function byId(state: IDMappedObjects<ChannelCategory> = {}, action: Gener
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -141,8 +139,6 @@ export function orderByTeam(state: RelationOneToOne<Team, $ID<ChannelCategory>[]
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}

View file

@ -433,18 +433,6 @@ describe('channels', () => {
});
expect(nextState.channel1).toBe(undefined);
});
test('remove all marks if user logs out', () => {
const state = deepFreeze({
channel1: true,
channel231: false,
});
const nextState = Reducers.manuallyUnread(state, {
type: UserTypes.LOGOUT_SUCCESS,
data: {},
});
expect(nextState.channel1).toBe(undefined);
expect(nextState.channel231).toBe(undefined);
});
});
describe('RECEIVED_CHANNELS', () => {
test('should not remove current channel', () => {

View file

@ -42,8 +42,6 @@ function currentChannelId(state = '', action: GenericAction) {
switch (action.type) {
case ChannelTypes.SELECT_CHANNEL:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}
@ -182,8 +180,6 @@ function channels(state: IDMappedObjects<Channel> = {}, action: GenericAction) {
return hasNewValues ? nextState : state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -217,8 +213,6 @@ function channelsInTeam(state: RelationOneToMany<Team, Channel> = {}, action: Ge
};
return channelListToSet(state, values);
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -396,8 +390,6 @@ function myMembers(state: RelationOneToOne<Channel, ChannelMembership> = {}, act
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -462,8 +454,6 @@ function membersInChannel(state: RelationOneToOne<Channel, UserIDMappedObjects<C
case ChannelTypes.UPDATED_CHANNEL_MEMBER_SCHEME_ROLES: {
return updateChannelMemberSchemeRoles(state, action);
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -546,8 +536,6 @@ function stats(state: RelationOneToOne<Channel, ChannelStats> = {}, action: Gene
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -633,11 +621,6 @@ export function manuallyUnread(state: RelationOneToOne<Channel, boolean> = {}, a
}
return state;
}
case UserTypes.LOGOUT_SUCCESS: {
// user is logging out, remove any reference
return {};
}
case ChannelTypes.ADD_MANUALLY_UNREAD:
case ChannelTypes.POST_UNREAD_SUCCESS: {
return {...state, [action.data.channelId]: true};

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {EmojiTypes, PostTypes, UserTypes} from '@mm-redux/action_types';
import {EmojiTypes, PostTypes} from '@mm-redux/action_types';
import {EmojisState, CustomEmoji} from '@mm-redux/types/emojis';
import * as types from '@mm-redux/types';
@ -25,7 +25,6 @@ export function customEmoji(state: types.utilities.IDMappedObjects<CustomEmoji>
return nextState;
}
case EmojiTypes.CLEAR_CUSTOM_EMOJIS:
case UserTypes.LOGOUT_SUCCESS:
return {};
case PostTypes.RECEIVED_NEW_POST:
@ -94,7 +93,6 @@ function nonExistentEmoji(state: Set<string> = new Set(), action: types.actions.
return changed ? nextState : state;
}
case EmojiTypes.CLEAR_CUSTOM_EMOJIS:
case UserTypes.LOGOUT_SUCCESS:
return new Set();
default:

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {FileTypes, PostTypes, UserTypes} from '@mm-redux/action_types';
import {FileTypes, PostTypes} from '@mm-redux/action_types';
import {GenericAction} from '@mm-redux/types/actions';
import {Post} from '@mm-redux/types/posts';
import {FileInfo} from '@mm-redux/types/files';
@ -50,8 +50,6 @@ export function files(state: Dictionary<FileInfo> = {}, action: GenericAction) {
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -109,8 +107,6 @@ export function fileIdsByPostId(state: Dictionary<Array<string>> = {}, action: G
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -132,8 +128,6 @@ function filePublicLink(state: {link: string} = {link: ''}, action: GenericActio
case FileTypes.RECEIVED_FILE_PUBLIC_LINK: {
return action.data;
}
case UserTypes.LOGOUT_SUCCESS:
return {link: ''};
default:
return state;

View file

@ -14,8 +14,6 @@ function config(state: Partial<Config> = {}, action: GenericAction) {
case GeneralTypes.SET_CONFIG_AND_LICENSE:
return Object.assign({}, state, action.data.config);
case GeneralTypes.CLIENT_CONFIG_RESET:
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -40,8 +38,6 @@ function credentials(state: any = {}, action: GenericAction) {
return {
url: action.data.url,
};
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -51,8 +47,6 @@ function dataRetentionPolicy(state: any = {}, action: GenericAction) {
switch (action.type) {
case GeneralTypes.RECEIVED_DATA_RETENTION_POLICY:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -74,8 +68,6 @@ function license(state: any = {}, action: GenericAction) {
case GeneralTypes.SET_CONFIG_AND_LICENSE:
return Object.assign({}, state, action.data.license);
case GeneralTypes.CLIENT_LICENSE_RESET:
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -85,8 +77,6 @@ function timezones(state: string[] = [], action: GenericAction) {
switch (action.type) {
case GeneralTypes.SUPPORTED_TIMEZONES_RECEIVED:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
return state;
}
@ -96,8 +86,6 @@ function serverVersion(state = '', action: GenericAction) {
switch (action.type) {
case GeneralTypes.RECEIVED_SERVER_VERSION:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {IntegrationTypes, UserTypes, ChannelTypes} from '@mm-redux/action_types';
import {IntegrationTypes, ChannelTypes} from '@mm-redux/action_types';
import {GenericAction} from '@mm-redux/types/actions';
import {Command, IncomingWebhook, OutgoingWebhook, OAuthApp} from '@mm-redux/types/integrations';
import {Dictionary, IDMappedObjects} from '@mm-redux/types/utilities';
@ -42,8 +42,6 @@ function incomingHooks(state: IDMappedObjects<IncomingWebhook> = {}, action: Gen
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
@ -85,8 +83,6 @@ function outgoingHooks(state: IDMappedObjects<OutgoingWebhook> = {}, action: Gen
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
@ -131,8 +127,6 @@ function commands(state: IDMappedObjects<Command> = {}, action: GenericAction) {
Reflect.deleteProperty(nextState, action.data.id);
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
@ -159,8 +153,6 @@ function systemCommands(state: IDMappedObjects<Command> = {}, action: GenericAct
}
return state;
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
@ -186,8 +178,6 @@ function oauthApps(state: IDMappedObjects<OAuthApp> = {}, action: GenericAction)
Reflect.deleteProperty(nextState, action.data.id);
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;

View file

@ -1,6 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ChannelTypes, GeneralTypes, PostTypes, UserTypes} from '@mm-redux/action_types';
import {ChannelTypes, GeneralTypes, PostTypes} from '@mm-redux/action_types';
import {Posts} from '../../constants';
import {comparePosts} from '@mm-redux/utils/post_utils';
import {Post, PostsState, PostOrderBlock, MessageHistory} from '@mm-redux/types/posts';
@ -165,9 +165,6 @@ export function handlePosts(state: RelationOneToOne<Post, Post> = {}, action: Ge
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -653,9 +650,6 @@ export function postsInChannel(state: Dictionary<Array<PostOrderBlock>> = {}, ac
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -937,9 +931,6 @@ export function postsInThread(state: RelationOneToMany<Post, Post> = {}, action:
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -949,8 +940,6 @@ function selectedPostId(state = '', action: GenericAction) {
switch (action.type) {
case PostTypes.RECEIVED_POST_SELECTED:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}
@ -960,8 +949,6 @@ function currentFocusedPostId(state = '', action: GenericAction) {
switch (action.type) {
case PostTypes.RECEIVED_FOCUSED_POST:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}
@ -1033,8 +1020,6 @@ export function reactions(state: RelationOneToOne<Post, Dictionary<Reaction>> =
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -1079,8 +1064,6 @@ export function openGraph(state: RelationOneToOne<Post, any> = {}, action: Gener
return posts.reduce(storeOpenGraphForPost, state);
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -1165,16 +1148,6 @@ function messagesHistory(state: Partial<MessageHistory> = {}, action: GenericAct
index: nextIndex,
};
}
case UserTypes.LOGOUT_SUCCESS: {
const index: Dictionary<number> = {};
index[Posts.MESSAGE_TYPES.POST] = -1;
index[Posts.MESSAGE_TYPES.COMMENT] = -1;
return {
messages: [],
index,
};
}
default:
return state;
}

View file

@ -53,9 +53,6 @@ function myPreferences(state: Dictionary<PreferenceType> = {}, action: GenericAc
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {RoleTypes, UserTypes} from '@mm-redux/action_types';
import {RoleTypes} from '@mm-redux/action_types';
import {GenericAction} from '@mm-redux/types/actions';
import {Dictionary} from '@mm-redux/types/utilities';
import {Role} from '@mm-redux/types/roles';
@ -11,8 +11,6 @@ function pending(state: Set<string> = new Set(), action: GenericAction) {
switch (action.type) {
case RoleTypes.SET_PENDING_ROLES:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return new Set();
default:
return state;
}
@ -49,9 +47,6 @@ function roles(state: Dictionary<Role> = {}, action: GenericAction) {
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {SchemeTypes, UserTypes} from '@mm-redux/action_types';
import {SchemeTypes} from '@mm-redux/action_types';
import {GenericAction} from '@mm-redux/types/actions';
import {SchemesState, Scheme} from '@mm-redux/types/schemes';
@ -34,9 +34,6 @@ function schemes(state: {
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}

View file

@ -100,17 +100,6 @@ describe('reducers.entities.search', () => {
const actualState = reducer({results: inputState}, action);
assert.deepEqual(actualState.results, expectedState);
});
describe('UserTypes.LOGOUT_SUCCESS', () => {
const inputState = ['abcd', 'efgh'];
const action = {
type: UserTypes.LOGOUT_SUCCESS,
};
const expectedState = [];
const actualState = reducer({results: inputState}, action);
assert.deepEqual(actualState.results, expectedState);
});
});
describe('matches', () => {
@ -251,20 +240,6 @@ describe('reducers.entities.search', () => {
const actualState = reducer({matches: inputState}, action);
assert.deepEqual(actualState.matches, expectedState);
});
describe('UserTypes.LOGOUT_SUCCESS', () => {
const inputState = {
abcd: ['test', 'testing'],
efgh: ['tests'],
};
const action = {
type: UserTypes.LOGOUT_SUCCESS,
};
const expectedState = [];
const actualState = reducer({matches: inputState}, action);
assert.deepEqual(actualState.matches, expectedState);
});
});
describe('pinned', () => {

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {PostTypes, PreferenceTypes, SearchTypes, UserTypes} from '@mm-redux/action_types';
import {PostTypes, PreferenceTypes, SearchTypes} from '@mm-redux/action_types';
import {Preferences} from '../../constants';
import {PreferenceType} from '@mm-redux/types/preferences';
import {GenericAction} from '@mm-redux/types/actions';
@ -28,7 +28,6 @@ function results(state: Array<string> = [], action: GenericAction) {
return state;
}
case SearchTypes.REMOVE_SEARCH_POSTS:
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
@ -53,7 +52,6 @@ function matches(state: Dictionary<Array<string>> = {}, action: GenericAction) {
return newState;
}
case SearchTypes.REMOVE_SEARCH_POSTS:
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
@ -115,7 +113,6 @@ function flagged(state: Array<string> = [], action: GenericAction) {
return state;
}
case SearchTypes.REMOVE_SEARCH_POSTS:
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
@ -187,8 +184,6 @@ function pinned(state: Dictionary<Array<string>> = {}, action: GenericAction) {
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
return state;
@ -232,8 +227,6 @@ function recent(state: Dictionary<Array<Search>> = {}, action: GenericAction) {
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
@ -254,8 +247,6 @@ function current(state: any = {}, action: GenericAction) {
},
};
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;

View file

@ -13,8 +13,6 @@ function currentTeamId(state = '', action: GenericAction) {
case TeamTypes.SELECT_TEAM:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}
@ -61,9 +59,6 @@ function teams(state: IDMappedObjects<Team> = {}, action: GenericAction) {
return {...state, [teamId]: {...team, scheme_id: schemeId}};
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -248,8 +243,6 @@ function myMembers(state: RelationOneToOne<Team, TeamMembership> = {}, action: G
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -328,8 +321,6 @@ function membersInTeam(state: RelationOneToOne<Team, RelationOneToOne<UserProfil
case TeamTypes.UPDATED_TEAM_MEMBER_SCHEME_ROLES: {
return updateTeamMemberSchemeRoles(state, action);
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}
@ -354,8 +345,6 @@ function stats(state: any = {}, action: GenericAction) {
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}

View file

@ -266,24 +266,6 @@ describe('Reducers.users', () => {
const newState = reducer(state, action);
assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel);
});
it('UserTypes.LOGOUT_SUCCESS, existing profiles', () => {
const state = {
profilesInChannel: {
id: new Set().add('old_user_id'),
other_id: new Set().add('other_user_id'),
},
};
const action = {
type: UserTypes.LOGOUT_SUCCESS,
};
const expectedState = {
profilesInChannel: {},
};
const newState = reducer(state, action);
assert.deepEqual(newState.profilesInChannel, expectedState.profilesInChannel);
});
});
describe('profilesNotInChannel', () => {
@ -545,23 +527,5 @@ describe('Reducers.users', () => {
const newState = reducer(state, action);
assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel);
});
it('UserTypes.LOGOUT_SUCCESS, existing profiles', () => {
const state = {
profilesNotInChannel: {
id: new Set().add('old_user_id'),
other_id: new Set().add('other_user_id'),
},
};
const action = {
type: UserTypes.LOGOUT_SUCCESS,
};
const expectedState = {
profilesNotInChannel: {},
};
const newState = reducer(state, action);
assert.deepEqual(newState.profilesNotInChannel, expectedState.profilesNotInChannel);
});
});
});

View file

@ -102,10 +102,7 @@ function currentUserId(state = '', action: GenericAction) {
return user ? user.id : state;
}
case UserTypes.LOGOUT_SUCCESS:
return '';
}
return state;
}
@ -139,9 +136,6 @@ function mySessions(state: Array<{id: string}> = [], action: GenericAction) {
case UserTypes.REVOKE_SESSIONS_FOR_ALL_USERS_SUCCESS:
return [];
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
return state;
}
@ -152,9 +146,6 @@ function myAudits(state = [], action: GenericAction) {
case UserTypes.RECEIVED_AUDITS:
return [...action.data];
case UserTypes.LOGOUT_SUCCESS:
return [];
default:
return state;
}
@ -212,8 +203,6 @@ function profiles(state: IDMappedObjects<UserProfile> = {}, action: GenericActio
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
case UserTypes.RECEIVED_TERMS_OF_SERVICE_STATUS: {
const data = action.data || action.payload;
return {
@ -255,9 +244,6 @@ function profilesInTeam(state: RelationOneToMany<Team, UserProfile> = {}, action
case UserTypes.RECEIVED_PROFILES_LIST_NOT_IN_TEAM:
return removeProfileListFromSet(state, action);
case UserTypes.LOGOUT_SUCCESS:
return {};
case UserTypes.PROFILE_NO_LONGER_VISIBLE:
return removeProfileFromTeams(state, action);
@ -283,9 +269,6 @@ function profilesNotInTeam(state: RelationOneToMany<Team, UserProfile> = {}, act
case UserTypes.RECEIVED_PROFILES_LIST_IN_TEAM:
return removeProfileListFromSet(state, action);
case UserTypes.LOGOUT_SUCCESS:
return {};
case UserTypes.PROFILE_NO_LONGER_VISIBLE:
return removeProfileFromTeams(state, action);
@ -312,9 +295,6 @@ function profilesWithoutTeam(state: Set<string> = new Set(), action: GenericActi
nextSet.delete(action.data.id);
return nextSet;
}
case UserTypes.LOGOUT_SUCCESS:
return new Set();
default:
return state;
}
@ -342,9 +322,6 @@ function profilesInChannel(state: RelationOneToMany<Channel, UserProfile> = {},
user_id: action.data.user_id,
}});
case UserTypes.LOGOUT_SUCCESS:
return {};
case UserTypes.PROFILE_NO_LONGER_VISIBLE:
return removeProfileFromTeams(state, action);
@ -396,9 +373,6 @@ function profilesNotInChannel(state: RelationOneToMany<Channel, UserProfile> = {
user_id: action.data.user_id,
}});
case UserTypes.LOGOUT_SUCCESS:
return {};
case UserTypes.PROFILE_NO_LONGER_VISIBLE:
return removeProfileFromTeams(state, action);
@ -424,8 +398,6 @@ function statuses(state: RelationOneToOne<UserProfile, string> = {}, action: Gen
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
case UserTypes.PROFILE_NO_LONGER_VISIBLE: {
if (state[action.data.user_id]) {
const newState = {...state};
@ -483,8 +455,6 @@ function isManualStatus(state: RelationOneToOne<UserProfile, boolean> = {}, acti
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
case UserTypes.PROFILE_NO_LONGER_VISIBLE: {
if (state[action.data.user_id]) {
const newState = {...state};
@ -541,7 +511,6 @@ function myUserAccessTokens(state: any = {}, action: GenericAction) {
}
case UserTypes.CLEAR_MY_USER_ACCESS_TOKENS:
case UserTypes.LOGOUT_SUCCESS:
return {};
default:

View file

@ -20,9 +20,6 @@ function checkMfa(state: RequestStatusType = initialRequestState(), action: Gene
case UserTypes.CHECK_MFA_FAILURE:
return {...state, status: RequestStatus.FAILURE, error: action.error};
case UserTypes.LOGOUT_SUCCESS:
return {...state, status: RequestStatus.NOT_STARTED, error: null};
default:
return state;
}
@ -39,28 +36,6 @@ function login(state: RequestStatusType = initialRequestState(), action: Generic
case UserTypes.LOGIN_FAILURE:
return {...state, status: RequestStatus.FAILURE, error: action.error};
case UserTypes.LOGOUT_SUCCESS:
return {...state, status: RequestStatus.NOT_STARTED, error: null};
default:
return state;
}
}
function logout(state: RequestStatusType = initialRequestState(), action: GenericAction): RequestStatusType {
switch (action.type) {
case UserTypes.LOGOUT_REQUEST:
return {...state, status: RequestStatus.STARTED};
case UserTypes.LOGOUT_SUCCESS:
return {...state, status: RequestStatus.SUCCESS, error: null};
case UserTypes.LOGOUT_FAILURE:
return {...state, status: RequestStatus.FAILURE, error: action.error};
case UserTypes.RESET_LOGOUT_STATE:
return initialRequestState();
default:
return state;
}
@ -72,7 +47,7 @@ function autocompleteUsers(state: RequestStatusType = initialRequestState(), act
UserTypes.AUTOCOMPLETE_USERS_SUCCESS,
UserTypes.AUTOCOMPLETE_USERS_FAILURE,
state,
action
action,
);
}
@ -82,14 +57,13 @@ function updateMe(state: RequestStatusType = initialRequestState(), action: Gene
UserTypes.UPDATE_ME_SUCCESS,
UserTypes.UPDATE_ME_FAILURE,
state,
action
action,
);
}
export default (combineReducers({
checkMfa,
login,
logout,
autocompleteUsers,
updateMe,
}) as (b: UsersRequestsStatuses, a: GenericAction) => UsersRequestsStatuses);

View file

@ -27,9 +27,5 @@ export default function(state = getInitialState(), action: GenericAction) {
};
}
if (action.type === UserTypes.LOGOUT_SUCCESS) {
return getInitialState();
}
return state;
}

View file

@ -1,102 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-undefined */
import * as redux from 'redux';
import {createOfflineReducer, networkStatusChangedAction, offlineCompose} from 'redux-offline';
import defaultOfflineConfig from 'redux-offline/lib/defaults';
import reducerRegistry from './reducer_registry';
import devTools from 'remote-redux-devtools';
const globalAny = global as any;
const window = globalAny.window;
const devToolsEnhancer = typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION__ ? // eslint-disable-line no-underscore-dangle
window.__REDUX_DEVTOOLS_EXTENSION__ : // eslint-disable-line no-underscore-dangle
() => {
return devTools({
name: 'Mattermost',
hostname: 'localhost',
port: 5678,
realtime: true,
});
};
import serviceReducer from '../reducers';
import deepFreezeAndThrowOnMutation from '@mm-redux/utils/deep_freeze';
import initialState from './initial_state';
import {offlineConfig, createReducer} from './helpers';
import {createMiddleware} from './middleware';
import {Reducer, Action} from '@mm-redux/types/actions';
import {GlobalState} from '@mm-redux/types/store';
/**
* Configures and constructs the redux store. Accepts the following parameters:
* preloadedState - Any preloaded state to be applied to the store after it is initially configured.
* appReducer - An object containing any app-specific reducer functions that the client needs.
* userOfflineConfig - Any additional configuration data to be passed into redux-offline aside from the default values.
* getAppReducer - A function that returns the appReducer as defined above. Only used in development to enable hot reloading.
* clientOptions - An object containing additional options used when configuring the redux store. The following options are available:
* additionalMiddleware - func | array - Allows for single or multiple additional middleware functions to be passed in from the client side.
* enableBuffer - bool - default = true - If true, the store will buffer all actions until offline state rehydration occurs.
* enableThunk - bool - default = true - If true, include the thunk middleware automatically. If false, thunk must be provided as part of additionalMiddleware.
*/
export default function configureServiceStore(preloadedState: any, appReducer: any, userOfflineConfig: any, getAppReducer: any, clientOptions: any) {
const baseOfflineConfig = Object.assign({}, defaultOfflineConfig, offlineConfig, userOfflineConfig);
const baseState = Object.assign({}, initialState, preloadedState);
const loadReduxDevtools = process.env.NODE_ENV !== 'test'; //eslint-disable-line no-process-env
const store = redux.createStore(
createOfflineReducer(createDevReducer(baseState, serviceReducer, appReducer)),
baseState,
offlineCompose(baseOfflineConfig)(
createMiddleware(clientOptions),
loadReduxDevtools ? [devToolsEnhancer()] : []
)
);
reducerRegistry.setChangeListener((reducers: any) => {
store.replaceReducer(createOfflineReducer(createDevReducer(baseState, reducers)));
});
// launch store persistor
if (baseOfflineConfig.persist) {
baseOfflineConfig.persist(store, baseOfflineConfig.persistOptions, baseOfflineConfig.persistCallback);
}
if (baseOfflineConfig.detectNetwork) {
baseOfflineConfig.detectNetwork((online: boolean) => {
store.dispatch(networkStatusChangedAction(online));
});
}
if ((module as any).hot) {
// Enable Webpack hot module replacement for reducers
(module as any).hot.accept(() => {
const nextServiceReducer = require('../reducers').default; // eslint-disable-line global-require
let nextAppReducer;
if (getAppReducer) {
nextAppReducer = getAppReducer(); // eslint-disable-line global-require
}
store.replaceReducer(createDevReducer(baseState, reducerRegistry.getReducers(), nextServiceReducer, nextAppReducer));
});
}
return store;
}
function createDevReducer(baseState: any, ...reducers: any) {
return enableFreezing(createReducer(baseState, ...reducers));
}
function enableFreezing(reducer: Reducer) {
return (state: GlobalState, action: Action) => {
const nextState = reducer(state, action);
if (nextState !== state) {
deepFreezeAndThrowOnMutation(nextState);
}
return nextState;
};
}

View file

@ -1,57 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import * as redux from 'redux';
import reducerRegistry from './reducer_registry';
import serviceReducer from '../reducers';
import {offlineConfig, createReducer} from './helpers';
import initialState from './initial_state';
import {createMiddleware} from './middleware';
import {createOfflineReducer, networkStatusChangedAction, offlineCompose} from 'redux-offline';
import defaultOfflineConfig from 'redux-offline/lib/defaults';
/**
* Configures and constructs the redux store. Accepts the following parameters:
* preloadedState - Any preloaded state to be applied to the store after it is initially configured.
* appReducer - An object containing any app-specific reducer functions that the client needs.
* userOfflineConfig - Any additional configuration data to be passed into redux-offline aside from the default values.
* getAppReducer - A function that returns the appReducer as defined above. Only used in development to enable hot reloading.
* clientOptions - An object containing additional options used when configuring the redux store. The following options are available:
* additionalMiddleware - func | array - Allows for single or multiple additional middleware functions to be passed in from the client side.
* enableBuffer - bool - default = true - If true, the store will buffer all actions until offline state rehydration occurs.
* enableThunk - bool - default = true - If true, include the thunk middleware automatically. If false, thunk must be provided as part of additionalMiddleware.
*/
export default function configureOfflineServiceStore(preloadedState: any, appReducer: any, userOfflineConfig: any, getAppReducer: any, clientOptions = {}) {
const baseState = Object.assign({}, initialState, preloadedState);
const baseOfflineConfig = Object.assign({}, defaultOfflineConfig, offlineConfig, userOfflineConfig);
const store = redux.createStore(
createOfflineReducer(createReducer(baseState, serviceReducer as any, appReducer)),
baseState,
offlineCompose(baseOfflineConfig)(
createMiddleware(clientOptions),
[]
)
);
reducerRegistry.setChangeListener((reducers: any) => {
store.replaceReducer(createOfflineReducer(createReducer(baseState, reducers)));
});
// launch store persistor
if (baseOfflineConfig.persist) {
baseOfflineConfig.persist(store, baseOfflineConfig.persistOptions, baseOfflineConfig.persistCallback);
}
if (baseOfflineConfig.detectNetwork) {
baseOfflineConfig.detectNetwork((online: boolean) => {
store.dispatch(networkStatusChangedAction(online));
});
}
return store;
}

View file

@ -1,42 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {General} from '../constants';
import reducerRegistry from './reducer_registry';
import {enableBatching, Action, Reducer} from '@mm-redux/types/actions';
export const offlineConfig = {
effect: (effect: Function, action: Action) => {
if (typeof effect !== 'function') {
throw new Error('Offline Action: effect must be a function.');
} else if (!('meta' in action && action.meta && action.meta.offline.commit)) {
throw new Error('Offline Action: commit action must be present.');
}
import {enableBatching, Reducer} from '@mm-redux/types/actions';
import AsyncStorage from '@react-native-community/async-storage';
return effect();
},
discard: (error: Error, action: Action, retries: number) => {
if ('meta' in action && action.meta && action.meta.offline.hasOwnProperty('maxRetry')) {
return retries >= action.meta.offline.maxRetry;
}
/* eslint-disable no-console */
return retries > 10;
},
};
export function createReducer(baseState: any, ...reducers: Reducer[]) {
export function createReducer(...reducers: Reducer[]) {
reducerRegistry.setReducers(Object.assign({}, ...reducers));
const baseReducer = combineReducers(reducerRegistry.getReducers());
// Root reducer wrapper that listens for reset events.
// Returns whatever is passed for the data property
// as the new state.
function offlineReducer(state = {}, action: Action) {
if ('type' in action && 'data' in action && action.type === General.OFFLINE_STORE_RESET) {
return baseReducer(action.data || baseState, action);
}
return enableBatching(baseReducer);
}
return baseReducer(state, action as any);
const KEY_PREFIX = 'reduxPersist:';
export async function getStoredState() {
const restoredState: Record<string, any> = {};
let storeKeys: Array<string> = [];
try {
const allKeys: Array<string> = await AsyncStorage.getAllKeys();
storeKeys = allKeys.filter((key) => key.includes(KEY_PREFIX));
const values = await AsyncStorage.multiGet(storeKeys);
values.forEach(([key, data]: [string, string]) => {
restoredState[key.slice(KEY_PREFIX.length)] = JSON.parse(data);
});
} catch (error) {
console.log('ERROR GETTING FROM AsyncStorage', error);
}
return enableBatching(offlineReducer);
}
return {storeKeys, restoredState};
}

View file

@ -1,6 +1,108 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable global-require, no-process-env */
const config = process.env.NODE_ENV === 'production' ? require('./configureStore.prod').default : require('./configureStore.dev').default;
export default config;
import * as redux from 'redux';
import {persistReducer, persistStore, Persistor, createPersistoid} from 'redux-persist';
import AsyncStorage from '@react-native-community/async-storage';
import {General} from '@mm-redux/constants';
import serviceReducer from '@mm-redux/reducers';
import {createReducer, getStoredState} from './helpers';
import initialState from './initial_state';
import {createMiddleware} from './middleware';
import reducerRegistry from './reducer_registry';
/**
* Configures and constructs the redux store. Accepts the following parameters:
* preloadedState - Any preloaded state to be applied to the store after it is initially configured.
* appReducer - An object containing any app-specific reducer functions that the client needs.
* persistConfig - Configuration for redux-persist.
* getAppReducer - A function that returns the appReducer as defined above. Only used in development to enable hot reloading.
* clientOptions - An object containing additional options used when configuring the redux store. The following options are available:
* additionalMiddleware - func | array - Allows for single or multiple additional middleware functions to be passed in from the client side.
* enableBuffer - bool - default = true - If true, the store will buffer all actions until offline state rehydration occurs.
* enableThunk - bool - default = true - If true, include the thunk middleware automatically. If false, thunk must be provided as part of additionalMiddleware.
*/
type ReduxStore = {
store: redux.Store;
persistor: Persistor;
}
type ClientOptions = {
additionalMiddleware: [];
enableBuffer: boolean;
enableThunk: boolean;
enhancers: [];
}
type V4Store = {
storeKeys: Array<string>;
restoredState: any;
}
const defaultClientOptions: ClientOptions = {
additionalMiddleware: [],
enableBuffer: true,
enableThunk: true,
enhancers: [],
};
export default function configureStore(preloadedState: any, appReducer: any, persistConfig: any, clientOptions: ClientOptions): ReduxStore {
const baseState = Object.assign({}, initialState, preloadedState);
const rootReducer = createReducer(serviceReducer as any, appReducer);
const persistedReducer = persistReducer({...persistConfig}, rootReducer);
const options = Object.assign({}, defaultClientOptions, clientOptions);
const store = redux.createStore(
persistedReducer,
baseState,
redux.compose(
redux.applyMiddleware(
...createMiddleware(options),
),
...options.enhancers,
),
);
reducerRegistry.setChangeListener((reducers: any) => {
const reducer = persistReducer(persistConfig, createReducer(baseState, reducers));
store.replaceReducer(reducer);
});
const persistor = persistStore(store, null);
getStoredState().then(({storeKeys, restoredState}: V4Store) => {
if (Object.keys(restoredState).length) {
const state = {
...restoredState,
views: {
...restoredState.views,
root: {
hydrationComplete: true,
},
},
_persist: persistor.getState(),
};
store.dispatch({
type: General.OFFLINE_STORE_PURGE,
state,
});
console.log('HYDRATED FROM v4', storeKeys); // eslint-disable-line no-console
const persistoid = createPersistoid(persistConfig);
store.subscribe(() => {
persistoid.update(store.getState());
});
store.dispatch({type: General.REHYDRATED});
AsyncStorage.multiRemove(storeKeys);
} else {
store.dispatch({type: General.REHYDRATED});
}
});
return {store, persistor};
}

View file

@ -205,10 +205,6 @@ const state: GlobalState = {
status: 'not_started',
error: null,
},
logout: {
status: 'not_started',
error: null,
},
autocompleteUsers: {
status: 'not_started',
error: null,

View file

@ -3,20 +3,15 @@
import thunk, {ThunkMiddleware} from 'redux-thunk';
import createActionBuffer from 'redux-action-buffer';
import {REHYDRATE} from 'redux-persist/constants';
import {PERSIST} from 'redux-persist';
import {General} from '@mm-redux/constants';
const defaultOptions = {
additionalMiddleware: [],
enableBuffer: true,
enableThunk: true,
};
export function createMiddleware(clientOptions: any): ThunkMiddleware[] {
const options = Object.assign({}, defaultOptions, clientOptions);
const {
additionalMiddleware,
enableBuffer,
enableThunk,
} = options;
} = clientOptions;
const middleware: ThunkMiddleware[] = [];
if (enableThunk) {
@ -32,7 +27,7 @@ export function createMiddleware(clientOptions: any): ThunkMiddleware[] {
}
if (enableBuffer) {
middleware.push(createActionBuffer(REHYDRATE));
middleware.push(createActionBuffer({breaker: General.REHYDRATED, passthrough: [PERSIST]}));
}
return middleware;

View file

@ -0,0 +1,142 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable no-console */
import MMKV from 'react-native-mmkv-storage';
type ReadOnlyArrayString = ReadonlyArray<string>;
type MultiGetCallbackFunction = (
errors: ReadonlyArray<Error> | null | undefined,
result: ReadonlyArray<ReadOnlyArrayString> | null | undefined,
) => void;
type MultiRequest = {
keys: ReadonlyArray<string>;
callback: MultiGetCallbackFunction | null | undefined;
keyIndex: number;
resolve: (
result?: Promise<ReadonlyArray<ReadOnlyArrayString> | null | undefined>,
) => void | null | undefined;
reject: (error?: any) => void | null | undefined;
};
function checkValidInput(usedKey: string, value?: any) {
const isValuePassed = arguments.length > 1;
if (typeof usedKey !== 'string') {
console.warn(
`[MMKVStorageAdapter] Using ${typeof usedKey} type is not suppported. This can lead to unexpected behavior/errors. Use string instead.\nKey passed: ${usedKey}\n`,
);
}
if (isValuePassed && typeof value !== 'string') {
if (value == null) {
throw new Error(
`[MMKVStorageAdapter] Passing null/undefined as value is not supported. If you want to remove value, Use .remove method instead.\nPassed value: ${value}\nPassed key: ${usedKey}\n`,
);
} else {
console.warn(
`[MMKVStorageAdapter] The value for key "${usedKey}" is not a string. This can lead to unexpected behavior/errors. Consider stringifying it.\nPassed value: ${value}\nPassed key: ${usedKey}\n`,
);
}
}
}
const MMKVStorageAdapter = {
_getRequests: [] as Array<MultiRequest>,
_getKeys: [] as Array<string>,
_immediate: null as number | null | undefined,
getItem: (
key: string,
callback?: (
error: Error | null | undefined,
result: string | null,
) => void | null | undefined,
): Promise<string | null> => {
return new Promise((resolve, reject) => {
checkValidInput(key);
MMKV.getStringAsync(key).then((result: string) => {
if (callback) {
callback(null, result);
}
resolve(result);
}).catch((error) => {
if (callback) {
callback(null, error);
}
reject(error);
});
});
},
setItem: (
key: string,
value: string,
callback?: (
error: Error | null | undefined
) => void | null | undefined,
): Promise<null> => {
return new Promise((resolve, reject) => {
checkValidInput(key, value);
MMKV.setStringAsync(key, value).then(() => {
if (callback) {
callback(null);
}
resolve(null);
}).catch((error) => {
if (callback) {
callback(error);
}
reject(error);
});
});
},
removeItem: (
key: string,
callback?: (
error: Error | null | undefined
) => void | null | undefined,
): Promise<null> => {
checkValidInput(key);
if (callback) {
callback(null);
}
return MMKV.removeItem(key);
},
clear: (callback?: (error: Error | null | undefined) => void | null | undefined): Promise<null> => {
if (callback) {
callback(null);
}
return MMKV.clearStore();
},
getAllKeys: (
callback?: (
error: Error | null | undefined,
keys: ReadOnlyArrayString | null | undefined
) => void,
): Promise<ReadOnlyArrayString> => {
return new Promise((resolve, reject) => {
MMKV.getKeysAsync().then((keys: Array<string>) => {
if (callback) {
callback(null, keys);
}
resolve(keys);
}).catch((error) => {
if (callback) {
callback(error, null);
}
reject(error);
});
});
},
};
export default MMKVStorageAdapter;

View file

@ -1,8 +1,6 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
declare module 'gfycat-sdk';
declare module 'redux-offline';
declare module 'redux-offline/lib/defaults';
declare module 'remote-redux-devtools';
declare module 'redux-persist';
declare module 'redux-persist/constants';

View file

@ -34,7 +34,6 @@ export type TeamsRequestsStatuses = {
export type UsersRequestsStatuses = {
checkMfa: RequestStatusType;
login: RequestStatusType;
logout: RequestStatusType;
autocompleteUsers: RequestStatusType;
updateMe: RequestStatusType;
};

View file

@ -1,16 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {UserTypes} from '@mm-redux/action_types';
import {DeviceTypes} from 'app/constants';
import {DeviceTypes} from '@constants';
export default function connection(state = true, action) {
switch (action.type) {
case DeviceTypes.CONNECTION_CHANGED:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return true;
}
return state;

View file

@ -3,8 +3,7 @@
import {Dimensions} from 'react-native';
import {UserTypes} from '@mm-redux/action_types';
import {DeviceTypes} from 'app/constants';
import {DeviceTypes} from '@constants';
const {height, width} = Dimensions.get('window');
const initialState = {
@ -21,8 +20,6 @@ export default function dimension(state = initialState, action) {
}
break;
}
case UserTypes.LOGOUT_SUCCESS:
return initialState;
}
return state;

View file

@ -3,12 +3,10 @@
import app from './app';
import device from './device';
import navigation from './navigation';
import views from './views';
export default {
app,
device,
navigation,
views,
};

View file

@ -1,18 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {NavigationTypes} from 'app/constants';
import {UserTypes} from '@mm-redux/action_types';
import EventEmitter from '@mm-redux/utils/event_emitter';
export default function(state = '', action) {
switch (action.type) {
case UserTypes.LOGOUT_SUCCESS:
setTimeout(() => {
EventEmitter.emit(NavigationTypes.NAVIGATION_RESET);
});
break;
}
return state;
}

View file

@ -1,16 +1,12 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {UserTypes} from '@mm-redux/action_types';
import {ViewTypes} from 'app/constants';
import {ViewTypes} from '@constants';
export default function banner(state = '', action) {
switch (action.type) {
case ViewTypes.ANNOUNCEMENT_BANNER:
return action.data;
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;
}

View file

@ -2,15 +2,14 @@
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {ViewTypes} from '@constants';
import {
ChannelTypes,
FileTypes,
PostTypes,
UserTypes,
} from '@mm-redux/action_types';
import {ViewTypes} from 'app/constants';
function displayName(state = '', action) {
switch (action.type) {
case ViewTypes.SET_CHANNEL_DISPLAY_NAME:
@ -355,9 +354,6 @@ function keepChannelIdAsUnread(state = null, action) {
}
return state;
}
case UserTypes.LOGOUT_SUCCESS:
return null;
default:
return state;
}

View file

@ -2,16 +2,13 @@
// See LICENSE.txt for license information.
import {combineReducers} from 'redux';
import {UserTypes} from '@mm-redux/action_types';
import {ViewTypes} from 'app/constants';
import {ViewTypes} from '@constants';
function emojiPickerCustomPage(state = 0, action) {
switch (action.type) {
case ViewTypes.INCREMENT_EMOJI_PICKER_PAGE:
return state + 1;
case UserTypes.LOGOUT_SUCCESS:
return 0;
default:
return state;
}

View file

@ -14,8 +14,6 @@ function locale(state = defaultLocale, action) {
const data = action.data || action.payload;
return data.locale;
}
case UserTypes.LOGOUT_SUCCESS:
return defaultLocale;
}
return state;

View file

@ -31,8 +31,6 @@ function submittedMenuActions(state = {}, action) {
return nextState;
}
case UserTypes.LOGOUT_SUCCESS:
return {};
default:
return state;
}

View file

@ -1,16 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ViewTypes} from 'app/constants';
import {UserTypes} from '@mm-redux/action_types';
import {ViewTypes} from '@constants';
export default function search(state = '', action) {
switch (action.type) {
case ViewTypes.SEARCH_DRAFT_CHANGED: {
return action.text;
}
case UserTypes.LOGOUT_SUCCESS:
return '';
default:
return state;

View file

@ -4,6 +4,7 @@
import initialState from 'app/initial_state';
import configureAppStore from './store';
const store = configureAppStore(initialState);
const {store, persistor} = configureAppStore(initialState);
export {persistor};
export default store;

View file

@ -3,22 +3,14 @@
import {Platform} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {batchActions} from 'redux-batched-actions';
import AsyncStorage from '@react-native-community/async-storage';
import {purgeStoredState} from 'redux-persist';
import {REHYDRATE} from 'redux-persist';
import {NavigationTypes, ViewTypes} from 'app/constants';
import {ViewTypes} from 'app/constants';
import initialState from 'app/initial_state';
import {throttle} from 'app/utils/general';
import {General} from '@mm-redux/constants';
import {ErrorTypes, GeneralTypes} from '@mm-redux/action_types';
import EventEmitter from '@mm-redux/utils/event_emitter';
import mattermostBucket from 'app/mattermost_bucket';
import {getStateForReset} from './utils';
import {
captureException,
LOGGER_JAVASCRIPT_WARNING,
@ -35,115 +27,92 @@ const SAVE_STATE_ACTIONS = [
'WEBSOCKET_SUCCESS',
];
export const middlewares = () => {
const middlewareFunctions = [
messageRetention,
];
if (Platform.OS === 'ios') {
middlewareFunctions.push(saveShareExtensionState);
}
return middlewareFunctions;
};
// This middleware stores key parts of state entities into a file (in the App Group container) on certain actions.
// iOS only. Allows the share extension to work, without having access available to the redux store object.
// Remove this middleware if/when state is moved to a persisted solution.
const saveShareExtensionState = (store) => {
function saveShareExtensionState(store) {
return (next) => (action) => {
if (SAVE_STATE_ACTIONS.includes(action.type)) {
throttle(saveStateToFile(store));
}
return next(action);
};
};
}
const saveStateToFile = async (store) => {
if (Platform.OS === 'ios') {
const state = store.getState();
async function saveStateToFile(store) {
const state = store.getState();
if (state.entities) {
const channelsInTeam = {...state.entities.channels.channelsInTeam};
Object.keys(channelsInTeam).forEach((teamId) => {
channelsInTeam[teamId] = Array.from(channelsInTeam[teamId]);
});
if (state.entities) {
const channelsInTeam = {...state.entities.channels.channelsInTeam};
Object.keys(channelsInTeam).forEach((teamId) => {
channelsInTeam[teamId] = Array.from(channelsInTeam[teamId]);
});
const profilesInChannel = {...state.entities.users.profilesInChannel};
Object.keys(profilesInChannel).forEach((channelId) => {
profilesInChannel[channelId] = Array.from(profilesInChannel[channelId]);
});
const profilesInChannel = {...state.entities.users.profilesInChannel};
Object.keys(profilesInChannel).forEach((channelId) => {
profilesInChannel[channelId] = Array.from(profilesInChannel[channelId]);
});
let url;
if (state.entities.users.currentUserId) {
url = state.entities.general.credentials.url || state.views.selectServer.serverUrl;
let url;
if (state.entities.users.currentUserId) {
url = state.entities.general.credentials.url || state.views.selectServer.serverUrl;
}
const entities = {
...state.entities,
general: {
...state.entities.general,
credentials: {
url,
},
},
channels: {
...state.entities.channels,
channelsInTeam,
},
users: {
...state.entities.users,
profilesInChannel,
profilesNotInTeam: [],
profilesWithoutTeam: [],
profilesNotInChannel: [],
},
};
mattermostBucket.writeToFile('entities', JSON.stringify(entities));
}
}
function messageRetention(store) {
return (next) => (action) => {
if (action.type === REHYDRATE) {
// On first run payload is not set (when installed)
if (!action.payload) {
action.payload = {
app: {
build: DeviceInfo.getBuildNumber(),
version: DeviceInfo.getVersion(),
},
views: {
root: {
hydrationComplete: true,
},
},
};
}
const entities = {
...state.entities,
general: {
...state.entities.general,
credentials: {
url,
},
},
channels: {
...state.entities.channels,
channelsInTeam,
},
users: {
...state.entities.users,
profilesInChannel,
profilesNotInTeam: [],
profilesWithoutTeam: [],
profilesNotInChannel: [],
},
};
mattermostBucket.writeToFile('entities', JSON.stringify(entities));
}
}
};
const purgeAppCacheWrapper = (persistConfig) => (store) => {
return (next) => (action) => {
if (action.type === General.OFFLINE_STORE_PURGE) {
purgeStoredState({...persistConfig, storage: AsyncStorage});
const state = store.getState();
const resetState = getStateForReset(initialState, state);
store.dispatch(batchActions([
{
type: General.OFFLINE_STORE_RESET,
data: resetState,
},
{
type: ErrorTypes.RESTORE_ERRORS,
data: [...state.errors],
},
{
type: GeneralTypes.RECEIVED_APP_DEVICE_TOKEN,
data: state.entities.general.deviceToken,
},
{
type: GeneralTypes.RECEIVED_APP_CREDENTIALS,
data: {
url: state.entities.general.credentials.url,
},
},
{
type: ViewTypes.SERVER_URL_CHANGED,
serverUrl: state.entities.general.credentials.url || state.views.selectServer.serverUrl,
},
{
type: GeneralTypes.RECEIVED_SERVER_VERSION,
data: state.entities.general.serverVersion,
},
{
type: General.STORE_REHYDRATION_COMPLETE,
},
], 'BATCH_FOR_RESTART'));
setTimeout(() => {
EventEmitter.emit(NavigationTypes.RESTART_APP);
}, 500);
}
return next(action);
};
};
const messageRetention = (store) => {
return (next) => (action) => {
if (action.type === 'persist/REHYDRATE') {
const {app} = action.payload;
const {entities, views} = action.payload;
@ -153,25 +122,24 @@ const messageRetention = (store) => {
// When a new version of the app has been detected
if (!app || !app.version || app.version !== DeviceInfo.getVersion() || app.build !== DeviceInfo.getBuildNumber()) {
return next(resetStateForNewVersion(action));
action.payload = resetStateForNewVersion(action.payload);
return next(action);
}
// Keep only the last 60 messages for the last 5 viewed channels in each team
// and apply data retention on those posts if applies
let nextAction;
try {
nextAction = cleanUpState(action);
action.payload = cleanUpState(action.payload);
} catch (e) {
// Sometimes, the payload is incomplete so log the error to Sentry and skip the cleanup
console.warn(e); // eslint-disable-line no-console
captureException(e, LOGGER_JAVASCRIPT_WARNING, store);
nextAction = action;
}
return next(nextAction);
return next(action);
} else if (action.type === ViewTypes.DATA_CLEANUP) {
const nextAction = cleanUpState(action, true);
return next(nextAction);
action.payload = cleanUpState(action.payload, true);
return next(action);
}
/* Uncomment the following lines to log the actions being dispatched */
@ -185,10 +153,9 @@ const messageRetention = (store) => {
return next(action);
};
};
}
function resetStateForNewVersion(action) {
const {payload} = action;
function resetStateForNewVersion(payload) {
const lastChannelForTeam = getLastChannelForTeam(payload);
let general = initialState.entities.general;
@ -317,29 +284,28 @@ function resetStateForNewVersion(action) {
},
};
return {
type: action.type,
payload: nextState,
error: action.error,
};
return nextState;
}
function getLastChannelForTeam(payload) {
const lastChannelForTeam = {...payload.views.team.lastChannelForTeam};
const convertLastChannelForTeam = Object.values(lastChannelForTeam).some((value) => !Array.isArray(value));
if (payload?.views?.team?.lastChannelForTeam) {
const lastChannelForTeam = {...payload.views.team.lastChannelForTeam};
const convertLastChannelForTeam = Object.values(lastChannelForTeam).some((value) => !Array.isArray(value));
if (convertLastChannelForTeam) {
Object.keys(lastChannelForTeam).forEach((id) => {
lastChannelForTeam[id] = [lastChannelForTeam[id]];
});
if (convertLastChannelForTeam) {
Object.keys(lastChannelForTeam).forEach((id) => {
lastChannelForTeam[id] = [lastChannelForTeam[id]];
});
}
return lastChannelForTeam;
}
return lastChannelForTeam;
return {};
}
export function cleanUpState(action, keepCurrent = false) {
const {payload: resetPayload} = resetStateForNewVersion(action);
const {payload} = action;
export function cleanUpState(payload, keepCurrent = false) {
const resetPayload = resetStateForNewVersion(payload);
const {currentChannelId} = payload.entities.channels;
const {lastChannelForTeam} = resetPayload.views.team;
@ -470,6 +436,9 @@ export function cleanUpState(action, keepCurrent = false) {
...resetPayload.views.channel,
...payload.views.channel,
},
root: {
hydrationComplete: true,
},
},
websocket: {
lastConnectAt: payload.websocket?.lastConnectAt,
@ -479,11 +448,7 @@ export function cleanUpState(action, keepCurrent = false) {
nextState.errors = payload.errors;
return {
type: action.type,
payload: nextState,
error: action.error,
};
return nextState;
}
// cleanUpPostsInChannel returns a copy of postsInChannel where only the most recent posts in each channel are kept
@ -564,17 +529,3 @@ function removePendingPost(pendingPostIds, id) {
pendingPostIds.splice(pendingIndex, 1);
}
}
export const middlewares = (persistConfig) => {
const middlewareFunctions = [
messageRetention,
purgeAppCacheWrapper(persistConfig),
];
if (Platform.OS === 'ios') {
middlewareFunctions.push(saveShareExtensionState);
}
return middlewareFunctions;
};

View file

@ -102,13 +102,13 @@ describe('cleanUpState', () => {
},
};
const result = cleanUpState({payload: state});
const result = cleanUpState(state);
expect(result.payload.entities.posts.posts.post1).toBeDefined();
expect(result.payload.entities.posts.posts.post2).toBeUndefined();
expect(result.payload.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1'], recent: true}]);
expect(result.payload.entities.search.results).toEqual(['post1', 'post2']);
expect(result.payload.entities.search.flagged).toEqual(['post1', 'post2', 'post3']);
expect(result.entities.posts.posts.post1).toBeDefined();
expect(result.entities.posts.posts.post2).toBeUndefined();
expect(result.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1'], recent: true}]);
expect(result.entities.search.results).toEqual(['post1', 'post2']);
expect(result.entities.search.flagged).toEqual(['post1', 'post2', 'post3']);
});
test('should keep failed pending post', () => {
@ -145,11 +145,11 @@ describe('cleanUpState', () => {
},
};
const result = cleanUpState({payload: state});
const result = cleanUpState(state);
expect(result.payload.entities.posts.pendingPostIds).toEqual(['pending']);
expect(result.payload.entities.posts.posts.pending).toBeDefined();
expect(result.payload.entities.posts.postsInChannel.channel1).toEqual([{order: ['pending', 'post1', 'post2'], recent: true}]);
expect(result.entities.posts.pendingPostIds).toEqual(['pending']);
expect(result.entities.posts.posts.pending).toBeDefined();
expect(result.entities.posts.postsInChannel.channel1).toEqual([{order: ['pending', 'post1', 'post2'], recent: true}]);
});
test('should remove non-failed pending post', () => {
@ -186,11 +186,11 @@ describe('cleanUpState', () => {
},
};
const result = cleanUpState({payload: state});
const result = cleanUpState(state);
expect(result.payload.entities.posts.pendingPostIds).toEqual([]);
expect(result.payload.entities.posts.posts.pending).toBeUndefined();
expect(result.payload.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1', 'post2'], recent: true}]);
expect(result.entities.posts.pendingPostIds).toEqual([]);
expect(result.entities.posts.posts.pending).toBeUndefined();
expect(result.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1', 'post2'], recent: true}]);
});
test('should remove non-existent pending post', () => {
@ -226,10 +226,10 @@ describe('cleanUpState', () => {
},
};
const result = cleanUpState({payload: state});
const result = cleanUpState(state);
expect(result.payload.entities.posts.pendingPostIds).toEqual([]);
expect(result.payload.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1', 'post2'], recent: true}]);
expect(result.entities.posts.pendingPostIds).toEqual([]);
expect(result.entities.posts.postsInChannel.channel1).toEqual([{order: ['post1', 'post2'], recent: true}]);
});
});

View file

@ -1,26 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import AsyncStorage from '@react-native-community/async-storage';
import {createBlacklistFilter} from 'redux-persist-transform-filter';
import {createTransform, persistStore} from 'redux-persist';
import {createTransform} from 'redux-persist';
import reduxReset from 'redux-reset';
import {General} from '@mm-redux/constants';
import {getConfig} from '@mm-redux/selectors/entities/general';
import configureStore from '@mm-redux/store';
import MMKVStorageAdapter from '@mm-redux/store/mmkv_adapter';
import appReducer from 'app/reducers';
import {getSiteUrl, setSiteUrl} from 'app/utils/image_cache_manager';
import {createSentryMiddleware} from 'app/utils/sentry/middleware';
import {middlewares} from './middleware';
import {createThunkMiddleware} from './thunk';
import {transformSet} from './utils';
function getAppReducer() {
return require('../../app/reducers'); // eslint-disable-line global-require
}
const usersSetTransform = [
'profilesInChannel',
'profilesNotInChannel',
@ -56,8 +51,9 @@ const channelViewBlackList = {loading: true, refreshing: true, loadingPosts: tru
const channelViewBlackListFilter = createTransform(
(inboundState) => {
const channel = {};
const keys = inboundState.channel ? Object.keys(inboundState.channel) : [];
for (const channelKey of Object.keys(inboundState.channel)) {
for (const channelKey of keys) {
if (!channelViewBlackList[channelKey]) {
channel[channelKey] = inboundState.channel[channelKey];
}
@ -76,8 +72,9 @@ const emojiBlackList = {nonExistentEmoji: true};
const emojiBlackListFilter = createTransform(
(inboundState) => {
const emojis = {};
const keys = inboundState.emojis ? Object.keys(inboundState.emojis) : [];
for (const emojiKey of Object.keys(inboundState.emojis)) {
for (const emojiKey of keys) {
if (!emojiBlackList[emojiKey]) {
emojis[emojiKey] = inboundState.emojis[emojiKey];
}
@ -124,47 +121,16 @@ const setTransformer = createTransform(
);
const persistConfig = {
effect: (effect, action) => {
if (typeof effect !== 'function') {
throw new Error('Offline Action: effect must be a function.');
} else if (!action.meta.offline.commit) {
throw new Error('Offline Action: commit action must be present.');
}
return effect();
},
persist: (store, options) => {
const persistor = persistStore(store, {storage: AsyncStorage, ...options}, () => {
store.dispatch({
type: General.STORE_REHYDRATION_COMPLETE,
});
});
store.subscribe(async () => {
const state = store.getState();
const config = getConfig(state);
if (getSiteUrl() !== config?.SiteURL) {
setSiteUrl(config.SiteURL);
}
});
return persistor;
},
persistOptions: {
autoRehydrate: {
log: false,
},
blacklist: ['device', 'navigation', 'offline', 'requests'],
debounce: 500,
transforms: [
setTransformer,
viewsBlackListFilter,
typingBlackListFilter,
channelViewBlackListFilter,
emojiBlackListFilter,
],
},
key: 'root',
storage: MMKVStorageAdapter,
blacklist: ['device', 'navigation', 'offline', 'requests'],
transforms: [
setTransformer,
viewsBlackListFilter,
typingBlackListFilter,
channelViewBlackListFilter,
emojiBlackListFilter,
],
};
export default function configureAppStore(initialState) {
@ -172,10 +138,11 @@ export default function configureAppStore(initialState) {
additionalMiddleware: [
createThunkMiddleware(),
createSentryMiddleware(),
...middlewares(persistConfig),
...middlewares(),
],
enableThunk: false, // We override the default thunk middleware
enhancers: [reduxReset(General.OFFLINE_STORE_PURGE)],
};
return configureStore(initialState, appReducer, persistConfig, getAppReducer, clientOptions);
return configureStore(initialState, appReducer, persistConfig, clientOptions);
}

View file

@ -45,14 +45,14 @@ export function transformSet(incoming, setTransforms, toStorage = true) {
export function waitForHydration(store, callback) {
let executed = false; // this is to prevent a race condition when subcription runs before unsubscribed
if (store.getState().views.root.hydrationComplete && !executed) {
if (store.getState().views?.root?.hydrationComplete && !executed) {
if (callback && typeof callback === 'function') {
executed = true;
callback();
}
} else {
const subscription = () => {
if (store.getState().views.root.hydrationComplete && !executed) {
if (store.getState().views?.root?.hydrationComplete && !executed) {
unsubscribeFromStore();
if (callback && typeof callback === 'function') {
executed = true;
@ -73,6 +73,7 @@ export function getStateForReset(initialState, currentState) {
const resetState = merge(initialState, {
entities: {
general: currentState.entities.general,
users: {
currentUserId,
profiles: {
@ -84,6 +85,15 @@ export function getStateForReset(initialState, currentState) {
},
preferences,
},
errors: currentState.errors,
views: {
selectServer: {
serverUrl: currentState.views?.selectServer?.serverUrl,
},
root: {
hydrationComplete: true,
},
},
});
return resetState;

View file

@ -52,6 +52,11 @@ describe('getStateForReset', () => {
},
},
},
views: {
selectServer: {
serverUrl: 'localhost:8065',
},
},
};
it('should keep the current user\'s ID and profile', () => {

View file

@ -80,13 +80,9 @@ class PushNotificationUtils {
if (foreground) {
EventEmitter.emit(ViewTypes.NOTIFICATION_IN_APP, notification);
} else if (userInteraction && !notification?.data?.localNotification) {
if (getState().views.root.hydrationComplete) { //TODO: Replace when realm is ready
waitForHydration(this.store, () => {
this.loadFromNotification(notification);
} else {
waitForHydration(this.store, () => {
this.loadFromNotification(notification);
});
}
});
}
}
};

View file

@ -15,6 +15,7 @@ module.exports = {
assets: './dist/assets',
'@actions': './app/actions',
'@constants': './app/constants',
'@i18n': './app/i18n',
'@selectors': './app/selectors',
'@telemetry': './app/telemetry',
'@utils': './app/utils',

View file

@ -32,6 +32,7 @@ PODS:
- libwebp/mux (1.1.0):
- libwebp/demux
- libwebp/webp (1.1.0)
- MMKV (1.0.24)
- Permission-Camera (2.0.10):
- RNPermissions
- Permission-PhotoLibrary (2.0.10):
@ -212,6 +213,9 @@ PODS:
- React
- react-native-image-picker (2.3.1):
- React
- react-native-mmkv-storage (0.2.2):
- MMKV (= 1.0.24)
- React
- react-native-netinfo (4.4.0):
- React
- react-native-notifications (2.0.6):
@ -338,6 +342,7 @@ DEPENDENCIES:
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- react-native-hw-keyboard-event (from `../node_modules/react-native-hw-keyboard-event`)
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
- react-native-mmkv-storage (from `../node_modules/react-native-mmkv-storage`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- react-native-notifications (from `../node_modules/react-native-notifications`)
- react-native-passcode-status (from `../node_modules/react-native-passcode-status`)
@ -378,6 +383,7 @@ SPEC REPOS:
https://github.com/cocoapods/specs.git:
- boost-for-react-native
- libwebp
- MMKV
- SDWebImage
- SDWebImageWebPCoder
- Sentry
@ -434,6 +440,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-hw-keyboard-event"
react-native-image-picker:
:path: "../node_modules/react-native-image-picker"
react-native-mmkv-storage:
:path: "../node_modules/react-native-mmkv-storage"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-notifications:
@ -509,6 +517,7 @@ SPEC CHECKSUMS:
glog: 1f3da668190260b06b429bb211bfbee5cd790c28
jail-monkey: d7c5048b2336f22ee9c9e0efa145f1f917338ea9
libwebp: 946cb3063cea9236285f7e9a8505d806d30e07f3
MMKV: 758b2edee46b08bdd958db4169191afb9a6d4ebd
Permission-Camera: 8f0e5decca5f28f70f28a8dc31f012c9bad40ad8
Permission-PhotoLibrary: b209bf23b784c9e1409a57d81c6d11ab1d3079c1
RCTRequired: b153add4da6e7dbc44aebf93f3cf4fcae392ddf1
@ -526,6 +535,7 @@ SPEC CHECKSUMS:
react-native-document-picker: 0573c02d742d4bef38a5d16b5f039754cfa69888
react-native-hw-keyboard-event: b517cefb8d5c659a38049c582de85ff43337dc53
react-native-image-picker: 668e72d0277dc8c12ae90e835507c1eddd2e4f85
react-native-mmkv-storage: 84162ebe353ecf7476d235c47becade29789ae2c
react-native-netinfo: 892a5130be97ff8bb69c523739c424a2ffc296d1
react-native-notifications: d5cb54ef8bf3004dcb56c887650dea08ecbddee7
react-native-passcode-status: 88c4f6e074328bc278bd127646b6c694ad5a530a

342
package-lock.json generated
View file

@ -6896,16 +6896,6 @@
"node-int64": "^0.4.0"
}
},
"buffer": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz",
"integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==",
"dev": true,
"requires": {
"base64-js": "^1.0.2",
"ieee754": "^1.1.4"
}
},
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
@ -7158,12 +7148,6 @@
}
}
},
"clone": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz",
"integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=",
"dev": true
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -7266,9 +7250,9 @@
},
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
}
}
},
@ -9801,25 +9785,25 @@
"dependencies": {
"abbrev": {
"version": "1.1.1",
"resolved": "",
"resolved": false,
"integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==",
"optional": true
},
"ansi-regex": {
"version": "2.1.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
},
"aproba": {
"version": "1.2.0",
"resolved": "",
"resolved": false,
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==",
"optional": true
},
"are-we-there-yet": {
"version": "1.1.5",
"resolved": "",
"resolved": false,
"integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==",
"optional": true,
"requires": {
@ -9829,13 +9813,13 @@
},
"balanced-match": {
"version": "1.0.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "",
"resolved": false,
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": {
@ -9845,37 +9829,37 @@
},
"chownr": {
"version": "1.1.1",
"resolved": "",
"resolved": false,
"integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==",
"optional": true
},
"code-point-at": {
"version": "1.1.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
},
"concat-map": {
"version": "0.0.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
},
"core-util-is": {
"version": "1.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=",
"optional": true
},
"debug": {
"version": "4.1.1",
"resolved": "",
"resolved": false,
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"optional": true,
"requires": {
@ -9884,25 +9868,25 @@
},
"deep-extend": {
"version": "0.6.0",
"resolved": "",
"resolved": false,
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"optional": true
},
"delegates": {
"version": "1.0.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
"optional": true
},
"detect-libc": {
"version": "1.0.3",
"resolved": "",
"resolved": false,
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
"optional": true
},
"fs-minipass": {
"version": "1.2.5",
"resolved": "",
"resolved": false,
"integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==",
"optional": true,
"requires": {
@ -9911,13 +9895,13 @@
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
"optional": true
},
"gauge": {
"version": "2.7.4",
"resolved": "",
"resolved": false,
"integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=",
"optional": true,
"requires": {
@ -9933,7 +9917,7 @@
},
"glob": {
"version": "7.1.3",
"resolved": "",
"resolved": false,
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"optional": true,
"requires": {
@ -9947,13 +9931,13 @@
},
"has-unicode": {
"version": "2.0.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=",
"optional": true
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "",
"resolved": false,
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"optional": true,
"requires": {
@ -9962,7 +9946,7 @@
},
"ignore-walk": {
"version": "3.0.1",
"resolved": "",
"resolved": false,
"integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==",
"optional": true,
"requires": {
@ -9971,7 +9955,7 @@
},
"inflight": {
"version": "1.0.6",
"resolved": "",
"resolved": false,
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"optional": true,
"requires": {
@ -9981,19 +9965,19 @@
},
"inherits": {
"version": "2.0.3",
"resolved": "",
"resolved": false,
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
},
"ini": {
"version": "1.3.5",
"resolved": "",
"resolved": false,
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==",
"optional": true
},
"is-fullwidth-code-point": {
"version": "1.0.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
@ -10002,13 +9986,13 @@
},
"isarray": {
"version": "1.0.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
"optional": true
},
"minimatch": {
"version": "3.0.4",
"resolved": "",
"resolved": false,
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
@ -10017,13 +10001,13 @@
},
"minimist": {
"version": "0.0.8",
"resolved": "",
"resolved": false,
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
},
"minipass": {
"version": "2.3.5",
"resolved": "",
"resolved": false,
"integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==",
"optional": true,
"requires": {
@ -10033,7 +10017,7 @@
},
"minizlib": {
"version": "1.2.1",
"resolved": "",
"resolved": false,
"integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==",
"optional": true,
"requires": {
@ -10042,7 +10026,7 @@
},
"mkdirp": {
"version": "0.5.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"optional": true,
"requires": {
@ -10051,13 +10035,13 @@
},
"ms": {
"version": "2.1.1",
"resolved": "",
"resolved": false,
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"optional": true
},
"needle": {
"version": "2.3.0",
"resolved": "",
"resolved": false,
"integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==",
"optional": true,
"requires": {
@ -10068,7 +10052,7 @@
},
"node-pre-gyp": {
"version": "0.12.0",
"resolved": "",
"resolved": false,
"integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==",
"optional": true,
"requires": {
@ -10086,7 +10070,7 @@
},
"nopt": {
"version": "4.0.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
"optional": true,
"requires": {
@ -10096,13 +10080,13 @@
},
"npm-bundled": {
"version": "1.0.6",
"resolved": "",
"resolved": false,
"integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==",
"optional": true
},
"npm-packlist": {
"version": "1.4.1",
"resolved": "",
"resolved": false,
"integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==",
"optional": true,
"requires": {
@ -10112,7 +10096,7 @@
},
"npmlog": {
"version": "4.1.2",
"resolved": "",
"resolved": false,
"integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==",
"optional": true,
"requires": {
@ -10124,19 +10108,19 @@
},
"number-is-nan": {
"version": "1.0.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
},
"object-assign": {
"version": "4.1.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
"optional": true
},
"once": {
"version": "1.4.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": {
@ -10145,19 +10129,19 @@
},
"os-homedir": {
"version": "1.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"optional": true
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=",
"optional": true
},
"osenv": {
"version": "0.1.5",
"resolved": "",
"resolved": false,
"integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
"optional": true,
"requires": {
@ -10167,19 +10151,19 @@
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
"optional": true
},
"process-nextick-args": {
"version": "2.0.0",
"resolved": "",
"resolved": false,
"integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==",
"optional": true
},
"rc": {
"version": "1.2.8",
"resolved": "",
"resolved": false,
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"optional": true,
"requires": {
@ -10199,7 +10183,7 @@
},
"readable-stream": {
"version": "2.3.6",
"resolved": "",
"resolved": false,
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
"optional": true,
"requires": {
@ -10214,7 +10198,7 @@
},
"rimraf": {
"version": "2.6.3",
"resolved": "",
"resolved": false,
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"optional": true,
"requires": {
@ -10223,43 +10207,43 @@
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "",
"resolved": false,
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"optional": true
},
"safer-buffer": {
"version": "2.1.2",
"resolved": "",
"resolved": false,
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
"optional": true
},
"sax": {
"version": "1.2.4",
"resolved": "",
"resolved": false,
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==",
"optional": true
},
"semver": {
"version": "5.7.0",
"resolved": "",
"resolved": false,
"integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==",
"optional": true
},
"set-blocking": {
"version": "2.0.0",
"resolved": "",
"resolved": false,
"integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=",
"optional": true
},
"signal-exit": {
"version": "3.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=",
"optional": true
},
"string-width": {
"version": "1.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
@ -10270,7 +10254,7 @@
},
"string_decoder": {
"version": "1.1.1",
"resolved": "",
"resolved": false,
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"optional": true,
"requires": {
@ -10279,7 +10263,7 @@
},
"strip-ansi": {
"version": "3.0.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": {
@ -10288,13 +10272,13 @@
},
"strip-json-comments": {
"version": "2.0.1",
"resolved": "",
"resolved": false,
"integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=",
"optional": true
},
"tar": {
"version": "4.4.8",
"resolved": "",
"resolved": false,
"integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==",
"optional": true,
"requires": {
@ -10309,13 +10293,13 @@
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
"optional": true
},
"wide-align": {
"version": "1.1.3",
"resolved": "",
"resolved": false,
"integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
"optional": true,
"requires": {
@ -10324,13 +10308,13 @@
},
"wrappy": {
"version": "1.0.2",
"resolved": "",
"resolved": false,
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
},
"yallist": {
"version": "3.0.3",
"resolved": "",
"resolved": false,
"integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==",
"optional": true
}
@ -10435,12 +10419,6 @@
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
"integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w=="
},
"get-params": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/get-params/-/get-params-0.1.2.tgz",
"integrity": "sha1-uuDfq6WIoMYNeDTA2Nwv9g7u8v4=",
"dev": true
},
"get-stream": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
@ -10931,12 +10909,6 @@
"safer-buffer": ">= 2.1.2 < 3"
}
},
"ieee754": {
"version": "1.1.13",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==",
"dev": true
},
"ignore": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz",
@ -10973,9 +10945,9 @@
"integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
},
"in-publish": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.0.tgz",
"integrity": "sha1-4g/146KvwmkDILbcVSaCqcf631E="
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/in-publish/-/in-publish-2.0.1.tgz",
"integrity": "sha512-oDM0kUSNFC31ShNxHKUyfZKy8ZeXZBWMjMdZHKLOk13uvT27VTL/QzRGfRUcevJhpkZAvlhPYuXkF7eNWrtyxQ=="
},
"indent-string": {
"version": "4.0.0",
@ -16858,12 +16830,6 @@
"esprima": "^4.0.0"
}
},
"jsan": {
"version": "3.1.13",
"resolved": "https://registry.npmjs.org/jsan/-/jsan-3.1.13.tgz",
"integrity": "sha512-9kGpCsGHifmw6oJet+y8HaCl14y7qgAsxVdV3pCHDySNR3BfDC30zgkssd7x5LRVAT22dnpbe9JdzzmXZnq9/g==",
"dev": true
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
@ -16977,7 +16943,8 @@
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
"integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=",
"dev": true
},
"json5": {
"version": "2.1.1",
@ -17106,12 +17073,6 @@
"integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=",
"dev": true
},
"linked-list": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/linked-list/-/linked-list-0.1.0.tgz",
"integrity": "sha1-eYsP+X0bkqT9CEgPVa6k6dSdN78=",
"dev": true
},
"load-json-file": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
@ -17180,11 +17141,6 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
},
"lodash-es": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
},
"lodash._baseisequal": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/lodash._baseisequal/-/lodash._baseisequal-3.0.7.tgz",
@ -18357,12 +18313,6 @@
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
"optional": true
},
"nanoid": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-2.1.7.tgz",
"integrity": "sha512-fmS3qwDldm4bE01HCIRqNk+f255CNjnAoeV3Zzzv0KemObHKqYgirVaZA9DtKcjogicWjYcHkJs4D5A8CjnuVQ==",
"dev": true
},
"nanomatch": {
"version": "1.2.13",
"resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz",
@ -19721,12 +19671,6 @@
"strict-uri-encode": "^2.0.0"
}
},
"querystring": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
"dev": true
},
"querystringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz",
@ -20310,6 +20254,11 @@
"resolved": "https://registry.npmjs.org/react-native-local-auth/-/react-native-local-auth-1.6.0.tgz",
"integrity": "sha512-36cYGZGCG82pMiVJbQa5WMA93khP4v5JqLutFkMyB/eRpCULHmojNIBlbUPIY9SCeN4sg5VBRFTVGCtTg2r2kA=="
},
"react-native-mmkv-storage": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/react-native-mmkv-storage/-/react-native-mmkv-storage-0.2.2.tgz",
"integrity": "sha512-QBMQzz7wpS2DGjOn5kIppqiD26p+jOJaJ0Am4DG2PImcUGe9iub5mkamz02mlqjyhq+8EhUes533uLK98APtmw=="
},
"react-native-navigation": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/react-native-navigation/-/react-native-navigation-6.3.0.tgz",
@ -20771,29 +20720,6 @@
"resolved": "https://registry.npmjs.org/redux-batched-actions/-/redux-batched-actions-0.4.1.tgz",
"integrity": "sha512-r6tLDyBP3U9cXNLEHs0n1mX5TQfmk6xE0Y9uinYZ5HOyAWDgIJxYqRRkU/bC6XrJ4nS7tasNbxaHJHVmf9UdkA=="
},
"redux-devtools-core": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/redux-devtools-core/-/redux-devtools-core-0.2.1.tgz",
"integrity": "sha512-RAGOxtUFdr/1USAvxrWd+Gq/Euzgw7quCZlO5TgFpDfG7rB5tMhZUrNyBjpzgzL2yMk0eHnPYIGm7NkIfRzHxQ==",
"dev": true,
"requires": {
"get-params": "^0.1.2",
"jsan": "^3.1.13",
"lodash": "^4.17.11",
"nanoid": "^2.0.0",
"remotedev-serialize": "^0.1.8"
}
},
"redux-devtools-instrument": {
"version": "1.9.6",
"resolved": "https://registry.npmjs.org/redux-devtools-instrument/-/redux-devtools-instrument-1.9.6.tgz",
"integrity": "sha512-MwvY4cLEB2tIfWWBzrUR02UM9qRG2i7daNzywRvabOSVdvAY7s9BxSwMmVRH1Y/7QWjplNtOwgT0apKhHg2Qew==",
"dev": true,
"requires": {
"lodash": "^4.2.0",
"symbol-observable": "^1.0.2"
}
},
"redux-mock-store": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.4.tgz",
@ -20803,23 +20729,10 @@
"lodash.isplainobject": "^4.0.6"
}
},
"redux-offline": {
"version": "github:mattermost/redux-offline#885024de96b6ec73650c340c8928066585c413df",
"from": "github:mattermost/redux-offline#885024de96b6ec73650c340c8928066585c413df",
"requires": {
"@react-native-community/netinfo": "^4.1.3",
"redux-persist": "^4.5.0"
}
},
"redux-persist": {
"version": "4.10.2",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-4.10.2.tgz",
"integrity": "sha512-U+e0ieMGC69Zr72929iJW40dEld7Mflh6mu0eJtVMLGfMq/aJqjxUM1hzyUWMR1VUyAEEdPHuQmeq5ti9krIgg==",
"requires": {
"json-stringify-safe": "^5.0.1",
"lodash": "^4.17.4",
"lodash-es": "^4.17.4"
}
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz",
"integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ=="
},
"redux-persist-node-storage": {
"version": "2.0.0",
@ -20844,6 +20757,11 @@
"lodash.unset": "^4.5.2"
}
},
"redux-reset": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/redux-reset/-/redux-reset-0.3.0.tgz",
"integrity": "sha1-q4B5W0ENWyhomYbqxAy4R6ac0BI="
},
"redux-thunk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.3.0.tgz",
@ -20999,29 +20917,6 @@
"es6-error": "^4.0.1"
}
},
"remote-redux-devtools": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/remote-redux-devtools/-/remote-redux-devtools-0.5.16.tgz",
"integrity": "sha512-xZ2D1VRIWzat5nsvcraT6fKEX9Cfi+HbQBCwzNnUAM8Uicm/anOc60XGalcaDPrVmLug7nhDl2nimEa3bL3K9w==",
"dev": true,
"requires": {
"jsan": "^3.1.13",
"querystring": "^0.2.0",
"redux-devtools-core": "^0.2.1",
"redux-devtools-instrument": "^1.9.4",
"rn-host-detect": "^1.1.5",
"socketcluster-client": "^14.2.1"
}
},
"remotedev-serialize": {
"version": "0.1.8",
"resolved": "https://registry.npmjs.org/remotedev-serialize/-/remotedev-serialize-0.1.8.tgz",
"integrity": "sha512-3YG/FDcOmiK22bl5oMRM8RRnbGrFEuPGjbcDG+z2xi5aQaNQNZ8lqoRnZTwXVfaZtutXuiAQOgPRrogzQk8edg==",
"dev": true,
"requires": {
"jsan": "^3.1.13"
}
},
"remove-trailing-separator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz",
@ -21236,12 +21131,6 @@
}
}
},
"rn-host-detect": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/rn-host-detect/-/rn-host-detect-1.2.0.tgz",
"integrity": "sha512-btNg5kzHcjZZ7t7mvvV/4wNJ9e3MPgrWivkRgWURzXL0JJ0pwWlU4zrbmdlz3HHzHOxhBhHB4D+/dbMFfu4/4A==",
"dev": true
},
"rn-placeholder": {
"version": "github:mattermost/rn-placeholder#02c629c65d0123a2eee623ada0fd17186415d3c3",
"from": "github:mattermost/rn-placeholder#02c629c65d0123a2eee623ada0fd17186415d3c3",
@ -21359,27 +21248,6 @@
"xmlchars": "^2.1.1"
}
},
"sc-channel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/sc-channel/-/sc-channel-1.2.0.tgz",
"integrity": "sha512-M3gdq8PlKg0zWJSisWqAsMmTVxYRTpVRqw4CWAdKBgAfVKumFcTjoCV0hYu7lgUXccCtCD8Wk9VkkE+IXCxmZA==",
"dev": true,
"requires": {
"component-emitter": "1.2.1"
}
},
"sc-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/sc-errors/-/sc-errors-2.0.1.tgz",
"integrity": "sha512-JoVhq3Ud+3Ujv2SIG7W0XtjRHsrNgl6iXuHHsh0s+Kdt5NwI6N2EGAZD4iteitdDv68ENBkpjtSvN597/wxPSQ==",
"dev": true
},
"sc-formatter": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/sc-formatter/-/sc-formatter-3.0.2.tgz",
"integrity": "sha512-9PbqYBpCq+OoEeRQ3QfFIGE6qwjjBcd2j7UjgDlhnZbtSnuGgHdcRklPKYGuYFH82V/dwd+AIpu8XvA1zqTd+A==",
"dev": true
},
"scheduler": {
"version": "0.15.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.15.0.tgz",
@ -21793,32 +21661,6 @@
}
}
},
"socketcluster-client": {
"version": "14.3.1",
"resolved": "https://registry.npmjs.org/socketcluster-client/-/socketcluster-client-14.3.1.tgz",
"integrity": "sha512-Sd/T0K/9UlqTfz+HUuFq90dshA5OBJPQbdkRzGtcKIOm52fkdsBTt0FYpiuzzxv5VrU7PWpRm6KIfNXyPwlLpw==",
"dev": true,
"requires": {
"buffer": "^5.2.1",
"clone": "2.1.1",
"component-emitter": "1.2.1",
"linked-list": "0.1.0",
"querystring": "0.2.0",
"sc-channel": "^1.2.0",
"sc-errors": "^2.0.1",
"sc-formatter": "^3.0.1",
"uuid": "3.2.1",
"ws": "7.1.0"
},
"dependencies": {
"uuid": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz",
"integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==",
"dev": true
}
}
},
"sort-json": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/sort-json/-/sort-json-2.0.0.tgz",
@ -23015,9 +22857,9 @@
"dev": true
},
"webpack-cli": {
"version": "3.3.10",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.10.tgz",
"integrity": "sha512-u1dgND9+MXaEt74sJR4PR7qkPxXUSQ0RXYq8x1L6Jg1MYVEmGPrH6Ah6C4arD4r0J1P5HKjRqpab36k0eIzPqg==",
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz",
"integrity": "sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g==",
"dev": true,
"requires": {
"chalk": "2.4.2",
@ -23119,9 +22961,9 @@
}
},
"yargs-parser": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz",
"integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==",
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",

View file

@ -51,6 +51,7 @@
"react-native-keychain": "4.0.5",
"react-native-linear-gradient": "2.5.6",
"react-native-local-auth": "1.6.0",
"react-native-mmkv-storage": "0.2.2",
"react-native-navigation": "6.3.0",
"react-native-notifications": "2.0.6",
"react-native-passcode-status": "1.1.2",
@ -71,9 +72,9 @@
"redux": "4.0.5",
"redux-action-buffer": "1.2.0",
"redux-batched-actions": "0.4.1",
"redux-offline": "github:mattermost/redux-offline#885024de96b6ec73650c340c8928066585c413df",
"redux-persist": "4.10.2",
"redux-persist": "6.0.0",
"redux-persist-transform-filter": "0.0.20",
"redux-reset": "0.3.0",
"redux-thunk": "2.3.0",
"reselect": "4.0.0",
"rn-fetch-blob": "0.12.0",
@ -133,7 +134,6 @@
"react-test-renderer": "16.13.0",
"redux-mock-store": "1.5.4",
"redux-persist-node-storage": "2.0.0",
"remote-redux-devtools": "0.5.16",
"socketcluster": "16.0.1",
"ts-jest": "25.2.1",
"typescript": "3.8.3",

View file

@ -151,12 +151,10 @@ module.exports = [
'app/mm-redux/selectors/entities/teams.ts',
'app/mm-redux/selectors/entities/timezone.ts',
'app/mm-redux/selectors/entities/users.ts',
'app/mm-redux/store/configureStore.prod.ts',
'app/mm-redux/store/helpers.ts',
'app/mm-redux/store/index.ts',
'app/mm-redux/store/initial_state.ts',
'app/mm-redux/store/middleware.ts',
'app/mm-redux/store/reducer_registry.ts',
'app/mm-redux/types/actions.ts',
'app/mm-redux/utils/channel_utils.ts',
'app/mm-redux/utils/emoji_utils.ts',
@ -837,19 +835,6 @@ module.exports = [
'node_modules/react/index.js',
'node_modules/redux-action-buffer/index.js',
'node_modules/redux-batched-actions/lib/index.js',
'node_modules/redux-offline/lib/actions.js',
'node_modules/redux-offline/lib/config.js',
'node_modules/redux-offline/lib/constants.js',
'node_modules/redux-offline/lib/defaults/batch.js',
'node_modules/redux-offline/lib/defaults/detectNetwork.native.js',
'node_modules/redux-offline/lib/defaults/discard.js',
'node_modules/redux-offline/lib/defaults/effect.js',
'node_modules/redux-offline/lib/defaults/index.js',
'node_modules/redux-offline/lib/defaults/persist.native.js',
'node_modules/redux-offline/lib/defaults/retry.js',
'node_modules/redux-offline/lib/index.js',
'node_modules/redux-offline/lib/middleware.js',
'node_modules/redux-offline/lib/updater.js',
'node_modules/redux-persist-transform-filter/dist/index.js',
'node_modules/redux-persist/lib/autoRehydrate.js',
'node_modules/redux-persist/lib/constants.js',

View file

@ -150,12 +150,10 @@ module.exports = [
'./node_modules/app/mm-redux/selectors/entities/teams.ts',
'./node_modules/app/mm-redux/selectors/entities/timezone.ts',
'./node_modules/app/mm-redux/selectors/entities/users.ts',
'./node_modules/app/mm-redux/store/configureStore.prod.ts',
'./node_modules/app/mm-redux/store/helpers.ts',
'./node_modules/app/mm-redux/store/index.ts',
'./node_modules/app/mm-redux/store/initial_state.ts',
'./node_modules/app/mm-redux/store/middleware.ts',
'./node_modules/app/mm-redux/store/reducer_registry.ts',
'./node_modules/app/mm-redux/types/actions.ts',
'./node_modules/app/mm-redux/utils/channel_utils.ts',
'./node_modules/app/mm-redux/utils/emoji_utils.ts',
@ -831,19 +829,6 @@ module.exports = [
'./node_modules/node_modules/react/index.js',
'./node_modules/node_modules/redux-action-buffer/index.js',
'./node_modules/node_modules/redux-batched-actions/lib/index.js',
'./node_modules/node_modules/redux-offline/lib/actions.js',
'./node_modules/node_modules/redux-offline/lib/config.js',
'./node_modules/node_modules/redux-offline/lib/constants.js',
'./node_modules/node_modules/redux-offline/lib/defaults/batch.js',
'./node_modules/node_modules/redux-offline/lib/defaults/detectNetwork.native.js',
'./node_modules/node_modules/redux-offline/lib/defaults/discard.js',
'./node_modules/node_modules/redux-offline/lib/defaults/effect.js',
'./node_modules/node_modules/redux-offline/lib/defaults/index.js',
'./node_modules/node_modules/redux-offline/lib/defaults/persist.native.js',
'./node_modules/node_modules/redux-offline/lib/defaults/retry.js',
'./node_modules/node_modules/redux-offline/lib/index.js',
'./node_modules/node_modules/redux-offline/lib/middleware.js',
'./node_modules/node_modules/redux-offline/lib/updater.js',
'./node_modules/node_modules/redux-persist-transform-filter/dist/index.js',
'./node_modules/node_modules/redux-persist/lib/autoRehydrate.js',
'./node_modules/node_modules/redux-persist/lib/constants.js',

View file

@ -87,6 +87,10 @@ jest.doMock('react-native', () => {
pick: jest.fn(),
},
RNPermissions: {},
RNFastStorage: {
setupLibrary: jest.fn(),
setStringAsync: jest.fn(),
},
};
return Object.setPrototypeOf({

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {AsyncNodeStorage} from 'redux-persist-node-storage';
import {createTransform, persistStore} from 'redux-persist';
import {createTransform} from 'redux-persist';
import configureStore from '@mm-redux/store';
@ -11,29 +11,16 @@ export default async function testConfigureStore(preloadedState) {
() => ({}),
);
const offlineConfig = {
detectNetwork: (callback) => callback(true),
persist: (store, options) => {
return persistStore(store, {storage: new AsyncNodeStorage('./.tmp'), ...options});
},
persistOptions: {
debounce: 1000,
transforms: [
storageTransform,
],
whitelist: [],
},
retry: (action, retries) => 200 * (retries + 1),
discard: (error, action, retries) => {
if (action.meta && action.meta.offline.hasOwnProperty('maxRetry')) {
return retries >= action.meta.offline.maxRetry;
}
return retries >= 1;
},
const persistConfig = {
key: 'root',
storage: new AsyncNodeStorage('./.tmp'),
whitelist: [],
transforms: [
storageTransform,
],
};
const store = configureStore(preloadedState, {}, offlineConfig, () => ({}), {enableBuffer: false});
const {store} = configureStore(preloadedState, {}, persistConfig, () => ({}), {enableBuffer: false});
const wait = () => new Promise((resolve) => setTimeout(resolve), 300); //eslint-disable-line
await wait();

View file

@ -34,6 +34,7 @@
"@actions/*": ["app/actions/*"],
"@constants/*": ["app/constants/*"],
"@constants": ["app/constants/index"],
"@i18n": ["app/i18n/index"],
"@selectors/*": ["app/selectors/*"],
"@telemetry/*": ["/app/telemetry/*"],
"@utils/*": ["app/utils/*"],