Cache localStorage objects for SettingsStore (#8366)
							parent
							
								
									65c74bd158
								
							
						
					
					
						commit
						859fdf7d51
					
				|  | @ -0,0 +1,87 @@ | |||
| /* | ||||
| Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| 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 SettingsHandler from "./SettingsHandler"; | ||||
| 
 | ||||
| /** | ||||
|  * Abstract settings handler wrapping around localStorage making getValue calls cheaper | ||||
|  * by caching the values and listening for localStorage updates from other tabs. | ||||
|  */ | ||||
| export default abstract class AbstractLocalStorageSettingsHandler extends SettingsHandler { | ||||
|     private itemCache = new Map<string, any>(); | ||||
|     private objectCache = new Map<string, object>(); | ||||
| 
 | ||||
|     protected constructor() { | ||||
|         super(); | ||||
| 
 | ||||
|         // Listen for storage changes from other tabs to bust the cache
 | ||||
|         window.addEventListener("storage", (e: StorageEvent) => { | ||||
|             if (e.key === null) { | ||||
|                 this.itemCache.clear(); | ||||
|                 this.objectCache.clear(); | ||||
|             } else { | ||||
|                 this.itemCache.delete(e.key); | ||||
|                 this.objectCache.delete(e.key); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     protected getItem(key: string): any { | ||||
|         if (!this.itemCache.has(key)) { | ||||
|             const value = localStorage.getItem(key); | ||||
|             this.itemCache.set(key, value); | ||||
|             return value; | ||||
|         } | ||||
| 
 | ||||
|         return this.itemCache.get(key); | ||||
|     } | ||||
| 
 | ||||
|     protected getObject<T extends object>(key: string): T | null { | ||||
|         if (!this.objectCache.has(key)) { | ||||
|             try { | ||||
|                 const value = JSON.parse(localStorage.getItem(key)); | ||||
|                 this.objectCache.set(key, value); | ||||
|                 return value; | ||||
|             } catch (err) { | ||||
|                 console.error("Failed to parse localStorage object", err); | ||||
|                 return null; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return this.objectCache.get(key) as T; | ||||
|     } | ||||
| 
 | ||||
|     protected setItem(key: string, value: any): void { | ||||
|         this.itemCache.set(key, value); | ||||
|         localStorage.setItem(key, value); | ||||
|     } | ||||
| 
 | ||||
|     protected setObject(key: string, value: object): void { | ||||
|         this.objectCache.set(key, value); | ||||
|         localStorage.setItem(key, JSON.stringify(value)); | ||||
|     } | ||||
| 
 | ||||
|     // handles both items and objects
 | ||||
|     protected removeItem(key: string): void { | ||||
|         localStorage.removeItem(key); | ||||
|         this.itemCache.delete(key); | ||||
|         this.objectCache.delete(key); | ||||
|     } | ||||
| 
 | ||||
|     public isSupported(): boolean { | ||||
|         return localStorage !== undefined && localStorage !== null; | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +1,7 @@ | |||
| /* | ||||
| Copyright 2017 Travis Ralston | ||||
| Copyright 2019 New Vector Ltd. | ||||
| Copyright 2019 The Matrix.org Foundation C.I.C. | ||||
| Copyright 2019 - 2022 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -16,17 +16,17 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import SettingsHandler from "./SettingsHandler"; | ||||
| import { MatrixClientPeg } from "../../MatrixClientPeg"; | ||||
| import { SettingLevel } from "../SettingLevel"; | ||||
| import { CallbackFn, WatchManager } from "../WatchManager"; | ||||
| import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler"; | ||||
| 
 | ||||
| /** | ||||
|  * Gets and sets settings at the "device" level for the current device. | ||||
|  * This handler does not make use of the roomId parameter. This handler | ||||
|  * will special-case features to support legacy settings. | ||||
|  */ | ||||
| export default class DeviceSettingsHandler extends SettingsHandler { | ||||
| export default class DeviceSettingsHandler extends AbstractLocalStorageSettingsHandler { | ||||
|     /** | ||||
|      * Creates a new device settings handler | ||||
|      * @param {string[]} featureNames The names of known features. | ||||
|  | @ -43,15 +43,15 @@ export default class DeviceSettingsHandler extends SettingsHandler { | |||
| 
 | ||||
|         // Special case notifications
 | ||||
|         if (settingName === "notificationsEnabled") { | ||||
|             const value = localStorage.getItem("notifications_enabled"); | ||||
|             const value = this.getItem("notifications_enabled"); | ||||
|             if (typeof(value) === "string") return value === "true"; | ||||
|             return null; // wrong type or otherwise not set
 | ||||
|         } else if (settingName === "notificationBodyEnabled") { | ||||
|             const value = localStorage.getItem("notifications_body_enabled"); | ||||
|             const value = this.getItem("notifications_body_enabled"); | ||||
|             if (typeof(value) === "string") return value === "true"; | ||||
|             return null; // wrong type or otherwise not set
 | ||||
|         } else if (settingName === "audioNotificationsEnabled") { | ||||
|             const value = localStorage.getItem("audio_notifications_enabled"); | ||||
|             const value = this.getItem("audio_notifications_enabled"); | ||||
|             if (typeof(value) === "string") return value === "true"; | ||||
|             return null; // wrong type or otherwise not set
 | ||||
|         } | ||||
|  | @ -68,15 +68,15 @@ export default class DeviceSettingsHandler extends SettingsHandler { | |||
| 
 | ||||
|         // Special case notifications
 | ||||
|         if (settingName === "notificationsEnabled") { | ||||
|             localStorage.setItem("notifications_enabled", newValue); | ||||
|             this.setItem("notifications_enabled", newValue); | ||||
|             this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); | ||||
|             return Promise.resolve(); | ||||
|         } else if (settingName === "notificationBodyEnabled") { | ||||
|             localStorage.setItem("notifications_body_enabled", newValue); | ||||
|             this.setItem("notifications_body_enabled", newValue); | ||||
|             this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); | ||||
|             return Promise.resolve(); | ||||
|         } else if (settingName === "audioNotificationsEnabled") { | ||||
|             localStorage.setItem("audio_notifications_enabled", newValue); | ||||
|             this.setItem("audio_notifications_enabled", newValue); | ||||
|             this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
|  | @ -87,7 +87,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { | |||
| 
 | ||||
|             delete settings["useIRCLayout"]; | ||||
|             settings["layout"] = newValue; | ||||
|             localStorage.setItem("mx_local_settings", JSON.stringify(settings)); | ||||
|             this.setObject("mx_local_settings", settings); | ||||
| 
 | ||||
|             this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); | ||||
|             return Promise.resolve(); | ||||
|  | @ -95,7 +95,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { | |||
| 
 | ||||
|         const settings = this.getSettings() || {}; | ||||
|         settings[settingName] = newValue; | ||||
|         localStorage.setItem("mx_local_settings", JSON.stringify(settings)); | ||||
|         this.setObject("mx_local_settings", settings); | ||||
|         this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue); | ||||
| 
 | ||||
|         return Promise.resolve(); | ||||
|  | @ -105,10 +105,6 @@ export default class DeviceSettingsHandler extends SettingsHandler { | |||
|         return true; // It's their device, so they should be able to
 | ||||
|     } | ||||
| 
 | ||||
|     public isSupported(): boolean { | ||||
|         return localStorage !== undefined && localStorage !== null; | ||||
|     } | ||||
| 
 | ||||
|     public watchSetting(settingName: string, roomId: string, cb: CallbackFn) { | ||||
|         this.watchers.watchSetting(settingName, roomId, cb); | ||||
|     } | ||||
|  | @ -118,9 +114,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { | |||
|     } | ||||
| 
 | ||||
|     private getSettings(): any { // TODO: [TS] Type return
 | ||||
|         const value = localStorage.getItem("mx_local_settings"); | ||||
|         if (!value) return null; | ||||
|         return JSON.parse(value); | ||||
|         return this.getObject("mx_local_settings"); | ||||
|     } | ||||
| 
 | ||||
|     // Note: features intentionally don't use the same key as settings to avoid conflicts
 | ||||
|  | @ -132,7 +126,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { | |||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         const value = localStorage.getItem("mx_labs_feature_" + featureName); | ||||
|         const value = this.getItem("mx_labs_feature_" + featureName); | ||||
|         if (value === "true") return true; | ||||
|         if (value === "false") return false; | ||||
|         // Try to read the next config level for the feature.
 | ||||
|  | @ -140,7 +134,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { | |||
|     } | ||||
| 
 | ||||
|     private writeFeature(featureName: string, enabled: boolean | null) { | ||||
|         localStorage.setItem("mx_labs_feature_" + featureName, `${enabled}`); | ||||
|         this.setItem("mx_labs_feature_" + featureName, `${enabled}`); | ||||
|         this.watchers.notifyUpdate(featureName, null, SettingLevel.DEVICE, enabled); | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| /* | ||||
| Copyright 2017 Travis Ralston | ||||
| Copyright 2019, 2020 The Matrix.org Foundation C.I.C. | ||||
| Copyright 2019, 2020 - 2022 The Matrix.org Foundation C.I.C. | ||||
| 
 | ||||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||||
| you may not use this file except in compliance with the License. | ||||
|  | @ -15,15 +15,15 @@ See the License for the specific language governing permissions and | |||
| limitations under the License. | ||||
| */ | ||||
| 
 | ||||
| import SettingsHandler from "./SettingsHandler"; | ||||
| import { SettingLevel } from "../SettingLevel"; | ||||
| import { WatchManager } from "../WatchManager"; | ||||
| import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler"; | ||||
| 
 | ||||
| /** | ||||
|  * Gets and sets settings at the "room-device" level for the current device in a particular | ||||
|  * room. | ||||
|  */ | ||||
| export default class RoomDeviceSettingsHandler extends SettingsHandler { | ||||
| export default class RoomDeviceSettingsHandler extends AbstractLocalStorageSettingsHandler { | ||||
|     constructor(public readonly watchers: WatchManager) { | ||||
|         super(); | ||||
|     } | ||||
|  | @ -32,7 +32,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { | |||
|         // Special case blacklist setting to use legacy values
 | ||||
|         if (settingName === "blacklistUnverifiedDevices") { | ||||
|             const value = this.read("mx_local_settings"); | ||||
|             if (value && value['blacklistUnverifiedDevicesPerRoom']) { | ||||
|             if (value?.['blacklistUnverifiedDevicesPerRoom']) { | ||||
|                 return value['blacklistUnverifiedDevicesPerRoom'][roomId]; | ||||
|             } | ||||
|         } | ||||
|  | @ -49,16 +49,15 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { | |||
|             if (!value) value = {}; | ||||
|             if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {}; | ||||
|             value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue; | ||||
|             localStorage.setItem("mx_local_settings", JSON.stringify(value)); | ||||
|             this.setObject("mx_local_settings", value); | ||||
|             this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue); | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         if (newValue === null) { | ||||
|             localStorage.removeItem(this.getKey(settingName, roomId)); | ||||
|             this.removeItem(this.getKey(settingName, roomId)); | ||||
|         } else { | ||||
|             newValue = JSON.stringify({ value: newValue }); | ||||
|             localStorage.setItem(this.getKey(settingName, roomId), newValue); | ||||
|             this.setObject(this.getKey(settingName, roomId), { value: newValue }); | ||||
|         } | ||||
| 
 | ||||
|         this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue); | ||||
|  | @ -69,14 +68,8 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { | |||
|         return true; // It's their device, so they should be able to
 | ||||
|     } | ||||
| 
 | ||||
|     public isSupported(): boolean { | ||||
|         return localStorage !== undefined && localStorage !== null; | ||||
|     } | ||||
| 
 | ||||
|     private read(key: string): any { | ||||
|         const rawValue = localStorage.getItem(key); | ||||
|         if (!rawValue) return null; | ||||
|         return JSON.parse(rawValue); | ||||
|         return this.getItem(key); | ||||
|     } | ||||
| 
 | ||||
|     private getKey(settingName: string, roomId: string): string { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Michael Telatynski
						Michael Telatynski