diff --git a/actions/global_actions.jsx b/actions/global_actions.jsx index bc25bd2d5a01..2b4fda3988c3 100644 --- a/actions/global_actions.jsx +++ b/actions/global_actions.jsx @@ -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(); diff --git a/actions/storage.js b/actions/storage.js new file mode 100644 index 000000000000..73083016898d --- /dev/null +++ b/actions/storage.js @@ -0,0 +1,81 @@ +// 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}; + }; +} diff --git a/package.json b/package.json index 932d4dadbf4d..8b9985d23bfa 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "jquery": "3.2.1", "key-mirror": "1.0.1", "localforage": "1.5.0", + "localforage-observable": "1.4.0", "marked": "mattermost/marked#17945726698a03420588acd55e5b38f692c03f31", "match-at": "0.1.1", "mattermost-redux": "1.0.1", @@ -94,6 +95,7 @@ "raw-loader": "0.5.1", "react-addons-test-utils": "15.6.2", "react-test-renderer": "15", + "redux-persist-node-storage": "1.0.2", "remote-redux-devtools": "0.5.12", "remote-redux-devtools-on-debugger": "0.8.2", "sass-loader": "6.0.6", diff --git a/reducers/index.js b/reducers/index.js index bbb01b4618c3..92340532f655 100644 --- a/reducers/index.js +++ b/reducers/index.js @@ -3,8 +3,10 @@ import plugins from './plugins'; import views from './views'; +import storage from './storage'; export default { views, - plugins + plugins, + storage }; diff --git a/reducers/storage.js b/reducers/storage.js new file mode 100644 index 000000000000..33bed552423f --- /dev/null +++ b/reducers/storage.js @@ -0,0 +1,58 @@ +// 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; + } +} diff --git a/selectors/storage.js b/selectors/storage.js new file mode 100644 index 000000000000..e672184c139e --- /dev/null +++ b/selectors/storage.js @@ -0,0 +1,23 @@ +// 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); + }; +} diff --git a/store/index.js b/store/index.js index 0989147e228f..0ebc080ae78b 100644 --- a/store/index.js +++ b/store/index.js @@ -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 diff --git a/stores/browser_store.jsx b/stores/browser_store.jsx index abdf1eb7c785..fcd02ed049bc 100644 --- a/stores/browser_store.jsx +++ b/stores/browser_store.jsx @@ -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); } } diff --git a/stores/post_store.jsx b/stores/post_store.jsx index 2ebe04cbaba2..cbeb3efaad3b 100644 --- a/stores/post_store.jsx +++ b/stores/post_store.jsx @@ -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; }); } diff --git a/tests/redux/actions/storage.test.js b/tests/redux/actions/storage.test.js new file mode 100644 index 000000000000..a8055d2fb76e --- /dev/null +++ b/tests/redux/actions/storage.test.js @@ -0,0 +1,106 @@ +// 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'} + ); + }); +}); diff --git a/tests/redux/reducers/storage.test.js b/tests/redux/reducers/storage.test.js new file mode 100644 index 000000000000..e1b03cda8bed --- /dev/null +++ b/tests/redux/reducers/storage.test.js @@ -0,0 +1,178 @@ +// 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( + touchedPairs, + [['prefix_key1', 1], ['prefix_key2', 2]] + ); + }); + + it('Storage.ACTION_ON_GLOBAL_ITEMS_WITH_PREFIX', async () => { + var touchedPairs = []; + storageReducer( + { + prefix_key1: 1, + prefix_key2: 2, + not_prefix_key: 3 + }, + { + type: StorageTypes.ACTION_ON_GLOBAL_ITEMS_WITH_PREFIX, + data: { + prefix: 'prefix', + action: (key, value) => touchedPairs.push([key, value]) + } + } + ); + assert.deepEqual( + touchedPairs, + [['prefix_key1', 1], ['prefix_key2', 2]] + ); + }); +}); diff --git a/tests/redux/selectors/storage.test.js b/tests/redux/selectors/storage.test.js new file mode 100644 index 000000000000..9e802eb9ea83 --- /dev/null +++ b/tests/redux/selectors/storage.test.js @@ -0,0 +1,46 @@ +// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import assert from 'assert'; + +import {getPrefix} from 'utils/storage_utils'; +import * as Selectors from 'selectors/storage'; + +describe('Selectors.Storage', () => { + const testState = { + entities: { + users: { + currentUserId: 'user_id', + profiles: { + user_id: { + id: 'user_id' + } + } + } + }, + storage: { + 'global-item': 'global-item-value', + user_id_item: 'item-value' + } + }; + + it('getPrefix', () => { + assert.equal(getPrefix({}), 'unknown_'); + assert.equal(getPrefix({entities: {}}), 'unknown_'); + assert.equal(getPrefix({entities: {users: {currentUserId: 'not-exists'}}}), 'unknown_'); + assert.equal(getPrefix({entities: {users: {currentUserId: 'not-exists', profiles: {}}}}), 'unknown_'); + assert.equal(getPrefix({entities: {users: {currentUserId: 'exists', profiles: {exists: {id: 'user_id'}}}}}), 'user_id_'); + }); + + it('makeGetGlobalItem', () => { + assert.equal(Selectors.makeGetGlobalItem('not-existing-global-item')(testState), null); + assert.equal(Selectors.makeGetGlobalItem('not-existing-global-item', 'default')(testState), 'default'); + assert.equal(Selectors.makeGetGlobalItem('global-item')(testState), 'global-item-value'); + }); + + it('makeGetItem', () => { + assert.equal(Selectors.makeGetItem('not-existing-item')(testState), null); + assert.equal(Selectors.makeGetItem('not-existing-item', 'default')(testState), 'default'); + assert.equal(Selectors.makeGetItem('item')(testState), 'item-value'); + }); +}); diff --git a/utils/constants.jsx b/utils/constants.jsx index 1062f1b484be..d6027672f5b4 100644 --- a/utils/constants.jsx +++ b/utils/constants.jsx @@ -314,6 +314,16 @@ export const StatTypes = keyMirror({ MONTHLY_ACTIVE_USERS: null }); +export const StorageTypes = keyMirror({ + SET_ITEM: null, + REMOVE_ITEM: null, + SET_GLOBAL_ITEM: null, + REMOVE_GLOBAL_ITEM: null, + CLEAR: null, + ACTION_ON_GLOBAL_ITEMS_WITH_PREFIX: null, + ACTION_ON_ITEMS_WITH_PREFIX: null +}); + export const ErrorPageTypes = { LOCAL_STORAGE: 'local_storage', OAUTH_MISSING_CODE: 'oauth_missing_code', diff --git a/utils/storage_utils.js b/utils/storage_utils.js new file mode 100644 index 000000000000..5e8fb4a01846 --- /dev/null +++ b/utils/storage_utils.js @@ -0,0 +1,16 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +export function getPrefix(state) { + if (state && state.entities && state.entities.users && state.entities.users.profiles) { + const user = state.entities.users.profiles[state.entities.users.currentUserId]; + if (user) { + return user.id + '_'; + } + } + + console.warn('Storage tried to operate without user present'); //eslint-disable-line no-console + + return 'unknown_'; +} + diff --git a/yarn.lock b/yarn.lock index b4e431ba14df..304e11afc68b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5110,12 +5110,25 @@ loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" +localforage-observable@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/localforage-observable/-/localforage-observable-1.4.0.tgz#cbe14e7a36b59b2239c1c64990a75c89b2bf41d7" + dependencies: + localforage "^1.5.0" + zen-observable "^0.2.1" + localforage@1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.5.0.tgz#6b994e19b56611fa85df3992df397ac4ab66e815" dependencies: lie "3.0.2" +localforage@^1.5.0: + version "1.5.3" + resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.5.3.tgz#698aa16af1022340b240be9d93192e8af022ff16" + dependencies: + lie "3.0.2" + locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -5864,6 +5877,12 @@ node-libs-browser@^2.0.0: util "^0.10.3" vm-browserify "0.0.4" +node-localstorage@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/node-localstorage/-/node-localstorage-1.3.0.tgz#2e436aae8dcc9ace97b43c65c16c0d577be0a55c" + dependencies: + write-file-atomic "^1.1.4" + node-notifier@^5.0.2: version "5.1.2" resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff" @@ -7131,6 +7150,12 @@ redux-devtools-instrument@^1.3.3: dependencies: redux-persist "^4.5.0" +redux-persist-node-storage@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/redux-persist-node-storage/-/redux-persist-node-storage-1.0.2.tgz#8f6ed009c8af5267db15d4382afee863079ac3fb" + dependencies: + node-localstorage "^1.3.0" + redux-persist-transform-filter@0.0.15: version "0.0.15" resolved "https://registry.yarnpkg.com/redux-persist-transform-filter/-/redux-persist-transform-filter-0.0.15.tgz#07614b2d595d88ff4e2fbbafc15d0b2293396ae6" @@ -7765,6 +7790,10 @@ slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" +slide@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" + smart-buffer@^1.0.13: version "1.1.15" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-1.1.15.tgz#7f114b5b65fab3e2a35aa775bb12f0d1c649bf16" @@ -8865,6 +8894,14 @@ wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" +write-file-atomic@^1.1.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-1.3.4.tgz#f807a4f0b1d9e913ae7a48112e6cc3af1991b45f" + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + slide "^1.1.5" + write-file-atomic@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" @@ -9004,3 +9041,7 @@ yauzl@^2.2.1, yauzl@^2.5.0: dependencies: buffer-crc32 "~0.2.3" fd-slicer "~1.0.1" + +zen-observable@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.2.1.tgz#c47676a64132b8475a61aa49e514755b5b9663f3"