From 94378326169e81e994b36f9b2b47249289c82fa9 Mon Sep 17 00:00:00 2001 From: blam Date: Fri, 29 May 2020 00:11:29 +0200 Subject: [PATCH] feat(core-api/UserSettings): Ported across the UserSettings API from internal and convert to ts --- .../src/apis/definitions/UserSettingsApi.ts | 78 +++++++++++++++ .../core-api/src/apis/definitions/index.ts | 1 + .../implementations/UserSettingsApi/index.ts | 97 +++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 packages/core-api/src/apis/definitions/UserSettingsApi.ts create mode 100644 packages/core-api/src/apis/implementations/UserSettingsApi/index.ts diff --git a/packages/core-api/src/apis/definitions/UserSettingsApi.ts b/packages/core-api/src/apis/definitions/UserSettingsApi.ts new file mode 100644 index 0000000000..62045e0e51 --- /dev/null +++ b/packages/core-api/src/apis/definitions/UserSettingsApi.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '../ApiRef'; + +type setValue = (value: T | null) => void; +type removeValue = () => void; +export type UserSettingsApi = { + /** + * Get persistent data. + * + * TODO: Replace with something less volatile than LocalStorage + * + * @param {String} storeName Name of the store. + * @param {String?} key (Optional) Unique key associated with the data. + * If not key is specified,the whole store is returned. + * @param {String} key (Optional) Empty value to return if there is not data. + * @return {Object} data The data that should is stored. + */ + getFromStore( + storeName: string, + key: string, + defaultValue: T | null, + ): T | null; + /** + * Remove persistent data. + * + * TODO: Replace with something less volatile than LocalStorage + * + * @param {String} storeName Name of the store. + * @param {String} key Unique key associated with the data. + */ + removeFromStore(storeName: string, key: string): void; + + /** + * Save persistent data. + * + * TODO: Replace with something less volatile than LocalStorage + * + * @param {String} storeName Name of the store. + * @param {String} key Unique key associated with the data. + * @param {Object} data The data that should be stored. + */ + saveToStore(storeName: string, key: string, data: any): void; + + /** + * React hook that observes a single store value and provides functions for updating the value. + * + * @param {String} storeName Name of the store. + * @param {String} key Unique key associated with the data. + * @return {[value, setValue(value), removeValue()]} An array to be deconstructed, + * The first element is the value, the second is a function to call to update the value, + * and the third is a function to call to clear the value. + */ + useStoreValue( + storeName: string, + key: string, + ): [T | null, setValue, removeValue]; +}; + +export const userSettingsApiRef = createApiRef({ + id: 'core.user.settings', + description: + 'Provides the ability to modify settings that are personalised to the user', +}); diff --git a/packages/core-api/src/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts index 2e008965cd..0acef2abb4 100644 --- a/packages/core-api/src/apis/definitions/index.ts +++ b/packages/core-api/src/apis/definitions/index.ts @@ -27,3 +27,4 @@ export * from './AppThemeApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; export * from './OAuthRequestApi'; +export * from './UserSettingsApi'; diff --git a/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts b/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts new file mode 100644 index 0000000000..8d73202414 --- /dev/null +++ b/packages/core-api/src/apis/implementations/UserSettingsApi/index.ts @@ -0,0 +1,97 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { useState, useEffect } from 'react'; +import { UserSettingsApi } from '../../definitions'; + +export class UserSettings implements UserSettingsApi { + getFromStore( + storeName: string, + key: string = '', + defaultValue: T | null, + ): T | null { + let store; + try { + store = JSON.parse(localStorage.getItem(storeName)!) || {}; + } catch (e) { + window.console.error( + `Error when parsing JSON config from storage for: ${storeName}`, + ); + return defaultValue as T; + } + + if (key) { + return (store[key] ?? defaultValue) as T; + } + return store as T; + } + + saveToStore(storeName: string, key: string, data: any): void { + const store = JSON.parse(localStorage.getItem(storeName)!) || {}; + store[key] = data; + localStorage.setItem(storeName, JSON.stringify(store)); + } + + removeFromStore(storeName: string, key: string): void { + const store = JSON.parse(localStorage.getItem(storeName)!) || {}; + delete store[key]; + localStorage.setItem(storeName, JSON.stringify(store)); + } + + useStoreValue( + storeName: string, + key: string, + ): [T | null, (value: T | null) => void, () => void] { + // We hardcode the emptyValue to null, since allowing objects or arrays would make + // the useEffect params a lot more complex and it would easy to get stuck in a loop. + const [value, setValue] = useState(() => + this.getFromStore(storeName, key, null), + ); + + // Listen to storage change events from other tabs + useEffect(() => { + const onChange = (event: StorageEvent) => { + if (event.key === storeName) { + // Avoid sending unnecessary updates when only the reference changes because of JSON.parse + setValue((currentValue: T | null) => { + const newValue = this.getFromStore(storeName, key, null); + + // Fastest way to do a deep equals, since we know references will differ, + // the values are serializable, and it's ok to signal a change if key order changes. + if (JSON.stringify(currentValue) === JSON.stringify(newValue)) { + return currentValue; + } + return newValue; + }); + } + }; + + window.addEventListener('storage', onChange); + return () => window.removeEventListener('storage', onChange); + }, [storeName, key]); + + const saveValue = (newValue: T | null) => { + this.saveToStore(storeName, key, newValue); + setValue(this.getFromStore(storeName, key, null)); + }; + + const removeValue = () => { + this.removeFromStore(storeName, key); + setValue(null); + }; + + return [value, saveValue, removeValue]; + } +}