Rebuild SettingsStore to be better supported

This does away with the room- and account-style settings, and just replaces them with `supportedLevels`. The handlers have also been moved out to be in better support of the other options, like SdkConfig and per-room-per-device.

Signed-off-by: Travis Ralston <travpc@gmail.com>
pull/21833/head
Travis Ralston 2017-10-28 19:13:06 -06:00
parent c43bf336a9
commit 989bdcf5fb
10 changed files with 736 additions and 431 deletions

View File

@ -1,431 +0,0 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import MatrixClientPeg from './MatrixClientPeg';
const SETTINGS = [
/*
// EXAMPLE SETTING
{
name: "my-setting",
type: "room", // or "account"
ignoreLevels: [], // options: "device", "room-account", "account", "room"
// "room-account" and "room" don't apply for `type: account`.
defaults: {
your: "defaults",
},
},
*/
// TODO: Populate this
];
// This controls the priority of particular handlers. Handler order should match the
// documentation throughout this file, as should the `types`. The priority is directly
// related to the index in the map, where index 0 is highest preference.
const PRIORITY_MAP = [
{level: 'device', settingClass: DeviceSetting, types: ['room', 'account']},
{level: 'room-account', settingClass: RoomAccountSetting, types: ['room']},
{level: 'account', settingClass: AccountSetting, types: ['room', 'account']},
{level: 'room', settingClass: RoomSetting, types: ['room']},
{level: 'default', settingClass: DefaultSetting, types: ['room', 'account']},
// TODO: Add support for 'legacy' settings (old events, etc)
// TODO: Labs handler? (or make UserSettingsStore use this as a backend)
];
/**
* Controls and manages application settings at different levels through a variety of
* backends. Settings may be overridden at each level to provide the user with more
* options for customization and tailoring of their experience. These levels are most
* notably at the device, room, and account levels. The preferred order of levels is:
* - per-device
* - per-account in a particular room
* - per-account
* - per-room
* - defaults (as defined here)
*
* There are two types of settings: Account and Room.
*
* Account Settings use the same preferences described above, but do not look at the
* per-account in a particular room or the per-room levels. Account Settings are best
* used for things like which theme the user would prefer.
*
* Room settings use the exact preferences described above. Room Settings are best
* suited for settings which room administrators may want to define a default for the
* room members, or where users may want an individual room to be different. Using the
* setting definitions, particular preferences may be excluded to prevent, for example,
* room administrators from defining that all messages should have timestamps when the
* user may not want that. An example of a Room Setting would be URL previews.
*/
export default class GranularSettingStore {
/**
* Gets the content for an account setting.
* @param {string} name The name of the setting to lookup
* @returns {Promise<*>} Resolves to the content for the setting, or null if the
* value cannot be found.
*/
static getAccountSetting(name) {
const handlers = GranularSettingStore._getHandlers('account');
const initFn = (SettingClass) => new SettingClass('account', name);
return GranularSettingStore._iterateHandlers(handlers, initFn);
}
/**
* Gets the content for a room setting.
* @param {string} name The name of the setting to lookup
* @param {string} roomId The room ID to lookup the setting for
* @returns {Promise<*>} Resolves to the content for the setting, or null if the
* value cannot be found.
*/
static getRoomSetting(name, roomId) {
const handlers = GranularSettingStore._getHandlers('room');
const initFn = (SettingClass) => new SettingClass('room', name, roomId);
return GranularSettingStore._iterateHandlers(handlers, initFn);
}
static _iterateHandlers(handlers, initFn) {
let index = 0;
const wrapperFn = () => {
// If we hit the end with no result, return 'not found'
if (handlers.length >= index) return null;
// Get the handler, increment the index, and create a setting object
const handler = handlers[index++];
const setting = initFn(handler.settingClass);
// Try to read the value of the setting. If we get nothing for a value,
// then try the next handler. Otherwise, return the value early.
return Promise.resolve(setting.getValue()).then((value) => {
if (!value) return wrapperFn();
return value;
});
};
return wrapperFn();
}
/**
* Sets the content for a particular account setting at a given level in the hierarchy.
* If the setting does not exist at the given level, this will attempt to create it. The
* default level may not be modified.
* @param {string} name The name of the setting.
* @param {string} level The level to set the value of. Either 'device' or 'account'.
* @param {Object} content The value for the setting, or null to clear the level's value.
* @returns {Promise} Resolves when completed
*/
static setAccountSetting(name, level, content) {
const handler = GranularSettingStore._getHandler('account', level);
if (!handler) throw new Error("Missing account setting handler for " + name + " at " + level);
const SettingClass = handler.settingClass;
const setting = new SettingClass('account', name);
return Promise.resolve(setting.setValue(content));
}
/**
* Sets the content for a particular room setting at a given level in the hierarchy. If
* the setting does not exist at the given level, this will attempt to create it. The
* default level may not be modified.
* @param {string} name The name of the setting.
* @param {string} level The level to set the value of. One of 'device', 'room-account',
* 'account', or 'room'.
* @param {string} roomId The room ID to set the value of.
* @param {Object} content The value for the setting, or null to clear the level's value.
* @returns {Promise} Resolves when completed
*/
static setRoomSetting(name, level, roomId, content) {
const handler = GranularSettingStore._getHandler('room', level);
if (!handler) throw new Error("Missing room setting handler for " + name + " at " + level);
const SettingClass = handler.settingClass;
const setting = new SettingClass('room', name, roomId);
return Promise.resolve(setting.setValue(content));
}
/**
* Checks to ensure the current user may set the given account setting.
* @param {string} name The name of the setting.
* @param {string} level The level to check at. Either 'device' or 'account'.
* @returns {boolean} Whether or not the current user may set the account setting value.
*/
static canSetAccountSetting(name, level) {
const handler = GranularSettingStore._getHandler('account', level);
if (!handler) return false;
const SettingClass = handler.settingClass;
const setting = new SettingClass('account', name);
return setting.canSetValue();
}
/**
* Checks to ensure the current user may set the given room setting.
* @param {string} name The name of the setting.
* @param {string} level The level to check at. One of 'device', 'room-account', 'account',
* or 'room'.
* @param {string} roomId The room ID to check in.
* @returns {boolean} Whether or not the current user may set the room setting value.
*/
static canSetRoomSetting(name, level, roomId) {
const handler = GranularSettingStore._getHandler('room', level);
if (!handler) return false;
const SettingClass = handler.settingClass;
const setting = new SettingClass('room', name, roomId);
return setting.canSetValue();
}
/**
* Removes an account setting at a given level, forcing the level to inherit from an
* earlier stage in the hierarchy.
* @param {string} name The name of the setting.
* @param {string} level The level to clear. Either 'device' or 'account'.
*/
static removeAccountSetting(name, level) {
// This is just a convenience method.
GranularSettingStore.setAccountSetting(name, level, null);
}
/**
* Removes a room setting at a given level, forcing the level to inherit from an earlier
* stage in the hierarchy.
* @param {string} name The name of the setting.
* @param {string} level The level to clear. One of 'device', 'room-account', 'account',
* or 'room'.
* @param {string} roomId The room ID to clear the setting on.
*/
static removeRoomSetting(name, level, roomId) {
// This is just a convenience method.
GranularSettingStore.setRoomSetting(name, level, roomId, null);
}
/**
* Determines whether or not a particular level is supported on the current platform.
* @param {string} level The level to check. One of 'device', 'room-account', 'account',
* 'room', or 'default'.
* @returns {boolean} Whether or not the level is supported.
*/
static isLevelSupported(level) {
return GranularSettingStore._getHandlersAtLevel(level).length > 0;
}
static _getHandlersAtLevel(level) {
return PRIORITY_MAP.filter((h) => h.level === level && h.settingClass.isSupported());
}
static _getHandlers(type) {
return PRIORITY_MAP.filter((h) => {
if (!h.types.includes(type)) return false;
if (!h.settingClass.isSupported()) return false;
return true;
});
}
static _getHandler(type, level) {
const handlers = GranularSettingStore._getHandlers(type);
return handlers.filter((h) => h.level === level)[0];
}
}
// Validate of properties is assumed to be done well prior to instantiation of these classes,
// therefore these classes don't do any sanity checking. The following interface is assumed:
// constructor(type, name, roomId) - roomId may be null for type=='account'
// getValue() - returns a promise for the value. Falsey resolves are treated as 'not found'.
// setValue(content) - sets the new value for the setting. Falsey should remove the value.
// canSetValue() - returns true if the current user can set this setting.
// static isSupported() - returns true if the setting type is supported
class DefaultSetting {
constructor(type, name, roomId = null) {
this.type = type;
this.name = name;
this.roomId = roomId;
}
getValue() {
for (const setting of SETTINGS) {
if (setting.type === this.type && setting.name === this.name) {
return setting.defaults;
}
}
return null;
}
setValue() {
throw new Error("Operation not permitted: Cannot set value of a default setting.");
}
canSetValue() {
// It's a default, so no, you can't.
return false;
}
static isSupported() {
return true; // defaults are always accepted
}
}
class DeviceSetting {
constructor(type, name, roomId = null) {
this.type = type;
this.name = name;
this.roomId = roomId;
}
_getKey() {
return "mx_setting_" + this.name + "_" + this.type;
}
getValue() {
if (!localStorage) return null;
const value = localStorage.getItem(this._getKey());
if (!value) return null;
return JSON.parse(value);
}
setValue(content) {
if (!localStorage) throw new Error("Operation not possible: No device storage available.");
if (!content) localStorage.removeItem(this._getKey());
else localStorage.setItem(this._getKey(), JSON.stringify(content));
}
canSetValue() {
// The user likely has control over their own localstorage.
return true;
}
static isSupported() {
// We can only do something if we have localstorage
return !!localStorage;
}
}
class RoomAccountSetting {
constructor(type, name, roomId = null) {
this.type = type;
this.name = name;
this.roomId = roomId;
}
_getEventType() {
return "im.vector.setting." + this.type + "." + this.name;
}
getValue() {
if (!MatrixClientPeg.get()) return null;
const room = MatrixClientPeg.getRoom(this.roomId);
if (!room) return null;
const event = room.getAccountData(this._getEventType());
if (!event || !event.getContent()) return null;
return event.getContent();
}
setValue(content) {
if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg");
return MatrixClientPeg.get().setRoomAccountData(this.roomId, this._getEventType(), content);
}
canSetValue() {
// It's their own room account data, so they should be able to set it.
return true;
}
static isSupported() {
// We can only do something if we have a client
return !!MatrixClientPeg.get();
}
}
class AccountSetting {
constructor(type, name, roomId = null) {
this.type = type;
this.name = name;
this.roomId = roomId;
}
_getEventType() {
return "im.vector.setting." + this.type + "." + this.name;
}
getValue() {
if (!MatrixClientPeg.get()) return null;
return MatrixClientPeg.getAccountData(this._getEventType());
}
setValue(content) {
if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg");
return MatrixClientPeg.setAccountData(this._getEventType(), content);
}
canSetValue() {
// It's their own account data, so they should be able to set it
return true;
}
static isSupported() {
// We can only do something if we have a client
return !!MatrixClientPeg.get();
}
}
class RoomSetting {
constructor(type, name, roomId = null) {
this.type = type;
this.name = name;
this.roomId = roomId;
}
_getEventType() {
return "im.vector.setting." + this.type + "." + this.name;
}
getValue() {
if (!MatrixClientPeg.get()) return null;
const room = MatrixClientPeg.get().getRoom(this.roomId);
if (!room) return null;
const stateEvent = room.currentState.getStateEvents(this._getEventType(), "");
if (!stateEvent || !stateEvent.getContent()) return null;
return stateEvent.getContent();
}
setValue(content) {
if (!MatrixClientPeg.get()) throw new Error("Operation not possible: No client peg");
return MatrixClientPeg.get().sendStateEvent(this.roomId, this._getEventType(), content, "");
}
canSetValue() {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.roomId);
if (!room) return false; // They're not in the room, likely.
return room.currentState.maySendStateEvent(this._getEventType(), cli.getUserId());
}
static isSupported() {
// We can only do something if we have a client
return !!MatrixClientPeg.get();
}
}

View File

@ -0,0 +1,47 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../MatrixClientPeg';
/**
* Gets and sets settings at the "account" level for the current user.
* This handler does not make use of the roomId parameter.
*/
export default class AccountSettingHandler extends SettingsHandler {
getValue(settingName, roomId) {
const value = MatrixClientPeg.get().getAccountData(this._getEventType(settingName));
if (!value) return Promise.reject();
return Promise.resolve(value);
}
setValue(settingName, roomId, newValue) {
return MatrixClientPeg.get().setAccountData(this._getEventType(settingName), newValue);
}
canSetValue(settingName, roomId) {
return true; // It's their account, so they should be able to
}
isSupported() {
return !!MatrixClientPeg.get();
}
_getEventType(settingName) {
return "im.vector.setting." + settingName;
}
}

View File

@ -0,0 +1,43 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
import SdkConfig from "../SdkConfig";
/**
* Gets and sets settings at the "config" level. This handler does not make use of the
* roomId parameter.
*/
export default class ConfigSettingsHandler extends SettingsHandler {
getValue(settingName, roomId) {
const settingsConfig = SdkConfig.get()["settingDefaults"];
if (!settingsConfig || !settingsConfig[settingName]) return Promise.reject();
return Promise.resolve(settingsConfig[settingName]);
}
setValue(settingName, roomId, newValue) {
throw new Error("Cannot change settings at the config level");
}
canSetValue(settingName, roomId) {
return false;
}
isSupported() {
return true; // SdkConfig is always there
}
}

View File

@ -0,0 +1,51 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
/**
* Gets settings at the "default" level. This handler does not support setting values.
* This handler does not make use of the roomId parameter.
*/
export default class DefaultSettingsHandler extends SettingsHandler {
/**
* Creates a new default settings handler with the given defaults
* @param {object} defaults The default setting values, keyed by setting name.
*/
constructor(defaults) {
super();
this._defaults = defaults;
}
getValue(settingName, roomId) {
const value = this._defaults[settingName];
if (!value) return Promise.reject();
return Promise.resolve(value);
}
setValue(settingName, roomId, newValue) {
throw new Error("Cannot set values on the default level handler");
}
canSetValue(settingName, roomId) {
return false;
}
isSupported() {
return true;
}
}

View File

@ -0,0 +1,90 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from "../MatrixClientPeg";
/**
* 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 {
/**
* Creates a new device settings handler
* @param {string[]} featureNames The names of known features.
*/
constructor(featureNames) {
super();
this._featureNames = featureNames;
}
getValue(settingName, roomId) {
if (this._featureNames.includes(settingName)) {
return Promise.resolve(this._readFeature(settingName));
}
const value = localStorage.getItem(this._getKey(settingName));
if (!value) return Promise.reject();
return Promise.resolve(value);
}
setValue(settingName, roomId, newValue) {
if (this._featureNames.includes(settingName)) {
return Promise.resolve(this._writeFeature(settingName));
}
if (newValue === null) {
localStorage.removeItem(this._getKey(settingName));
} else {
localStorage.setItem(this._getKey(settingName), newValue);
}
return Promise.resolve();
}
canSetValue(settingName, roomId) {
return true; // It's their device, so they should be able to
}
isSupported() {
return !!localStorage;
}
_getKey(settingName) {
return "mx_setting_" + settingName;
}
// Note: features intentionally don't use the same key as settings to avoid conflicts
// and to be backwards compatible.
_readFeature(featureName) {
if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest()) {
// Guests should not have any labs features enabled.
return {enabled: false};
}
const value = localStorage.getItem("mx_labs_feature_" + featureName);
const enabled = value === "true";
return {enabled};
}
_writeFeature(featureName, enabled) {
localStorage.setItem("mx_labs_feature_" + featureName, enabled);
}
}

View File

@ -0,0 +1,52 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../MatrixClientPeg';
/**
* Gets and sets settings at the "room-account" level for the current user.
*/
export default class RoomAccountSettingsHandler extends SettingsHandler {
getValue(settingName, roomId) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return Promise.reject();
const value = room.getAccountData(this._getEventType(settingName));
if (!value) return Promise.reject();
return Promise.resolve(value);
}
setValue(settingName, roomId, newValue) {
return MatrixClientPeg.get().setRoomAccountData(
roomId, this._getEventType(settingName), newValue
);
}
canSetValue(settingName, roomId) {
const room = MatrixClientPeg.get().getRoom(roomId);
return !!room; // If they have the room, they can set their own account data
}
isSupported() {
return !!MatrixClientPeg.get();
}
_getEventType(settingName) {
return "im.vector.setting." + settingName;
}
}

View File

@ -0,0 +1,52 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
/**
* Gets and sets settings at the "room-device" level for the current device in a particular
* room.
*/
export default class RoomDeviceSettingsHandler extends SettingsHandler {
getValue(settingName, roomId) {
const value = localStorage.getItem(this._getKey(settingName, roomId));
if (!value) return Promise.reject();
return Promise.resolve(value);
}
setValue(settingName, roomId, newValue) {
if (newValue === null) {
localStorage.removeItem(this._getKey(settingName, roomId));
} else {
localStorage.setItem(this._getKey(settingName, roomId), newValue);
}
return Promise.resolve();
}
canSetValue(settingName, roomId) {
return true; // It's their device, so they should be able to
}
isSupported() {
return !!localStorage;
}
_getKey(settingName, roomId) {
return "mx_setting_" + settingName + "_" + roomId;
}
}

View File

@ -0,0 +1,56 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../MatrixClientPeg';
/**
* Gets and sets settings at the "room" level.
*/
export default class RoomSettingsHandler extends SettingsHandler {
getValue(settingName, roomId) {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return Promise.reject();
const event = room.currentState.getStateEvents(this._getEventType(settingName), "");
if (!event || !event.getContent()) return Promise.reject();
return Promise.resolve(event.getContent());
}
setValue(settingName, roomId, newValue) {
return MatrixClientPeg.get().sendStateEvent(
roomId, this._getEventType(settingName), newValue, ""
);
}
canSetValue(settingName, roomId) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId);
const eventType = this._getEventType(settingName);
if (!room) return false;
return room.currentState.maySendStateEvent(eventType, cli.getUserId());
}
isSupported() {
return !!MatrixClientPeg.get();
}
_getEventType(settingName) {
return "im.vector.setting." + settingName;
}
}

View File

@ -0,0 +1,70 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
/**
* Represents the base class for all level handlers. This class performs no logic
* and should be overridden.
*/
export default class SettingsHandler {
/**
* Gets the value for a particular setting at this level for a particular room.
* If no room is applicable, the roomId may be null. The roomId may not be
* applicable to this level and may be ignored by the handler.
* @param {string} settingName The name of the setting.
* @param {String} roomId The room ID to read from, may be null.
* @return {Promise<object>} Resolves to the setting value. Rejected if the value
* could not be found.
*/
getValue(settingName, roomId) {
throw new Error("Operation not possible: getValue was not overridden");
}
/**
* Sets the value for a particular setting at this level for a particular room.
* If no room is applicable, the roomId may be null. The roomId may not be
* applicable to this level and may be ignored by the handler. Setting a value
* to null will cause the level to remove the value. The current user should be
* able to set the value prior to calling this.
* @param {string} settingName The name of the setting to change.
* @param {String} roomId The room ID to set the value in, may be null.
* @param {Object} newValue The new value for the setting, may be null.
* @return {Promise} Resolves when the setting has been saved.
*/
setValue(settingName, roomId, newValue) {
throw new Error("Operation not possible: setValue was not overridden");
}
/**
* Determines if the current user is able to set the value of the given setting
* in the given room at this level.
* @param {string} settingName The name of the setting to check.
* @param {String} roomId The room ID to check in, may be null
* @returns {boolean} True if the setting can be set by the user, false otherwise.
*/
canSetValue(settingName, roomId) {
return false;
}
/**
* Determines if this level is supported on this device.
* @returns {boolean} True if this level is supported on the current device.
*/
isSupported() {
return false;
}
}

View File

@ -0,0 +1,275 @@
/*
Copyright 2017 Travis Ralston
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 Promise from 'bluebird';
import DeviceSettingsHandler from "./DeviceSettingsHandler";
import RoomDeviceSettingsHandler from "./RoomDeviceSettingsHandler";
import DefaultSettingsHandler from "./DefaultSettingsHandler";
import RoomAccountSettingsHandler from "./RoomAccountSettingsHandler";
import AccountSettingsHandler from "./AccountSettingsHandler";
import RoomSettingsHandler from "./RoomSettingsHandler";
import ConfigSettingsHandler from "./ConfigSettingsHandler";
import {_t, _td} from '../languageHandler';
import SdkConfig from "../SdkConfig";
// Preset levels for room-based settings (eg: URL previews).
// Doesn't include 'room' because most settings don't need it. Use .concat('room') to add.
const LEVELS_PRESET_ROOM = ['device', 'room-device', 'room-account', 'account'];
// Preset levels for account-based settings (eg: interface language).
const LEVELS_PRESET_ACCOUNT = ['device', 'account'];
// Preset levels for features (labs) settings.
const LEVELS_PRESET_FEATURE = ['device'];
const SETTINGS = {
"my-setting": {
isFeature: false, // optional
displayName: _td("Cool Name"),
supportedLevels: [
// The order does not matter.
"device", // Affects the current device only
"room-device", // Affects the current room on the current device
"room-account", // Affects the current room for the current account
"account", // Affects the current account
"room", // Affects the current room (controlled by room admins)
// "default" and "config" are always supported and do not get listed here.
],
defaults: {
your: "value",
},
},
// TODO: Populate this
};
// Convert the above into simpler formats for the handlers
let defaultSettings = {};
let featureNames = [];
for (let key of Object.keys(SETTINGS)) {
defaultSettings[key] = SETTINGS[key].defaults;
if (SETTINGS[key].isFeature) featureNames.push(key);
}
const LEVEL_HANDLERS = {
"device": new DeviceSettingsHandler(featureNames),
"room-device": new RoomDeviceSettingsHandler(),
"room-account": new RoomAccountSettingsHandler(),
"account": new AccountSettingsHandler(),
"room": new RoomSettingsHandler(),
"config": new ConfigSettingsHandler(),
"default": new DefaultSettingsHandler(defaultSettings),
};
/**
* 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:
* - "device" - Values are determined by the current device
* - "room-device" - Values are determined by the current device for a particular room
* - "room-account" - Values are determined by the current account for a particular room
* - "account" - Values are determined by the current account
* - "room" - Values are determined by a particular room (by the room admins)
* - "config" - Values are determined by the config.json
* - "default" - Values are determined by the hardcoded defaults
*
* 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 {
/**
* Gets the translated display name for a given setting
* @param {string} settingName The setting to look up.
* @return {String} The display name for the setting, or null if not found.
*/
static getDisplayName(settingName) {
if (!SETTINGS[settingName] || !SETTINGS[settingName].displayName) return null;
return _t(SETTINGS[settingName].displayName);
}
/**
* 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.
*/
static isFeature(settingName) {
if (!SETTINGS[settingName]) return false;
return SETTINGS[settingName].isFeature;
}
/**
* Determines if a given feature is enabled. The feature given must be a known
* feature.
* @param {string} settingName The name of the setting that is a feature.
* @param {String} roomId The optional room ID to validate in, may be null.
* @return {boolean} True if the feature is enabled, false otherwise
*/
static isFeatureEnabled(settingName, roomId = null) {
if (!SettingsStore.isFeature(settingName)) {
throw new Error("Setting " + settingName + " is not a feature");
}
// Synchronously get the setting value (which should be {enabled: true/false})
const value = Promise.coroutine(function* () {
return yield SettingsStore.getValue(settingName, roomId);
})();
return value.enabled;
}
/**
* 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.
* @return {Promise<*>} Resolves to the value for the setting. May result in null.
*/
static getValue(settingName, roomId) {
const levelOrder = [
'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default'
];
if (SettingsStore.isFeature(settingName)) {
const configValue = SettingsStore._getFeatureState(settingName);
if (configValue === "enable") return Promise.resolve({enabled: true});
if (configValue === "disable") return Promise.resolve({enabled: false});
// else let it fall through the default process
}
const handlers = SettingsStore._getHandlers(settingName);
// This wrapper function allows for iterating over the levelOrder to find a suitable
// handler that is supported by the setting. It does this by building the promise chain
// on the fly, wrapping the rejection from handler.getValue() to try the next handler.
// If the last handler also rejects the getValue() call, then this wrapper will convert
// the reply to `null` as per our contract to the caller.
let index = 0;
const wrapperFn = () => {
// Find the next handler that we can use
let handler = null;
while (!handler && index < levelOrder.length) {
handler = handlers[levelOrder[index++]];
}
// No handler == no reply (happens when the last available handler rejects)
if (!handler) return null;
// Get the value and see if the handler will reject us (meaning it doesn't have
// a value for us).
const value = handler.getValue(settingName, roomId);
return value.then(null, () => wrapperFn()); // pass success through
};
return wrapperFn();
}
/**
* 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.
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level
* to change the value at.
* @param {Object} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
*/
static setValue(settingName, roomId, level, value) {
const handler = SettingsStore._getHandler(settingName, level);
if (!handler) {
throw new Error("Setting " + settingName + " does not have a handler for " + level);
}
if (!handler.canSetValue(settingName, roomId)) {
throw new Error("User cannot set " + settingName + " at level " + level);
}
return handler.setValue(settingName, roomId, value);
}
/**
* 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.
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to
* check at.
* @return {boolean} True if the user may set the setting, false otherwise.
*/
static canSetValue(settingName, roomId, level) {
const handler = SettingsStore._getHandler(settingName, level);
if (!handler) return false;
return handler.canSetValue(settingName, roomId);
}
/**
* Determines if the given level is supported on this device.
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level
* to check the feasibility of.
* @return {boolean} True if the level is supported, false otherwise.
*/
static isLevelSupported(level) {
if (!LEVEL_HANDLERS[level]) return false;
return LEVEL_HANDLERS[level].isSupported();
}
static _getHandler(settingName, level) {
const handlers = SettingsStore._getHandlers(settingName);
if (!handlers[level]) return null;
return handlers[level];
}
static _getHandlers(settingName) {
if (!SETTINGS[settingName]) return {};
const handlers = {};
for (let level of SETTINGS[settingName].supportedLevels) {
if (!LEVEL_HANDLERS[level]) throw new Error("Unexpected level " + level);
handlers[level] = LEVEL_HANDLERS[level];
}
return handlers;
}
static _getFeatureState(settingName) {
const featuresConfig = SdkConfig.get()['features'];
const enableLabs = SdkConfig.get()['enableLabs']; // we'll honour the old flag
let featureState = enableLabs ? "labs" : "disable";
if (featuresConfig && featuresConfig[settingName] !== undefined) {
featureState = featuresConfig[settingName];
}
const allowedStates = ['enable', 'disable', 'labs'];
if (!allowedStates.contains(featureState)) {
console.warn("Feature state '" + featureState + "' is invalid for " + settingName);
featureState = "disable"; // to prevent accidental features.
}
return featureState;
}
}