Revert "[MM-23490] Save state to file via async middleware vs store subscription (#4059)"
This reverts commit 1d7149a26d.
This commit is contained in:
parent
1d7149a26d
commit
844d81072f
5 changed files with 247 additions and 282 deletions
|
|
@ -143,6 +143,7 @@ const state = {
|
|||
root: {
|
||||
deepLinkURL: '',
|
||||
hydrationComplete: false,
|
||||
purge: false,
|
||||
},
|
||||
selectServer: {
|
||||
serverUrl: Config.DefaultServerUrl,
|
||||
|
|
|
|||
|
|
@ -26,7 +26,17 @@ function hydrationComplete(state = false, action) {
|
|||
}
|
||||
}
|
||||
|
||||
function purge(state = false, action) {
|
||||
switch (action.type) {
|
||||
case General.OFFLINE_STORE_PURGE:
|
||||
return true;
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default combineReducers({
|
||||
deepLinkURL,
|
||||
hydrationComplete,
|
||||
purge,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,23 +1,10 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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 {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 EphemeralStore from './ephemeral_store';
|
||||
|
||||
|
|
@ -26,124 +13,7 @@ import {
|
|||
LOGGER_JAVASCRIPT_WARNING,
|
||||
} from 'app/utils/sentry';
|
||||
|
||||
const SAVE_STATE_ACTIONS = [
|
||||
'CONNECTION_CHANGED',
|
||||
'DATA_CLEANUP',
|
||||
'LOGIN',
|
||||
'Offline/STATUS_CHANGED',
|
||||
'persist/REHYDRATE',
|
||||
'RECEIVED_APP_STATE',
|
||||
'WEBSOCKET_CLOSED',
|
||||
'WEBSOCKET_SUCCESS',
|
||||
];
|
||||
|
||||
// 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) => {
|
||||
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();
|
||||
|
||||
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]);
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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) => {
|
||||
export function messageRetention(store) {
|
||||
return (next) => (action) => {
|
||||
if (action.type === 'persist/REHYDRATE') {
|
||||
const {app} = action.payload;
|
||||
|
|
@ -188,7 +58,7 @@ const messageRetention = (store) => {
|
|||
|
||||
return next(action);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function resetStateForNewVersion(action) {
|
||||
const {payload} = action;
|
||||
|
|
@ -564,17 +434,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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,12 +10,10 @@ import {
|
|||
cleanUpPostsInChannel,
|
||||
cleanUpState,
|
||||
getAllFromPostsInChannel,
|
||||
middlewares,
|
||||
messageRetention,
|
||||
} from 'app/store/middleware';
|
||||
|
||||
describe('messageRetention', () => {
|
||||
const messageRetention = middlewares()[0];
|
||||
|
||||
describe('should chain the same incoming action type', () => {
|
||||
const actions = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,21 +1,29 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {batchActions} from 'redux-batched-actions';
|
||||
import {Platform} from 'react-native';
|
||||
import AsyncStorage from '@react-native-community/async-storage';
|
||||
import {createBlacklistFilter} from 'redux-persist-transform-filter';
|
||||
import {createTransform, persistStore} from 'redux-persist';
|
||||
|
||||
import {General} from '@mm-redux/constants';
|
||||
import {getConfig} from '@mm-redux/selectors/entities/general';
|
||||
import configureStore from '@mm-redux/store';
|
||||
import {ErrorTypes, GeneralTypes} from 'mattermost-redux/action_types';
|
||||
import {General} from 'mattermost-redux/constants';
|
||||
import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
||||
import configureStore from 'mattermost-redux/store';
|
||||
import EventEmitter from 'mattermost-redux/utils/event_emitter';
|
||||
|
||||
import {NavigationTypes, ViewTypes} from 'app/constants';
|
||||
import appReducer from 'app/reducers';
|
||||
import {throttle} from 'app/utils/general';
|
||||
import {getSiteUrl, setSiteUrl} from 'app/utils/image_cache_manager';
|
||||
import {createSentryMiddleware} from 'app/utils/sentry/middleware';
|
||||
|
||||
import {middlewares} from './middleware';
|
||||
import mattermostBucket from 'app/mattermost_bucket';
|
||||
|
||||
import {messageRetention} from './middleware';
|
||||
import {createThunkMiddleware} from './thunk';
|
||||
import {transformSet} from './utils';
|
||||
import {transformSet, getStateForReset} from './utils';
|
||||
|
||||
function getAppReducer() {
|
||||
return require('../../app/reducers'); // eslint-disable-line global-require
|
||||
|
|
@ -42,140 +50,232 @@ const setTransforms = [
|
|||
...rolesSetTransform,
|
||||
];
|
||||
|
||||
const viewsBlackListFilter = createBlacklistFilter(
|
||||
'views',
|
||||
['extension', 'root'],
|
||||
);
|
||||
|
||||
const typingBlackListFilter = createBlacklistFilter(
|
||||
'entities',
|
||||
['typing'],
|
||||
);
|
||||
|
||||
const channelViewBlackList = {loading: true, refreshing: true, loadingPosts: true, retryFailed: true, loadMorePostsVisible: true};
|
||||
const channelViewBlackListFilter = createTransform(
|
||||
(inboundState) => {
|
||||
const channel = {};
|
||||
|
||||
for (const channelKey of Object.keys(inboundState.channel)) {
|
||||
if (!channelViewBlackList[channelKey]) {
|
||||
channel[channelKey] = inboundState.channel[channelKey];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...inboundState,
|
||||
channel,
|
||||
};
|
||||
},
|
||||
null,
|
||||
{whitelist: ['views']}, // Only run this filter on the views state (or any other entry that ends up being named views)
|
||||
);
|
||||
|
||||
const emojiBlackList = {nonExistentEmoji: true};
|
||||
const emojiBlackListFilter = createTransform(
|
||||
(inboundState) => {
|
||||
const emojis = {};
|
||||
|
||||
for (const emojiKey of Object.keys(inboundState.emojis)) {
|
||||
if (!emojiBlackList[emojiKey]) {
|
||||
emojis[emojiKey] = inboundState.emojis[emojiKey];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...inboundState,
|
||||
emojis,
|
||||
};
|
||||
},
|
||||
null,
|
||||
{whitelist: ['entities']}, // Only run this filter on the entities state (or any other entry that ends up being named entities)
|
||||
);
|
||||
|
||||
const setTransformer = createTransform(
|
||||
(inboundState, key) => {
|
||||
if (key === 'entities') {
|
||||
const state = {...inboundState};
|
||||
for (const prop in state) {
|
||||
if (state.hasOwnProperty(prop)) {
|
||||
state[prop] = transformSet(state[prop], setTransforms);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
return inboundState;
|
||||
},
|
||||
(outboundState, key) => {
|
||||
if (key === 'entities') {
|
||||
const state = {...outboundState};
|
||||
for (const prop in state) {
|
||||
if (state.hasOwnProperty(prop)) {
|
||||
state[prop] = transformSet(state[prop], setTransforms, false);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
return outboundState;
|
||||
},
|
||||
);
|
||||
|
||||
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,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default function configureAppStore(initialState) {
|
||||
const viewsBlackListFilter = createBlacklistFilter(
|
||||
'views',
|
||||
['extension', 'login', 'root'],
|
||||
);
|
||||
|
||||
const typingBlackListFilter = createBlacklistFilter(
|
||||
'entities',
|
||||
['typing'],
|
||||
);
|
||||
|
||||
const channelViewBlackList = {loading: true, refreshing: true, loadingPosts: true, retryFailed: true, loadMorePostsVisible: true};
|
||||
const channelViewBlackListFilter = createTransform(
|
||||
(inboundState) => {
|
||||
const channel = {};
|
||||
|
||||
for (const channelKey of Object.keys(inboundState.channel)) {
|
||||
if (!channelViewBlackList[channelKey]) {
|
||||
channel[channelKey] = inboundState.channel[channelKey];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...inboundState,
|
||||
channel,
|
||||
};
|
||||
},
|
||||
null,
|
||||
{whitelist: ['views']}, // Only run this filter on the views state (or any other entry that ends up being named views)
|
||||
);
|
||||
|
||||
const emojiBlackList = {nonExistentEmoji: true};
|
||||
const emojiBlackListFilter = createTransform(
|
||||
(inboundState) => {
|
||||
const emojis = {};
|
||||
|
||||
for (const emojiKey of Object.keys(inboundState.emojis)) {
|
||||
if (!emojiBlackList[emojiKey]) {
|
||||
emojis[emojiKey] = inboundState.emojis[emojiKey];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...inboundState,
|
||||
emojis,
|
||||
};
|
||||
},
|
||||
null,
|
||||
{whitelist: ['entities']}, // Only run this filter on the entities state (or any other entry that ends up being named entities)
|
||||
);
|
||||
|
||||
const setTransformer = createTransform(
|
||||
(inboundState, key) => {
|
||||
if (key === 'entities') {
|
||||
const state = {...inboundState};
|
||||
for (const prop in state) {
|
||||
if (state.hasOwnProperty(prop)) {
|
||||
state[prop] = transformSet(state[prop], setTransforms);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
return inboundState;
|
||||
},
|
||||
(outboundState, key) => {
|
||||
if (key === 'entities') {
|
||||
const state = {...outboundState};
|
||||
for (const prop in state) {
|
||||
if (state.hasOwnProperty(prop)) {
|
||||
state[prop] = transformSet(state[prop], setTransforms, false);
|
||||
}
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
return outboundState;
|
||||
},
|
||||
);
|
||||
|
||||
const offlineOptions = {
|
||||
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,
|
||||
});
|
||||
});
|
||||
|
||||
let purging = false;
|
||||
|
||||
// for iOS write the entities to a shared file
|
||||
if (Platform.OS === 'ios') {
|
||||
store.subscribe(throttle(() => {
|
||||
const state = store.getState();
|
||||
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]);
|
||||
});
|
||||
|
||||
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));
|
||||
}
|
||||
}, 1000));
|
||||
}
|
||||
|
||||
// check to see if the logout request was successful
|
||||
store.subscribe(async () => {
|
||||
const state = store.getState();
|
||||
const config = getConfig(state);
|
||||
|
||||
if (getSiteUrl() !== config?.SiteURL) {
|
||||
setSiteUrl(config.SiteURL);
|
||||
}
|
||||
|
||||
if (state.views.root.purge && !purging) {
|
||||
purging = true;
|
||||
|
||||
await persistor.purge();
|
||||
|
||||
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(() => {
|
||||
purging = false;
|
||||
EventEmitter.emit(NavigationTypes.RESTART_APP);
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
return persistor;
|
||||
},
|
||||
persistOptions: {
|
||||
autoRehydrate: {
|
||||
log: false,
|
||||
},
|
||||
blacklist: ['device', 'navigation', 'offline', 'requests'],
|
||||
transforms: [
|
||||
setTransformer,
|
||||
viewsBlackListFilter,
|
||||
typingBlackListFilter,
|
||||
channelViewBlackListFilter,
|
||||
emojiBlackListFilter,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const clientOptions = {
|
||||
additionalMiddleware: [
|
||||
createThunkMiddleware(),
|
||||
createSentryMiddleware(),
|
||||
...middlewares(persistConfig),
|
||||
messageRetention,
|
||||
],
|
||||
enableThunk: false, // We override the default thunk middleware
|
||||
};
|
||||
|
||||
return configureStore(initialState, appReducer, persistConfig, getAppReducer, clientOptions);
|
||||
return configureStore(initialState, appReducer, offlineOptions, getAppReducer, clientOptions);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue