mattermost-mobile/app/store/index.ts
Elias Nahum 0e2bc65d70
MM-23848 consolidate store, upgrade mmkv and fix logout (#4145)
* MM-23848 consolidate store, upgrade mmkv and fix logout

* Feedback review

* Add store.ts to modulesPath
2020-04-18 10:18:24 -04:00

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