mirror of https://github.com/vector-im/riot-web
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
parent
c43bf336a9
commit
989bdcf5fb
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue