Commit 399de2b1 authored by Jesús Espino's avatar Jesús Espino Committed by Joram Wilander

Migrate BrowserStore to redux (#241)

parent 94632b08
......@@ -460,7 +460,7 @@ export function emitUserLoggedOutEvent(redirectTo = '/', shouldSignalLogout = tr
}
export function clientLogout(redirectTo = '/') {
BrowserStore.clear();
BrowserStore.clear({exclude: [Constants.RECENT_EMOJI_KEY, '__landingPageSeen__', 'selected_teams']});
ErrorStore.clearLastError();
ChannelStore.clear();
stopPeriodicStatusUpdates();
......
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {StorageTypes} from 'utils/constants';
import {getPrefix} from 'utils/storage_utils';
export function setItem(name, value) {
return async (dispatch, getState) => {
const state = getState();
const prefix = getPrefix(state);
dispatch({
type: StorageTypes.SET_ITEM,
data: {prefix, name, value}
}, getState);
return {data: true};
};
}
export function removeItem(name) {
return async (dispatch, getState) => {
const state = getState();
const prefix = getPrefix(state);
dispatch({
type: StorageTypes.REMOVE_ITEM,
data: {prefix, name}
}, getState);
return {data: true};
};
}
export function setGlobalItem(name, value) {
return async (dispatch, getState) => {
dispatch({
type: StorageTypes.SET_GLOBAL_ITEM,
data: {name, value}
}, getState);
return {data: true};
};
}
export function removeGlobalItem(name) {
return async (dispatch, getState) => {
dispatch({
type: StorageTypes.REMOVE_GLOBAL_ITEM,
data: {name}
}, getState);
return {data: true};
};
}
export function clear(options) {
return async (dispatch, getState) => {
dispatch({
type: StorageTypes.CLEAR,
data: options
}, getState);
return {data: false};
};
}
export function actionOnGlobalItemsWithPrefix(prefix, action) {
return async (dispatch, getState) => {
dispatch({
type: StorageTypes.ACTION_ON_GLOBAL_ITEMS_WITH_PREFIX,
data: {prefix, action}
}, getState);
return {data: false};
};
}
export function actionOnItemsWithPrefix(prefix, action) {
return async (dispatch, getState) => {
const state = getState();
const globalPrefix = getPrefix(state);
dispatch({
type: StorageTypes.ACTION_ON_ITEMS_WITH_PREFIX,
data: {globalPrefix, prefix, action}
}, getState);
return {data: false};
};
}
......@@ -3,8 +3,10 @@
import plugins from './plugins';
import views from './views';
import storage from './storage';
export default {
views,
plugins
plugins,
storage
};
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {StorageTypes} from 'utils/constants';
export default function storage(state = {}, action) {
const nextState = {...state};
var key;
switch (action.type) {
case StorageTypes.SET_ITEM: {
nextState[action.data.prefix + action.data.name] = action.data.value;
return nextState;
}
case StorageTypes.REMOVE_ITEM: {
Reflect.deleteProperty(nextState, action.data.prefix + action.data.name);
return nextState;
}
case StorageTypes.SET_GLOBAL_ITEM: {
nextState[action.data.name] = action.data.value;
return nextState;
}
case StorageTypes.REMOVE_GLOBAL_ITEM: {
Reflect.deleteProperty(nextState, action.data.name);
return nextState;
}
case StorageTypes.CLEAR: {
var cleanState = {};
action.data.exclude.forEach((excluded) => {
if (state[excluded]) {
cleanState[excluded] = state[excluded];
}
});
return cleanState;
}
case StorageTypes.ACTION_ON_GLOBAL_ITEMS_WITH_PREFIX: {
for (key in state) {
if (key.lastIndexOf(action.data.prefix, 0) === 0) {
nextState[key] = action.data.action(key, state[key]);
}
}
return nextState;
}
case StorageTypes.ACTION_ON_ITEMS_WITH_PREFIX: {
var globalPrefix = action.data.globalPrefix;
var globalPrefixLen = action.data.globalPrefix.length;
for (key in state) {
if (key.lastIndexOf(globalPrefix + action.data.prefix, 0) === 0) {
var userkey = key.substring(globalPrefixLen);
nextState[key] = action.data.action(userkey, state[key]);
}
}
return nextState;
}
default:
return state;
}
}
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import {getPrefix} from 'utils/storage_utils';
function getGlobalItem(state, name, defaultValue) {
if (state && state.storage && typeof state.storage[name] !== 'undefined' && state.storage[name] !== null) {
return state.storage[name];
}
return defaultValue;
}
export function makeGetItem(name, defaultValue) {
return (state) => {
return getGlobalItem(state, getPrefix(state) + name, defaultValue);
};
}
export function makeGetGlobalItem(name, defaultValue) {
return (state) => {
return getGlobalItem(state, name, defaultValue);
};
}
......@@ -4,7 +4,8 @@
import {batchActions} from 'redux-batched-actions';
import {createTransform, persistStore} from 'redux-persist';
import localForage from 'localforage';
import localForage from "localforage";
import { extendPrototype } from "localforage-observable";
import {General, RequestStatus} from 'mattermost-redux/constants';
import configureServiceStore from 'mattermost-redux/store';
......@@ -34,7 +35,7 @@ const setTransforms = [
...teamSetTransform
];
export default function configureStore(initialState) {
export default function configureStore(initialState, persistorStorage = null) {
const setTransformer = createTransform(
(inboundState, key) => {
if (key === 'entities') {
......@@ -68,13 +69,37 @@ export default function configureStore(initialState) {
const offlineOptions = {
persist: (store, options) => {
const persistor = persistStore(store, {storage: localForage, ...options}, () => {
const localforage = extendPrototype(localForage);
var storage = persistorStorage || localforage;
const KEY_PREFIX = "reduxPersist:";
const persistor = persistStore(store, {storage, keyPrefix: KEY_PREFIX, ...options}, () => {
store.dispatch({
type: General.STORE_REHYDRATION_COMPLETE,
complete: true
});
});
if (localforage === storage) {
localforage.ready(() => {
localforage.configObservables({
crossTabNotification: true,
});
var observable = localforage.newObservable({
crossTabNotification: true,
changeDetection: true
});
observable.subscribe({
next: (args) => {
if(args.key && args.key.indexOf(KEY_PREFIX) === 0){
var keyspace = args.key.substr(KEY_PREFIX.length)
var statePartial = {}
statePartial[keyspace] = args.newValue
persistor.rehydrate(statePartial, {serial: true})
}
}
})
})
}
let purging = false;
// check to see if the logout request was successful
......
......@@ -3,85 +3,40 @@
import {browserHistory} from 'react-router/es6';
import * as Selectors from 'selectors/storage';
import * as Actions from 'actions/storage';
import store from 'stores/redux_store.jsx';
import {Constants, ErrorPageTypes} from 'utils/constants.jsx';
import {ErrorPageTypes} from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
function getPrefix() {
const state = store.getState();
if (state && state.entities && state.entities.users) {
const user = state.entities.users.profiles[state.entities.users.currentUserId];
if (user) {
return user.id + '_';
}
}
console.warn('BrowserStore tried to operate without user present'); //eslint-disable-line no-console
return 'unknown_';
}
const dispatch = store.dispatch;
const getState = store.getState;
class BrowserStoreClass {
constructor() {
this.hasCheckedLocalStorage = false;
this.localStorageSupported = false;
}
setItem(name, value) {
this.setGlobalItem(getPrefix() + name, value);
dispatch(Actions.setItem(name, value));
}
getItem(name, defaultValue) {
return this.getGlobalItem(getPrefix() + name, defaultValue);
return Selectors.makeGetItem(name, defaultValue)(getState());
}
removeItem(name) {
this.removeGlobalItem(getPrefix() + name);
dispatch(Actions.removeItem(name));
}
setGlobalItem(name, value) {
try {
if (this.isLocalStorageSupported()) {
localStorage.setItem(name, JSON.stringify(value));
} else {
sessionStorage.setItem(name, JSON.stringify(value));
}
} catch (err) {
console.log('An error occurred while setting local storage, clearing all props'); //eslint-disable-line no-console
localStorage.clear();
sessionStorage.clear();
window.location.reload(true);
}
dispatch(Actions.setGlobalItem(name, value));
}
getGlobalItem(name, defaultValue = null) {
var result = null;
try {
if (this.isLocalStorageSupported()) {
result = JSON.parse(localStorage.getItem(name));
} else {
result = JSON.parse(sessionStorage.getItem(name));
}
} catch (err) {
result = null;
}
if (typeof result === 'undefined' || result === null) {
result = defaultValue;
}
return result;
return Selectors.makeGetGlobalItem(name, defaultValue)(getState());
}
removeGlobalItem(name) {
if (this.isLocalStorageSupported()) {
localStorage.removeItem(name);
} else {
sessionStorage.removeItem(name);
}
dispatch(Actions.removeGlobalItem(name));
}
signalLogout() {
......@@ -119,54 +74,15 @@ class BrowserStoreClass {
* Signature for action is action(key, value)
*/
actionOnGlobalItemsWithPrefix(prefix, action) {
var storage = sessionStorage;
if (this.isLocalStorageSupported()) {
storage = localStorage;
}
for (var key in storage) {
if (key.lastIndexOf(prefix, 0) === 0) {
action(key, this.getGlobalItem(key));
}
}
dispatch(Actions.actionOnGlobalItemsWithPrefix(prefix, action));
}
actionOnItemsWithPrefix(prefix, action) {
var globalPrefix = getPrefix();
var globalPrefixiLen = globalPrefix.length;
for (var key in sessionStorage) {
if (key.lastIndexOf(globalPrefix + prefix, 0) === 0) {
var userkey = key.substring(globalPrefixiLen);
action(userkey, this.getGlobalItem(key));
}
}
dispatch(Actions.actionOnItemsWithPrefix(prefix, action));
}
clear() {
// persist some values through logout since they're independent of which user is logged in
const logoutId = sessionStorage.getItem('__logout__');
const landingPageSeen = this.hasSeenLandingPage();
const selectedTeams = this.getItem('selected_teams');
const recentEmojis = localStorage.getItem(Constants.RECENT_EMOJI_KEY);
sessionStorage.clear();
localStorage.clear();
if (recentEmojis) {
localStorage.setItem(Constants.RECENT_EMOJI_KEY, recentEmojis);
}
if (logoutId) {
sessionStorage.setItem('__logout__', logoutId);
}
if (landingPageSeen) {
this.setLandingPageSeen(landingPageSeen);
}
if (selectedTeams) {
this.setItem('selected_teams', selectedTeams);
}
clear(options) {
dispatch(Actions.clear(options));
}
isLocalStorageSupported() {
......@@ -200,15 +116,11 @@ class BrowserStoreClass {
}
hasSeenLandingPage() {
if (this.isLocalStorageSupported()) {
return JSON.parse(sessionStorage.getItem('__landingPageSeen__'));
}
return true;
return this.getItem('__landingPageSeen__', false);
}
setLandingPageSeen(landingPageSeen) {
return sessionStorage.setItem('__landingPageSeen__', JSON.stringify(landingPageSeen));
return this.setItem('__landingPageSeen__', landingPageSeen);
}
}
......
......@@ -183,18 +183,18 @@ class PostStoreClass extends EventEmitter {
clearDraftUploads() {
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', (key, value) => {
if (value) {
value.uploadsInProgress = [];
BrowserStore.setGlobalItem(key, value);
return {...value, uploadsInProgress: []};
}
return value;
});
}
clearCommentDraftUploads() {
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => {
if (value) {
value.uploadsInProgress = [];
BrowserStore.setGlobalItem(key, value);
return {...value, uploadsInProgress: []};
}
return value;
});
}
......
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import assert from 'assert';
import * as Actions from 'actions/storage';
import configureStore from 'store';
describe('Actions.Storage', () => {
let store;
beforeEach(async () => {
store = await configureStore();
});
it('setItem', async () => {
await Actions.setItem('test', 'value')(store.dispatch, store.getState);
assert.deepEqual(
store.getState().storage,
{unknown_test: 'value'}
);
});
it('removeItem', async () => {
await Actions.setItem('test1', 'value1')(store.dispatch, store.getState);
await Actions.setItem('test2', 'value2')(store.dispatch, store.getState);
assert.deepEqual(
store.getState().storage,
{
unknown_test1: 'value1',
unknown_test2: 'value2'
}
);
await Actions.removeItem('test1')(store.dispatch, store.getState);
assert.deepEqual(
store.getState().storage,
{unknown_test2: 'value2'}
);
});
it('setGlobalItem', async () => {
await Actions.setGlobalItem('test', 'value')(store.dispatch, store.getState);
assert.deepEqual(
store.getState().storage,
{test: 'value'}
);
});
it('removeGlobalItem', async () => {
await Actions.setGlobalItem('test1', 'value1')(store.dispatch, store.getState);
await Actions.setGlobalItem('test2', 'value2')(store.dispatch, store.getState);
assert.deepEqual(
store.getState().storage,
{
test1: 'value1',
test2: 'value2'
}
);
await Actions.removeGlobalItem('test1')(store.dispatch, store.getState);
assert.deepEqual(
store.getState().storage,
{test2: 'value2'}
);
});
it('actionOnGlobalItemsWithPrefix', async () => {
var touchedPairs = [];
await Actions.setGlobalItem('prefix_test1', 1)(store.dispatch, store.getState);
await Actions.setGlobalItem('prefix_test2', 2)(store.dispatch, store.getState);
await Actions.setGlobalItem('not_prefix_test', 3)(store.dispatch, store.getState);
await Actions.actionOnGlobalItemsWithPrefix(
'prefix',
(key, value) => touchedPairs.push([key, value])
)(store.dispatch, store.getState);
assert.deepEqual(
touchedPairs,
[['prefix_test1', 1], ['prefix_test2', 2]]
);
});
it('actionOnItemsWithPrefix', async () => {
var touchedPairs = [];
await Actions.setItem('prefix_test1', 1)(store.dispatch, store.getState);
await Actions.setItem('prefix_test2', 2)(store.dispatch, store.getState);
await Actions.setItem('not_prefix_test', 3)(store.dispatch, store.getState);
await Actions.actionOnItemsWithPrefix(
'prefix',
(key, value) => touchedPairs.push([key, value])
)(store.dispatch, store.getState);
assert.deepEqual(
touchedPairs,
[['prefix_test1', 1], ['prefix_test2', 2]]
);
});
it('clear', async () => {
await Actions.setGlobalItem('key', 'value')(store.dispatch, store.getState);
await Actions.setGlobalItem('excluded', 'not-cleared')(store.dispatch, store.getState);
await Actions.clear({exclude: ['excluded']})(store.dispatch, store.getState);
assert.deepEqual(
store.getState().storage,
{excluded: 'not-cleared'}
);
});
});
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import assert from 'assert';
import storageReducer from 'reducers/storage';
import {StorageTypes} from 'utils/constants';
describe('Reducers.Storage', () => {
it('Storage.SET_ITEM', async () => {
const nextState = storageReducer(
{},
{
type: StorageTypes.SET_ITEM,
data: {
name: 'key',
prefix: 'user_id_',
value: 'value'
}
}
);
assert.deepEqual(
nextState,
{
user_id_key: 'value'
}
);
});
it('Storage.SET_GLOBAL_ITEM', async () => {
const nextState = storageReducer(
{},
{
type: StorageTypes.SET_GLOBAL_ITEM,
data: {
name: 'key',
value: 'value'
}
}
);
assert.deepEqual(
nextState,
{
key: 'value'
}
);
});
it('Storage.REMOVE_ITEM', async () => {
var nextState = storageReducer(
{
user_id_key: 'value'
},
{
type: StorageTypes.REMOVE_ITEM,
data: {
name: 'key',
prefix: 'user_id_'
}
}
);
assert.deepEqual(
nextState,
{}
);
nextState = storageReducer(
{},
{
type: StorageTypes.REMOVE_ITEM,
data: {
name: 'key',
prefix: 'user_id_'
}
}
);
assert.deepEqual(
nextState,
{}
);
});
it('Storage.REMOVE_GLOBAL_ITEM', async () => {
var nextState = storageReducer(
{
key: 'value'
},
{
type: StorageTypes.REMOVE_GLOBAL_ITEM,
data: {
name: 'key'
}
}
);
assert.deepEqual(
nextState,
{}
);
nextState = storageReducer(
{},
{
type: StorageTypes.REMOVE_GLOBAL_ITEM,
data: {
name: 'key'
}
}
);
assert.deepEqual(
nextState,
{}
);
});
it('Storage.CLEAR', async () => {
const nextState = storageReducer(
{
key: 'value',
excluded: 'not-cleared'
},
{
type: StorageTypes.CLEAR,
data: {
exclude: ['excluded']
}
}
);
assert.deepEqual(
nextState,
{
excluded: 'not-cleared'
}
);
});
it('Storage.ACTION_ON_ITEMS_WITH_PREFIX', async () => {
var touchedPairs = [];
storageReducer(
{
user_id_prefix_key1: 1,
user_id_prefix_key2: 2,
user_id_not_prefix_key: 3
},
{
type: StorageTypes.ACTION_ON_ITEMS_WITH_PREFIX,
data: {
globalPrefix: 'user_id_',
prefix: 'prefix',
action: (key, value) => touchedPairs.push([key, value])
}
}
);
assert.deepEqual(