* MM-23848 consolidate store, upgrade mmkv and fix logout * Feedback review * Add store.ts to modulesPath
242 lines
8 KiB
TypeScript
242 lines
8 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import AsyncStorage from '@react-native-community/async-storage';
|
|
import * as redux from 'redux';
|
|
import {createPersistoid, createTransform, persistReducer, persistStore, Persistor, PersistConfig} from 'redux-persist';
|
|
import {createBlacklistFilter} from 'redux-persist-transform-filter';
|
|
import reduxReset from 'redux-reset';
|
|
|
|
import {General} from '@mm-redux/constants';
|
|
import serviceReducer from '@mm-redux/reducers';
|
|
import {GlobalState} from '@mm-redux/types/store';
|
|
|
|
import initialState from '@store/initial_state';
|
|
import appReducer from 'app/reducers';
|
|
|
|
import {createReducer, getStoredState} from './helpers';
|
|
import {createMiddlewares} from './middlewares';
|
|
import Store from './store';
|
|
import {transformSet} from './utils';
|
|
|
|
/**
|
|
* 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 = {
|
|
enableBuffer: boolean;
|
|
enableThunk: boolean;
|
|
}
|
|
|
|
type V4Store = {
|
|
storeKeys: Array<string>;
|
|
restoredState: any;
|
|
}
|
|
|
|
const usersSetTransform = [
|
|
'profilesInChannel',
|
|
'profilesNotInChannel',
|
|
'profilesInTeam',
|
|
'profilesNotInTeam',
|
|
];
|
|
|
|
const channelSetTransform = [
|
|
'channelsInTeam',
|
|
];
|
|
|
|
const rolesSetTransform = [
|
|
'pending',
|
|
];
|
|
|
|
const setTransforms = [
|
|
...usersSetTransform,
|
|
...channelSetTransform,
|
|
...rolesSetTransform,
|
|
];
|
|
|
|
const viewsBlackListFilter = createBlacklistFilter(
|
|
'views',
|
|
['extension', 'root'],
|
|
);
|
|
|
|
const typingBlackListFilter = createBlacklistFilter(
|
|
'entities',
|
|
['typing'],
|
|
);
|
|
|
|
const channelViewBlackList: any = {loading: true, refreshing: true, loadingPosts: true, retryFailed: true, loadMorePostsVisible: true};
|
|
const channelViewBlackListFilter = createTransform(
|
|
(inboundState: any) => {
|
|
const channel: any = {};
|
|
const keys: Array<string> = inboundState.channel ? Object.keys(inboundState.channel) : [];
|
|
|
|
for (const channelKey of keys) {
|
|
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: any = {nonExistentEmoji: true};
|
|
const emojiBlackListFilter = createTransform(
|
|
(inboundState: any) => {
|
|
const emojis: any = {};
|
|
const keys: Array<string> = inboundState.emojis ? Object.keys(inboundState.emojis) : [];
|
|
|
|
for (const emojiKey of keys) {
|
|
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: any, key: string) => {
|
|
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: any, key: string) => {
|
|
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;
|
|
},
|
|
);
|
|
|
|
/**
|
|
* Function to configure the redux store with persistence
|
|
* @param storage the storage engine to use
|
|
* @param preloadedState (optional) the initial state to use (applies to tests)
|
|
* @param optionalConfig (optional) persist configuration (applies to tests)
|
|
* @param optionalOptions (optional) middleware configuration (applies to tests)
|
|
*/
|
|
export default function configureStore(storage: any, preloadedState: any = {}, optionalConfig: {}, optionalOptions = {}): ReduxStore {
|
|
const defaultOptions: ClientOptions = {
|
|
enableBuffer: true,
|
|
enableThunk: true,
|
|
};
|
|
|
|
const defaultConfig: PersistConfig<GlobalState> = {
|
|
key: 'root',
|
|
storage,
|
|
serialize: (state: GlobalState) => ({...state}),
|
|
deserialize: false,
|
|
blacklist: ['device', 'navigation', 'requests', '_persist'],
|
|
transforms: [
|
|
setTransformer,
|
|
viewsBlackListFilter,
|
|
typingBlackListFilter,
|
|
channelViewBlackListFilter,
|
|
emojiBlackListFilter,
|
|
],
|
|
throttle: 100,
|
|
};
|
|
|
|
const persistConfig: PersistConfig<GlobalState> = Object.assign({}, defaultConfig, optionalConfig);
|
|
const baseState: any = Object.assign({}, initialState, preloadedState);
|
|
const rootReducer: any = createReducer(serviceReducer as any, appReducer as any);
|
|
const persistedReducer = persistReducer({...persistConfig}, rootReducer);
|
|
const options: ClientOptions = Object.assign({}, defaultOptions, optionalOptions);
|
|
|
|
const store = redux.createStore(
|
|
persistedReducer,
|
|
baseState,
|
|
redux.compose(
|
|
redux.applyMiddleware(
|
|
...createMiddlewares(options),
|
|
),
|
|
reduxReset(General.OFFLINE_STORE_PURGE),
|
|
),
|
|
);
|
|
|
|
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 {
|
|
let executed = false;
|
|
const unsubscribe = store.subscribe(() => {
|
|
if (store.getState()._persist?.rehydrated && !executed) { // eslint-disable-line no-underscore-dangle
|
|
unsubscribe();
|
|
executed = true;
|
|
store.dispatch({type: General.REHYDRATED});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
Store.redux = store;
|
|
Store.persistor = persistor;
|
|
|
|
return {store, persistor};
|
|
}
|