Cache localStorage objects for SettingsStore (#8366)
This commit is contained in:
parent
65c74bd158
commit
859fdf7d51
3 changed files with 109 additions and 35 deletions
87
src/settings/handlers/AbstractLocalStorageSettingsHandler.ts
Normal file
87
src/settings/handlers/AbstractLocalStorageSettingsHandler.ts
Normal file
|
@ -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 2017 Travis Ralston
|
||||||
Copyright 2019 New Vector Ltd.
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SettingsHandler from "./SettingsHandler";
|
|
||||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import { SettingLevel } from "../SettingLevel";
|
import { SettingLevel } from "../SettingLevel";
|
||||||
import { CallbackFn, WatchManager } from "../WatchManager";
|
import { CallbackFn, WatchManager } from "../WatchManager";
|
||||||
|
import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets and sets settings at the "device" level for the current device.
|
* Gets and sets settings at the "device" level for the current device.
|
||||||
* This handler does not make use of the roomId parameter. This handler
|
* This handler does not make use of the roomId parameter. This handler
|
||||||
* will special-case features to support legacy settings.
|
* 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
|
* Creates a new device settings handler
|
||||||
* @param {string[]} featureNames The names of known features.
|
* @param {string[]} featureNames The names of known features.
|
||||||
|
@ -43,15 +43,15 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||||
|
|
||||||
// Special case notifications
|
// Special case notifications
|
||||||
if (settingName === "notificationsEnabled") {
|
if (settingName === "notificationsEnabled") {
|
||||||
const value = localStorage.getItem("notifications_enabled");
|
const value = this.getItem("notifications_enabled");
|
||||||
if (typeof(value) === "string") return value === "true";
|
if (typeof(value) === "string") return value === "true";
|
||||||
return null; // wrong type or otherwise not set
|
return null; // wrong type or otherwise not set
|
||||||
} else if (settingName === "notificationBodyEnabled") {
|
} 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";
|
if (typeof(value) === "string") return value === "true";
|
||||||
return null; // wrong type or otherwise not set
|
return null; // wrong type or otherwise not set
|
||||||
} else if (settingName === "audioNotificationsEnabled") {
|
} 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";
|
if (typeof(value) === "string") return value === "true";
|
||||||
return null; // wrong type or otherwise not set
|
return null; // wrong type or otherwise not set
|
||||||
}
|
}
|
||||||
|
@ -68,15 +68,15 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||||
|
|
||||||
// Special case notifications
|
// Special case notifications
|
||||||
if (settingName === "notificationsEnabled") {
|
if (settingName === "notificationsEnabled") {
|
||||||
localStorage.setItem("notifications_enabled", newValue);
|
this.setItem("notifications_enabled", newValue);
|
||||||
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else if (settingName === "notificationBodyEnabled") {
|
} else if (settingName === "notificationBodyEnabled") {
|
||||||
localStorage.setItem("notifications_body_enabled", newValue);
|
this.setItem("notifications_body_enabled", newValue);
|
||||||
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
} else if (settingName === "audioNotificationsEnabled") {
|
} else if (settingName === "audioNotificationsEnabled") {
|
||||||
localStorage.setItem("audio_notifications_enabled", newValue);
|
this.setItem("audio_notifications_enabled", newValue);
|
||||||
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||||
|
|
||||||
delete settings["useIRCLayout"];
|
delete settings["useIRCLayout"];
|
||||||
settings["layout"] = newValue;
|
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);
|
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
|
@ -95,7 +95,7 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||||
|
|
||||||
const settings = this.getSettings() || {};
|
const settings = this.getSettings() || {};
|
||||||
settings[settingName] = newValue;
|
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);
|
this.watchers.notifyUpdate(settingName, null, SettingLevel.DEVICE, newValue);
|
||||||
|
|
||||||
return Promise.resolve();
|
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
|
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) {
|
public watchSetting(settingName: string, roomId: string, cb: CallbackFn) {
|
||||||
this.watchers.watchSetting(settingName, roomId, cb);
|
this.watchers.watchSetting(settingName, roomId, cb);
|
||||||
}
|
}
|
||||||
|
@ -118,9 +114,7 @@ export default class DeviceSettingsHandler extends SettingsHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSettings(): any { // TODO: [TS] Type return
|
private getSettings(): any { // TODO: [TS] Type return
|
||||||
const value = localStorage.getItem("mx_local_settings");
|
return this.getObject("mx_local_settings");
|
||||||
if (!value) return null;
|
|
||||||
return JSON.parse(value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: features intentionally don't use the same key as settings to avoid conflicts
|
// 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;
|
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 === "true") return true;
|
||||||
if (value === "false") return false;
|
if (value === "false") return false;
|
||||||
// Try to read the next config level for the feature.
|
// 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) {
|
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);
|
this.watchers.notifyUpdate(featureName, null, SettingLevel.DEVICE, enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Travis Ralston
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SettingsHandler from "./SettingsHandler";
|
|
||||||
import { SettingLevel } from "../SettingLevel";
|
import { SettingLevel } from "../SettingLevel";
|
||||||
import { WatchManager } from "../WatchManager";
|
import { WatchManager } from "../WatchManager";
|
||||||
|
import AbstractLocalStorageSettingsHandler from "./AbstractLocalStorageSettingsHandler";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets and sets settings at the "room-device" level for the current device in a particular
|
* Gets and sets settings at the "room-device" level for the current device in a particular
|
||||||
* room.
|
* room.
|
||||||
*/
|
*/
|
||||||
export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
export default class RoomDeviceSettingsHandler extends AbstractLocalStorageSettingsHandler {
|
||||||
constructor(public readonly watchers: WatchManager) {
|
constructor(public readonly watchers: WatchManager) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
||||||
// Special case blacklist setting to use legacy values
|
// Special case blacklist setting to use legacy values
|
||||||
if (settingName === "blacklistUnverifiedDevices") {
|
if (settingName === "blacklistUnverifiedDevices") {
|
||||||
const value = this.read("mx_local_settings");
|
const value = this.read("mx_local_settings");
|
||||||
if (value && value['blacklistUnverifiedDevicesPerRoom']) {
|
if (value?.['blacklistUnverifiedDevicesPerRoom']) {
|
||||||
return value['blacklistUnverifiedDevicesPerRoom'][roomId];
|
return value['blacklistUnverifiedDevicesPerRoom'][roomId];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,16 +49,15 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
|
||||||
if (!value) value = {};
|
if (!value) value = {};
|
||||||
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
|
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
|
||||||
value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue;
|
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);
|
this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, newValue);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newValue === null) {
|
if (newValue === null) {
|
||||||
localStorage.removeItem(this.getKey(settingName, roomId));
|
this.removeItem(this.getKey(settingName, roomId));
|
||||||
} else {
|
} else {
|
||||||
newValue = JSON.stringify({ value: newValue });
|
this.setObject(this.getKey(settingName, roomId), { value: newValue });
|
||||||
localStorage.setItem(this.getKey(settingName, roomId), newValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.watchers.notifyUpdate(settingName, roomId, SettingLevel.ROOM_DEVICE, 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
|
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 {
|
private read(key: string): any {
|
||||||
const rawValue = localStorage.getItem(key);
|
return this.getItem(key);
|
||||||
if (!rawValue) return null;
|
|
||||||
return JSON.parse(rawValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getKey(settingName: string, roomId: string): string {
|
private getKey(settingName: string, roomId: string): string {
|
||||||
|
|
Loading…
Reference in a new issue