2017-10-29 01:13:06 +00:00
|
|
|
/*
|
|
|
|
Copyright 2017 Travis Ralston
|
2020-07-29 16:57:14 +00:00
|
|
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
2017-10-29 01:13:06 +00:00
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2017-11-05 04:47:18 +00:00
|
|
|
import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler";
|
|
|
|
import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler";
|
|
|
|
import DefaultSettingsHandler from "./handlers/DefaultSettingsHandler";
|
|
|
|
import RoomAccountSettingsHandler from "./handlers/RoomAccountSettingsHandler";
|
|
|
|
import AccountSettingsHandler from "./handlers/AccountSettingsHandler";
|
|
|
|
import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
|
|
|
|
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
|
2020-07-29 16:57:14 +00:00
|
|
|
import { _t } from '../languageHandler';
|
2020-05-14 02:41:41 +00:00
|
|
|
import dis from '../dispatcher/dispatcher';
|
2020-07-29 16:57:14 +00:00
|
|
|
import { ISetting, SETTINGS } from "./Settings";
|
2017-11-05 04:47:18 +00:00
|
|
|
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
|
2021-05-19 08:06:01 +00:00
|
|
|
import { WatchManager, CallbackFn as WatchCallbackFn } from "./WatchManager";
|
2020-07-29 16:57:14 +00:00
|
|
|
import { SettingLevel } from "./SettingLevel";
|
|
|
|
import SettingsHandler from "./handlers/SettingsHandler";
|
2017-10-29 01:13:06 +00:00
|
|
|
|
2019-02-26 19:43:10 +00:00
|
|
|
const defaultWatchManager = new WatchManager();
|
|
|
|
|
2017-11-04 05:19:45 +00:00
|
|
|
// Convert the settings to easier to manage objects for the handlers
|
2017-10-29 01:53:12 +00:00
|
|
|
const defaultSettings = {};
|
2019-01-25 03:57:40 +00:00
|
|
|
const invertedDefaultSettings = {};
|
2017-10-29 01:53:12 +00:00
|
|
|
const featureNames = [];
|
|
|
|
for (const key of Object.keys(SETTINGS)) {
|
2017-10-29 02:21:34 +00:00
|
|
|
defaultSettings[key] = SETTINGS[key].default;
|
2017-10-29 01:13:06 +00:00
|
|
|
if (SETTINGS[key].isFeature) featureNames.push(key);
|
2019-01-25 03:57:40 +00:00
|
|
|
if (SETTINGS[key].invertedSettingName) {
|
|
|
|
// Invert now so that the rest of the system will invert it back
|
|
|
|
// to what was intended.
|
2020-11-03 19:41:59 +00:00
|
|
|
invertedDefaultSettings[SETTINGS[key].invertedSettingName] = !SETTINGS[key].default;
|
2019-01-25 03:57:40 +00:00
|
|
|
}
|
2017-10-29 01:13:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const LEVEL_HANDLERS = {
|
2020-07-29 16:57:14 +00:00
|
|
|
[SettingLevel.DEVICE]: new DeviceSettingsHandler(featureNames, defaultWatchManager),
|
|
|
|
[SettingLevel.ROOM_DEVICE]: new RoomDeviceSettingsHandler(defaultWatchManager),
|
|
|
|
[SettingLevel.ROOM_ACCOUNT]: new RoomAccountSettingsHandler(defaultWatchManager),
|
|
|
|
[SettingLevel.ACCOUNT]: new AccountSettingsHandler(defaultWatchManager),
|
|
|
|
[SettingLevel.ROOM]: new RoomSettingsHandler(defaultWatchManager),
|
2020-08-17 19:24:55 +00:00
|
|
|
[SettingLevel.CONFIG]: new ConfigSettingsHandler(featureNames),
|
2020-07-29 16:57:14 +00:00
|
|
|
[SettingLevel.DEFAULT]: new DefaultSettingsHandler(defaultSettings, invertedDefaultSettings),
|
2017-10-29 01:13:06 +00:00
|
|
|
};
|
|
|
|
|
2017-11-05 03:32:00 +00:00
|
|
|
// Wrap all the handlers with local echo
|
|
|
|
for (const key of Object.keys(LEVEL_HANDLERS)) {
|
|
|
|
LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]);
|
|
|
|
}
|
|
|
|
|
2021-02-18 21:56:19 +00:00
|
|
|
export const LEVEL_ORDER = [
|
2020-07-29 16:57:14 +00:00
|
|
|
SettingLevel.DEVICE,
|
|
|
|
SettingLevel.ROOM_DEVICE,
|
|
|
|
SettingLevel.ROOM_ACCOUNT,
|
|
|
|
SettingLevel.ACCOUNT,
|
|
|
|
SettingLevel.ROOM,
|
|
|
|
SettingLevel.CONFIG,
|
|
|
|
SettingLevel.DEFAULT,
|
2017-10-30 02:44:00 +00:00
|
|
|
];
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
export type CallbackFn = (
|
|
|
|
settingName: string,
|
|
|
|
roomId: string,
|
|
|
|
atLevel: SettingLevel,
|
|
|
|
newValAtLevel: any,
|
|
|
|
newVal: any,
|
|
|
|
) => void;
|
|
|
|
|
2020-07-30 14:43:33 +00:00
|
|
|
interface IHandlerMap {
|
2020-07-29 16:57:14 +00:00
|
|
|
// @ts-ignore - TS wants this to be a string key but we know better
|
|
|
|
[level: SettingLevel]: SettingsHandler;
|
|
|
|
}
|
|
|
|
|
2020-07-30 14:44:34 +00:00
|
|
|
export type LabsFeatureState = "labs" | "disable" | "enable" | string;
|
|
|
|
|
2017-10-29 01:13:06 +00:00
|
|
|
/**
|
|
|
|
* Controls and manages application settings by providing varying levels at which the
|
|
|
|
* setting value may be specified. The levels are then used to determine what the setting
|
|
|
|
* value should be given a set of circumstances. The levels, in priority order, are:
|
2020-07-29 16:57:14 +00:00
|
|
|
* - SettingLevel.DEVICE - Values are determined by the current device
|
|
|
|
* - SettingLevel.ROOM_DEVICE - Values are determined by the current device for a particular room
|
|
|
|
* - SettingLevel.ROOM_ACCOUNT - Values are determined by the current account for a particular room
|
|
|
|
* - SettingLevel.ACCOUNT - Values are determined by the current account
|
|
|
|
* - SettingLevel.ROOM - Values are determined by a particular room (by the room admins)
|
|
|
|
* - SettingLevel.CONFIG - Values are determined by the config.json
|
|
|
|
* - SettingLevel.DEFAULT - Values are determined by the hardcoded defaults
|
2017-10-29 01:13:06 +00:00
|
|
|
*
|
|
|
|
* Each level has a different method to storing the setting value. For implementation
|
|
|
|
* specific details, please see the handlers. The "config" and "default" levels are
|
|
|
|
* both always supported on all platforms. All other settings should be guarded by
|
|
|
|
* isLevelSupported() prior to attempting to set the value.
|
|
|
|
*
|
|
|
|
* Settings can also represent features. Features are significant portions of the
|
|
|
|
* application that warrant a dedicated setting to toggle them on or off. Features are
|
|
|
|
* special-cased to ensure that their values respect the configuration (for example, a
|
|
|
|
* feature may be reported as disabled even though a user has specifically requested it
|
|
|
|
* be enabled).
|
|
|
|
*/
|
|
|
|
export default class SettingsStore {
|
2019-02-26 19:43:10 +00:00
|
|
|
// We support watching settings for changes, and do this by tracking which callbacks have
|
|
|
|
// been given to us. We end up returning the callbackRef to the caller so they can unsubscribe
|
|
|
|
// at a later point.
|
2019-02-22 23:33:20 +00:00
|
|
|
//
|
|
|
|
// We also maintain a list of monitors which are special watchers: they cause dispatches
|
|
|
|
// when the setting changes. We track which rooms we're monitoring though to ensure we
|
|
|
|
// don't duplicate updates on the bus.
|
2021-05-19 08:06:01 +00:00
|
|
|
private static watchers = new Map<string, WatchCallbackFn>();
|
2020-07-29 16:57:14 +00:00
|
|
|
private static monitors = {}; // { settingName => { roomId => callbackRef } }
|
2019-02-22 23:33:20 +00:00
|
|
|
|
2020-02-04 10:41:48 +00:00
|
|
|
// Counter used for generation of watcher IDs
|
2020-07-29 16:57:14 +00:00
|
|
|
private static watcherCount = 1;
|
2020-02-04 10:41:48 +00:00
|
|
|
|
2020-08-17 19:19:15 +00:00
|
|
|
/**
|
|
|
|
* Gets all the feature-style setting names.
|
|
|
|
* @returns {string[]} The names of the feature settings.
|
|
|
|
*/
|
|
|
|
public static getFeatureSettingNames(): string[] {
|
|
|
|
return Object.keys(SETTINGS).filter(n => SettingsStore.isFeature(n));
|
|
|
|
}
|
|
|
|
|
2019-02-22 23:33:20 +00:00
|
|
|
/**
|
|
|
|
* Watches for changes in a particular setting. This is done without any local echo
|
2019-02-26 19:43:10 +00:00
|
|
|
* wrapping and fires whenever a change is detected in a setting's value, at any level.
|
|
|
|
* Watching is intended to be used in scenarios where the app needs to react to changes
|
|
|
|
* made by other devices. It is otherwise expected that callers will be able to use the
|
|
|
|
* Controller system or track their own changes to settings. Callers should retain the
|
|
|
|
* returned reference to later unsubscribe from updates.
|
2019-02-22 23:33:20 +00:00
|
|
|
* @param {string} settingName The setting name to watch
|
|
|
|
* @param {String} roomId The room ID to watch for changes in. May be null for 'all'.
|
|
|
|
* @param {function} callbackFn A function to be called when a setting change is
|
2019-02-26 19:43:10 +00:00
|
|
|
* detected. Five arguments can be expected: the setting name, the room ID (may be null),
|
|
|
|
* the level the change happened at, the new value at the given level, and finally the new
|
|
|
|
* value for the setting regardless of level. The callback is responsible for determining
|
|
|
|
* if the change in value is worthwhile enough to react upon.
|
2019-02-22 23:33:20 +00:00
|
|
|
* @returns {string} A reference to the watcher that was employed.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static watchSetting(settingName: string, roomId: string, callbackFn: CallbackFn): string {
|
2019-02-22 23:33:20 +00:00
|
|
|
const setting = SETTINGS[settingName];
|
|
|
|
const originalSettingName = settingName;
|
|
|
|
if (!setting) throw new Error(`${settingName} is not a setting`);
|
|
|
|
|
|
|
|
if (setting.invertedSettingName) {
|
|
|
|
settingName = setting.invertedSettingName;
|
|
|
|
}
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
const watcherId = `${new Date().getTime()}_${SettingsStore.watcherCount++}_${settingName}_${roomId}`;
|
2019-02-22 23:33:20 +00:00
|
|
|
|
2019-02-26 19:43:10 +00:00
|
|
|
const localizedCallback = (changedInRoomId, atLevel, newValAtLevel) => {
|
|
|
|
const newValue = SettingsStore.getValue(originalSettingName);
|
|
|
|
callbackFn(originalSettingName, changedInRoomId, atLevel, newValAtLevel, newValue);
|
|
|
|
};
|
2019-02-22 23:33:20 +00:00
|
|
|
|
2021-05-19 08:06:01 +00:00
|
|
|
SettingsStore.watchers.set(watcherId, localizedCallback);
|
2019-02-26 19:43:10 +00:00
|
|
|
defaultWatchManager.watchSetting(settingName, roomId, localizedCallback);
|
2019-02-22 23:33:20 +00:00
|
|
|
|
|
|
|
return watcherId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stops the SettingsStore from watching a setting. This is a no-op if the watcher
|
|
|
|
* provided is not found.
|
|
|
|
* @param {string} watcherReference The watcher reference (received from #watchSetting)
|
|
|
|
* to cancel.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static unwatchSetting(watcherReference: string) {
|
2021-05-19 08:06:01 +00:00
|
|
|
if (!SettingsStore.watchers.has(watcherReference)) {
|
2020-01-27 22:36:12 +00:00
|
|
|
console.warn(`Ending non-existent watcher ID ${watcherReference}`);
|
|
|
|
return;
|
|
|
|
}
|
2019-02-22 23:33:20 +00:00
|
|
|
|
2021-05-19 08:06:01 +00:00
|
|
|
defaultWatchManager.unwatchSetting(SettingsStore.watchers.get(watcherReference));
|
|
|
|
SettingsStore.watchers.delete(watcherReference);
|
2019-02-22 23:33:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets up a monitor for a setting. This behaves similar to #watchSetting except instead
|
|
|
|
* of making a call to a callback, it forwards all changes to the dispatcher. Callers can
|
|
|
|
* expect to listen for the 'setting_updated' action with an object containing settingName,
|
2019-02-26 19:43:10 +00:00
|
|
|
* roomId, level, newValueAtLevel, and newValue.
|
2019-02-22 23:33:20 +00:00
|
|
|
* @param {string} settingName The setting name to monitor.
|
|
|
|
* @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static monitorSetting(settingName: string, roomId: string) {
|
2020-06-08 23:11:58 +00:00
|
|
|
roomId = roomId || null; // the thing wants null specifically to work, so appease it.
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
if (!this.monitors[settingName]) this.monitors[settingName] = {};
|
2019-02-22 23:33:20 +00:00
|
|
|
|
|
|
|
const registerWatcher = () => {
|
2020-07-29 16:57:14 +00:00
|
|
|
this.monitors[settingName][roomId] = SettingsStore.watchSetting(
|
2019-02-26 19:43:10 +00:00
|
|
|
settingName, roomId, (settingName, inRoomId, level, newValueAtLevel, newValue) => {
|
2019-02-22 23:33:20 +00:00
|
|
|
dis.dispatch({
|
|
|
|
action: 'setting_updated',
|
|
|
|
settingName,
|
|
|
|
roomId: inRoomId,
|
|
|
|
level,
|
2019-02-26 19:43:10 +00:00
|
|
|
newValueAtLevel,
|
2019-02-22 23:33:20 +00:00
|
|
|
newValue,
|
|
|
|
});
|
|
|
|
},
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
const hasRoom = Object.keys(this.monitors[settingName]).find((r) => r === roomId || r === null);
|
2019-02-22 23:33:20 +00:00
|
|
|
if (!hasRoom) {
|
|
|
|
registerWatcher();
|
|
|
|
} else {
|
|
|
|
if (roomId === null) {
|
|
|
|
// Unregister all existing watchers and register the new one
|
2020-07-29 16:57:14 +00:00
|
|
|
for (const roomId of Object.keys(this.monitors[settingName])) {
|
|
|
|
SettingsStore.unwatchSetting(this.monitors[settingName][roomId]);
|
2019-02-22 23:33:20 +00:00
|
|
|
}
|
2020-07-29 16:57:14 +00:00
|
|
|
this.monitors[settingName] = {};
|
2019-02-22 23:33:20 +00:00
|
|
|
registerWatcher();
|
|
|
|
} // else a watcher is already registered for the room, so don't bother registering it again
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-29 01:13:06 +00:00
|
|
|
/**
|
|
|
|
* Gets the translated display name for a given setting
|
|
|
|
* @param {string} settingName The setting to look up.
|
2020-07-29 16:57:14 +00:00
|
|
|
* @param {SettingLevel} atLevel
|
2017-10-30 03:48:29 +00:00
|
|
|
* The level to get the display name for; Defaults to 'default'.
|
2017-10-29 01:13:06 +00:00
|
|
|
* @return {String} The display name for the setting, or null if not found.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static getDisplayName(settingName: string, atLevel = SettingLevel.DEFAULT) {
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!SETTINGS[settingName] || !SETTINGS[settingName].displayName) return null;
|
2017-10-30 03:48:29 +00:00
|
|
|
|
|
|
|
let displayName = SETTINGS[settingName].displayName;
|
|
|
|
if (displayName instanceof Object) {
|
|
|
|
if (displayName[atLevel]) displayName = displayName[atLevel];
|
|
|
|
else displayName = displayName["default"];
|
|
|
|
}
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
return _t(displayName as string);
|
2017-10-29 01:13:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if a setting is also a feature.
|
|
|
|
* @param {string} settingName The setting to look up.
|
|
|
|
* @return {boolean} True if the setting is a feature.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static isFeature(settingName: string) {
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!SETTINGS[settingName]) return false;
|
|
|
|
return SETTINGS[settingName].isFeature;
|
|
|
|
}
|
|
|
|
|
2021-04-27 15:29:42 +00:00
|
|
|
public static getBetaInfo(settingName: string) {
|
2021-04-30 11:30:05 +00:00
|
|
|
// consider a beta disabled if the config is explicitly set to false, in which case treat as normal Labs flag
|
|
|
|
if (SettingsStore.isFeature(settingName)
|
|
|
|
&& SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, null, true, true) !== false
|
|
|
|
) {
|
|
|
|
return SETTINGS[settingName]?.betaInfo;
|
|
|
|
}
|
2021-04-27 15:29:42 +00:00
|
|
|
}
|
|
|
|
|
2020-09-18 18:09:45 +00:00
|
|
|
/**
|
|
|
|
* Determines if a setting is enabled.
|
|
|
|
* If a setting is disabled then it should be hidden from the user.
|
|
|
|
* @param {string} settingName The setting to look up.
|
|
|
|
* @return {boolean} True if the setting is enabled.
|
|
|
|
*/
|
2020-09-18 18:14:24 +00:00
|
|
|
public static isEnabled(settingName: string): boolean {
|
2020-09-16 11:55:04 +00:00
|
|
|
if (!SETTINGS[settingName]) return false;
|
|
|
|
return SETTINGS[settingName].controller ? !SETTINGS[settingName].controller.settingDisabled : true;
|
|
|
|
}
|
|
|
|
|
2017-10-29 01:13:06 +00:00
|
|
|
/**
|
|
|
|
* Gets the value of a setting. The room ID is optional if the setting is not to
|
|
|
|
* be applied to any particular room, otherwise it should be supplied.
|
|
|
|
* @param {string} settingName The name of the setting to read the value of.
|
|
|
|
* @param {String} roomId The room ID to read the setting value in, may be null.
|
2017-11-04 22:38:26 +00:00
|
|
|
* @param {boolean} excludeDefault True to disable using the default value.
|
2017-10-29 01:45:48 +00:00
|
|
|
* @return {*} The value, or null if not found
|
2017-10-29 01:13:06 +00:00
|
|
|
*/
|
2021-01-19 00:40:32 +00:00
|
|
|
public static getValue<T = any>(settingName: string, roomId: string = null, excludeDefault = false): T {
|
2017-11-16 02:24:32 +00:00
|
|
|
// Verify that the setting is actually a setting
|
|
|
|
if (!SETTINGS[settingName]) {
|
|
|
|
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
|
|
|
}
|
|
|
|
|
|
|
|
const setting = SETTINGS[settingName];
|
|
|
|
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
|
|
|
|
|
|
|
|
return SettingsStore.getValueAt(levelOrder[0], settingName, roomId, false, excludeDefault);
|
2017-10-30 02:44:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a setting's value at a particular level, ignoring all levels that are more specific.
|
2020-07-29 16:57:14 +00:00
|
|
|
* @param {SettingLevel|"config"|"default"} level The
|
2017-11-16 02:04:49 +00:00
|
|
|
* level to look at.
|
2017-10-30 02:44:00 +00:00
|
|
|
* @param {string} settingName The name of the setting to read.
|
|
|
|
* @param {String} roomId The room ID to read the setting value in, may be null.
|
2017-10-31 01:49:44 +00:00
|
|
|
* @param {boolean} explicit If true, this method will not consider other levels, just the one
|
|
|
|
* provided. Defaults to false.
|
2017-11-04 22:38:26 +00:00
|
|
|
* @param {boolean} excludeDefault True to disable using the default value.
|
2017-10-30 02:44:00 +00:00
|
|
|
* @return {*} The value, or null if not found.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static getValueAt(
|
|
|
|
level: SettingLevel,
|
|
|
|
settingName: string,
|
|
|
|
roomId: string = null,
|
|
|
|
explicit = false,
|
|
|
|
excludeDefault = false,
|
|
|
|
): any {
|
2017-11-04 21:46:15 +00:00
|
|
|
// Verify that the setting is actually a setting
|
2019-01-25 03:57:40 +00:00
|
|
|
const setting = SETTINGS[settingName];
|
|
|
|
if (!setting) {
|
2017-11-04 21:46:15 +00:00
|
|
|
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
|
|
|
}
|
|
|
|
|
2017-11-09 00:41:32 +00:00
|
|
|
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
|
2020-07-29 16:57:14 +00:00
|
|
|
if (!levelOrder.includes(SettingLevel.DEFAULT)) levelOrder.push(SettingLevel.DEFAULT); // always include default
|
2017-11-09 00:41:32 +00:00
|
|
|
|
|
|
|
const minIndex = levelOrder.indexOf(level);
|
|
|
|
if (minIndex === -1) throw new Error("Level " + level + " is not prioritized");
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
const handlers = SettingsStore.getHandlers(settingName);
|
2017-10-29 01:13:06 +00:00
|
|
|
|
2019-01-25 03:57:40 +00:00
|
|
|
// Check if we need to invert the setting at all. Do this after we get the setting
|
|
|
|
// handlers though, otherwise we'll fail to read the value.
|
|
|
|
if (setting.invertedSettingName) {
|
|
|
|
//console.warn(`Inverting ${settingName} to be ${setting.invertedSettingName} - legacy setting`);
|
|
|
|
settingName = setting.invertedSettingName;
|
|
|
|
}
|
|
|
|
|
2017-10-31 01:49:44 +00:00
|
|
|
if (explicit) {
|
2017-11-05 22:37:06 +00:00
|
|
|
const handler = handlers[level];
|
2019-01-25 03:57:40 +00:00
|
|
|
if (!handler) {
|
2020-07-29 16:57:14 +00:00
|
|
|
return SettingsStore.getFinalValue(setting, level, roomId, null, null);
|
2019-01-25 03:57:40 +00:00
|
|
|
}
|
2019-01-25 16:30:13 +00:00
|
|
|
const value = handler.getValue(settingName, roomId);
|
2020-07-29 16:57:14 +00:00
|
|
|
return SettingsStore.getFinalValue(setting, level, roomId, value, level);
|
2017-10-31 01:49:44 +00:00
|
|
|
}
|
|
|
|
|
2017-11-09 00:41:32 +00:00
|
|
|
for (let i = minIndex; i < levelOrder.length; i++) {
|
|
|
|
const handler = handlers[levelOrder[i]];
|
2017-10-29 01:45:48 +00:00
|
|
|
if (!handler) continue;
|
2017-11-09 00:41:32 +00:00
|
|
|
if (excludeDefault && levelOrder[i] === "default") continue;
|
2017-10-29 01:13:06 +00:00
|
|
|
|
2019-01-25 16:30:13 +00:00
|
|
|
const value = handler.getValue(settingName, roomId);
|
2017-10-29 07:43:52 +00:00
|
|
|
if (value === null || value === undefined) continue;
|
2020-07-29 16:57:14 +00:00
|
|
|
return SettingsStore.getFinalValue(setting, level, roomId, value, levelOrder[i]);
|
2017-10-29 01:45:48 +00:00
|
|
|
}
|
2017-10-29 01:13:06 +00:00
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
return SettingsStore.getFinalValue(setting, level, roomId, null, null);
|
2017-11-05 04:47:18 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 15:18:57 +00:00
|
|
|
/**
|
|
|
|
* Gets the default value of a setting.
|
|
|
|
* @param {string} settingName The name of the setting to read the value of.
|
2020-04-28 14:55:26 +00:00
|
|
|
* @param {String} roomId The room ID to read the setting value in, may be null.
|
2020-04-28 13:27:18 +00:00
|
|
|
* @return {*} The default value
|
2020-04-14 15:18:57 +00:00
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static getDefaultValue(settingName: string): any {
|
2020-04-14 15:18:57 +00:00
|
|
|
// Verify that the setting is actually a setting
|
|
|
|
if (!SETTINGS[settingName]) {
|
|
|
|
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return SETTINGS[settingName].default;
|
|
|
|
}
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
private static getFinalValue(
|
|
|
|
setting: ISetting,
|
|
|
|
level: SettingLevel,
|
|
|
|
roomId: string,
|
|
|
|
calculatedValue: any,
|
2020-07-30 14:41:51 +00:00
|
|
|
calculatedAtLevel: SettingLevel,
|
2020-07-29 16:57:14 +00:00
|
|
|
): any {
|
2019-01-25 16:04:56 +00:00
|
|
|
let resultingValue = calculatedValue;
|
2017-11-05 04:47:18 +00:00
|
|
|
|
2019-01-25 16:04:56 +00:00
|
|
|
if (setting.controller) {
|
|
|
|
const actualValue = setting.controller.getValueOverride(level, roomId, calculatedValue, calculatedAtLevel);
|
|
|
|
if (actualValue !== undefined && actualValue !== null) resultingValue = actualValue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (setting.invertedSettingName) resultingValue = !resultingValue;
|
|
|
|
return resultingValue;
|
2017-10-29 22:02:51 +00:00
|
|
|
}
|
2019-01-25 03:57:40 +00:00
|
|
|
|
2018-10-27 03:50:35 +00:00
|
|
|
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
|
2017-10-29 01:13:06 +00:00
|
|
|
/**
|
|
|
|
* Sets the value for a setting. The room ID is optional if the setting is not being
|
|
|
|
* set for a particular room, otherwise it should be supplied. The value may be null
|
|
|
|
* to indicate that the level should no longer have an override.
|
|
|
|
* @param {string} settingName The name of the setting to change.
|
|
|
|
* @param {String} roomId The room ID to change the value in, may be null.
|
2020-07-29 16:57:14 +00:00
|
|
|
* @param {SettingLevel} level The level
|
2017-10-29 01:13:06 +00:00
|
|
|
* to change the value at.
|
2017-10-29 01:45:48 +00:00
|
|
|
* @param {*} value The new value of the setting, may be null.
|
2017-10-29 01:13:06 +00:00
|
|
|
* @return {Promise} Resolves when the setting has been changed.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
|
2018-08-13 14:51:37 +00:00
|
|
|
/* eslint-enable valid-jsdoc */
|
2020-07-29 16:57:14 +00:00
|
|
|
public static async setValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise<void> {
|
2017-11-04 21:46:15 +00:00
|
|
|
// Verify that the setting is actually a setting
|
2019-01-25 03:57:40 +00:00
|
|
|
const setting = SETTINGS[settingName];
|
|
|
|
if (!setting) {
|
2017-11-04 21:46:15 +00:00
|
|
|
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
|
|
|
}
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
const handler = SettingsStore.getHandler(settingName, level);
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!handler) {
|
|
|
|
throw new Error("Setting " + settingName + " does not have a handler for " + level);
|
|
|
|
}
|
|
|
|
|
2019-01-25 03:57:40 +00:00
|
|
|
if (setting.invertedSettingName) {
|
|
|
|
// Note: We can't do this when the `level` is "default", however we also
|
|
|
|
// know that the user can't possible change the default value through this
|
|
|
|
// function so we don't bother checking it.
|
|
|
|
//console.warn(`Inverting ${settingName} to be ${setting.invertedSettingName} - legacy setting`);
|
|
|
|
settingName = setting.invertedSettingName;
|
|
|
|
value = !value;
|
|
|
|
}
|
|
|
|
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!handler.canSetValue(settingName, roomId)) {
|
2017-10-30 03:48:29 +00:00
|
|
|
throw new Error("User cannot set " + settingName + " at " + level + " in " + roomId);
|
2017-10-29 01:13:06 +00:00
|
|
|
}
|
|
|
|
|
2018-08-09 14:56:02 +00:00
|
|
|
await handler.setValue(settingName, roomId, value);
|
|
|
|
|
2019-01-25 03:57:40 +00:00
|
|
|
const controller = setting.controller;
|
2018-08-09 14:56:02 +00:00
|
|
|
if (controller) {
|
2017-11-05 04:47:18 +00:00
|
|
|
controller.onChange(level, roomId, value);
|
2018-08-09 14:56:02 +00:00
|
|
|
}
|
2017-10-29 01:13:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if the current user is permitted to set the given setting at the given
|
|
|
|
* level for a particular room. The room ID is optional if the setting is not being
|
|
|
|
* set for a particular room, otherwise it should be supplied.
|
|
|
|
* @param {string} settingName The name of the setting to check.
|
|
|
|
* @param {String} roomId The room ID to check in, may be null.
|
2020-07-29 16:57:14 +00:00
|
|
|
* @param {SettingLevel} level The level to
|
2017-10-29 01:13:06 +00:00
|
|
|
* check at.
|
|
|
|
* @return {boolean} True if the user may set the setting, false otherwise.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static canSetValue(settingName: string, roomId: string, level: SettingLevel): boolean {
|
2017-11-04 21:46:15 +00:00
|
|
|
// Verify that the setting is actually a setting
|
|
|
|
if (!SETTINGS[settingName]) {
|
|
|
|
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
|
|
|
}
|
|
|
|
|
2021-04-30 11:42:16 +00:00
|
|
|
// When non-beta features are specified in the config.json, we force them as enabled or disabled.
|
|
|
|
if (SettingsStore.isFeature(settingName) && !SETTINGS[settingName]?.betaInfo) {
|
2020-08-17 19:51:41 +00:00
|
|
|
const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true);
|
|
|
|
if (configVal === true || configVal === false) return false;
|
|
|
|
}
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
const handler = SettingsStore.getHandler(settingName, level);
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!handler) return false;
|
|
|
|
return handler.canSetValue(settingName, roomId);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines if the given level is supported on this device.
|
2020-07-29 16:57:14 +00:00
|
|
|
* @param {SettingLevel} level The level
|
2017-10-29 01:13:06 +00:00
|
|
|
* to check the feasibility of.
|
|
|
|
* @return {boolean} True if the level is supported, false otherwise.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static isLevelSupported(level: SettingLevel): boolean {
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!LEVEL_HANDLERS[level]) return false;
|
|
|
|
return LEVEL_HANDLERS[level].isSupported();
|
|
|
|
}
|
|
|
|
|
2021-01-14 17:30:25 +00:00
|
|
|
/**
|
|
|
|
* Determines the first supported level out of all the levels that can be used for a
|
|
|
|
* specific setting.
|
|
|
|
* @param {string} settingName The setting name.
|
|
|
|
* @return {SettingLevel}
|
|
|
|
*/
|
|
|
|
public static firstSupportedLevel(settingName: string): SettingLevel {
|
|
|
|
// Verify that the setting is actually a setting
|
|
|
|
const setting = SETTINGS[settingName];
|
|
|
|
if (!setting) {
|
|
|
|
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
|
|
|
|
}
|
|
|
|
|
|
|
|
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
|
|
|
|
if (!levelOrder.includes(SettingLevel.DEFAULT)) levelOrder.push(SettingLevel.DEFAULT); // always include default
|
|
|
|
|
|
|
|
const handlers = SettingsStore.getHandlers(settingName);
|
|
|
|
|
|
|
|
for (const level of levelOrder) {
|
|
|
|
const handler = handlers[level];
|
|
|
|
if (!handler) continue;
|
|
|
|
return level;
|
|
|
|
}
|
2021-01-14 17:37:18 +00:00
|
|
|
return null;
|
2021-01-14 17:30:25 +00:00
|
|
|
}
|
|
|
|
|
2019-01-25 03:57:40 +00:00
|
|
|
/**
|
|
|
|
* Debugging function for reading explicit setting values without going through the
|
|
|
|
* complicated/biased functions in the SettingsStore. This will print information to
|
|
|
|
* the console for analysis. Not intended to be used within the application.
|
|
|
|
* @param {string} realSettingName The setting name to try and read.
|
|
|
|
* @param {string} roomId Optional room ID to test the setting in.
|
|
|
|
*/
|
2020-07-29 16:57:14 +00:00
|
|
|
public static debugSetting(realSettingName: string, roomId: string) {
|
2019-01-25 03:57:40 +00:00
|
|
|
console.log(`--- DEBUG ${realSettingName}`);
|
|
|
|
|
|
|
|
// Note: we intentionally use JSON.stringify here to avoid the console masking the
|
|
|
|
// problem if there's a type representation issue. Also, this way it is guaranteed
|
|
|
|
// to show up in a rageshake if required.
|
|
|
|
|
|
|
|
const def = SETTINGS[realSettingName];
|
|
|
|
console.log(`--- definition: ${def ? JSON.stringify(def) : '<NOT_FOUND>'}`);
|
|
|
|
console.log(`--- default level order: ${JSON.stringify(LEVEL_ORDER)}`);
|
|
|
|
console.log(`--- registered handlers: ${JSON.stringify(Object.keys(LEVEL_HANDLERS))}`);
|
|
|
|
|
|
|
|
const doChecks = (settingName) => {
|
|
|
|
for (const handlerName of Object.keys(LEVEL_HANDLERS)) {
|
|
|
|
const handler = LEVEL_HANDLERS[handlerName];
|
|
|
|
|
|
|
|
try {
|
|
|
|
const value = handler.getValue(settingName, roomId);
|
|
|
|
console.log(`--- ${handlerName}@${roomId || '<no_room>'} = ${JSON.stringify(value)}`);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`--- ${handler}@${roomId || '<no_room>'} THREW ERROR: ${e.message}`);
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (roomId) {
|
|
|
|
try {
|
|
|
|
const value = handler.getValue(settingName, null);
|
|
|
|
console.log(`--- ${handlerName}@<no_room> = ${JSON.stringify(value)}`);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`--- ${handler}@<no_room> THREW ERROR: ${e.message}`);
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`--- calculating as returned by SettingsStore`);
|
|
|
|
console.log(`--- these might not match if the setting uses a controller - be warned!`);
|
|
|
|
|
|
|
|
try {
|
|
|
|
const value = SettingsStore.getValue(settingName, roomId);
|
|
|
|
console.log(`--- SettingsStore#generic@${roomId || '<no_room>'} = ${JSON.stringify(value)}`);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`--- SettingsStore#generic@${roomId || '<no_room>'} THREW ERROR: ${e.message}`);
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (roomId) {
|
|
|
|
try {
|
|
|
|
const value = SettingsStore.getValue(settingName, null);
|
|
|
|
console.log(`--- SettingsStore#generic@<no_room> = ${JSON.stringify(value)}`);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`--- SettingsStore#generic@$<no_room> THREW ERROR: ${e.message}`);
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const level of LEVEL_ORDER) {
|
|
|
|
try {
|
|
|
|
const value = SettingsStore.getValueAt(level, settingName, roomId);
|
|
|
|
console.log(`--- SettingsStore#${level}@${roomId || '<no_room>'} = ${JSON.stringify(value)}`);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`--- SettingsStore#${level}@${roomId || '<no_room>'} THREW ERROR: ${e.message}`);
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (roomId) {
|
|
|
|
try {
|
|
|
|
const value = SettingsStore.getValueAt(level, settingName, null);
|
|
|
|
console.log(`--- SettingsStore#${level}@<no_room> = ${JSON.stringify(value)}`);
|
|
|
|
} catch (e) {
|
|
|
|
console.log(`--- SettingsStore#${level}@$<no_room> THREW ERROR: ${e.message}`);
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
doChecks(realSettingName);
|
|
|
|
|
|
|
|
if (def.invertedSettingName) {
|
|
|
|
console.log(`--- TESTING INVERTED SETTING NAME`);
|
|
|
|
console.log(`--- inverted: ${def.invertedSettingName}`);
|
|
|
|
doChecks(def.invertedSettingName);
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(`--- END DEBUG`);
|
|
|
|
}
|
|
|
|
|
2020-07-29 16:57:14 +00:00
|
|
|
private static getHandler(settingName: string, level: SettingLevel): SettingsHandler {
|
|
|
|
const handlers = SettingsStore.getHandlers(settingName);
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!handlers[level]) return null;
|
|
|
|
return handlers[level];
|
|
|
|
}
|
|
|
|
|
2020-07-30 14:43:33 +00:00
|
|
|
private static getHandlers(settingName: string): IHandlerMap {
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!SETTINGS[settingName]) return {};
|
|
|
|
|
|
|
|
const handlers = {};
|
2017-10-29 02:21:34 +00:00
|
|
|
for (const level of SETTINGS[settingName].supportedLevels) {
|
2017-10-29 01:13:06 +00:00
|
|
|
if (!LEVEL_HANDLERS[level]) throw new Error("Unexpected level " + level);
|
2020-02-04 15:27:38 +00:00
|
|
|
if (SettingsStore.isLevelSupported(level)) handlers[level] = LEVEL_HANDLERS[level];
|
2017-10-29 01:13:06 +00:00
|
|
|
}
|
|
|
|
|
2017-10-29 23:08:39 +00:00
|
|
|
// Always support 'default'
|
|
|
|
if (!handlers['default']) handlers['default'] = LEVEL_HANDLERS['default'];
|
|
|
|
|
2017-10-29 01:13:06 +00:00
|
|
|
return handlers;
|
|
|
|
}
|
|
|
|
}
|
2019-01-25 03:57:40 +00:00
|
|
|
|
|
|
|
// For debugging purposes
|
2020-07-29 16:57:14 +00:00
|
|
|
window.mxSettingsStore = SettingsStore;
|