mirror of https://github.com/vector-im/riot-web
				
				
				
			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