diff options
Diffstat (limited to 'packages/web-util/src/hooks/useLocalStorage.ts')
-rw-r--r-- | packages/web-util/src/hooks/useLocalStorage.ts | 139 |
1 files changed, 139 insertions, 0 deletions
diff --git a/packages/web-util/src/hooks/useLocalStorage.ts b/packages/web-util/src/hooks/useLocalStorage.ts new file mode 100644 index 000000000..abd80bacc --- /dev/null +++ b/packages/web-util/src/hooks/useLocalStorage.ts @@ -0,0 +1,139 @@ +/* + This file is part of GNU Anastasis + (C) 2021-2022 Anastasis SARL + + GNU Anastasis is free software; you can redistribute it and/or modify it under the + terms of the GNU Affero General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Anastasis is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License along with + GNU Anastasis; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AbsoluteTime, Codec, codecForString } from "@gnu-taler/taler-util"; +import { useEffect, useState } from "preact/hooks"; +import { + ObservableMap, + browserStorageMap, + localStorageMap, + memoryMap, +} from "../utils/observable.js"; + +declare const opaque_StorageKey: unique symbol; + +export type StorageKey<Key> = { + id: string; + [opaque_StorageKey]: true; + codec: Codec<Key>; +}; + +export function buildStorageKey<Key>( + name: string, + codec: Codec<Key>, +): StorageKey<Key>; +export function buildStorageKey(name: string): StorageKey<string>; +export function buildStorageKey<Key = string>( + name: string, + codec?: Codec<Key>, +): StorageKey<Key> { + return { + id: name, + codec: codec ?? (codecForString() as Codec<Key>), + } as StorageKey<Key>; +} + +export interface StorageState<Type = string> { + value?: Type; + update: (s: Type) => void; + reset: () => void; +} + +const supportLocalStorage = typeof window !== "undefined"; +const supportBrowserStorage = + typeof chrome !== "undefined" && typeof chrome.storage !== "undefined"; + +/** + * Build setting storage + */ +const storage: ObservableMap<string, string> = (function buildStorage() { + if (supportBrowserStorage) { + //browser storage is like local storage but + //with app sync. + //Works for almost every browser + if (supportLocalStorage) { + return browserStorageMap(localStorageMap()); + } else { + // service worker doesn't have local storage + return browserStorageMap(memoryMap<string>()); + } + } else if (supportLocalStorage) { + // fallback if browser is too old + return localStorageMap(); + } else { + // new need to save settings somewhere + return memoryMap<string>(); + } +})(); +//with initial value +export function useLocalStorage<Type = string>( + key: StorageKey<Type>, + defaultValue: Type, +): Required<StorageState<Type>>; +//without initial value +export function useLocalStorage<Type = string>( + key: StorageKey<Type>, +): StorageState<Type>; +// impl +export function useLocalStorage<Type = string>( + key: StorageKey<Type>, + defaultValue?: Type, +): StorageState<Type> { + const current = convert(storage.get(key.id), key, defaultValue); + + const [_, setStoredValue] = useState(AbsoluteTime.now().t_ms); + + useEffect(() => { + return storage.onUpdate(key.id, () => { + // const newValue = storage.get(key.id); + setStoredValue(AbsoluteTime.now().t_ms); + }); + }, [key.id]); + + const setValue = (value?: Type): void => { + if (value === undefined) { + storage.delete(key.id); + } else { + storage.set( + key.id, + key.codec ? JSON.stringify(value) : (value as string), + ); + } + }; + + return { + value: current, + update: setValue, + reset: () => { + setValue(defaultValue); + }, + }; +} + +function convert<Type>(updated: string | undefined, key: StorageKey<Type>, defaultValue?: Type): Type | undefined { + if (updated === undefined) return defaultValue; //optional + try { + return key.codec.decode(JSON.parse(updated)); + } catch (e) { + //decode error + return defaultValue; + } +} |