// 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; 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 = 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 = 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 = { 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 = 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}; }