diff --git a/src/GranularSettingStore.js b/src/GranularSettingStore.js deleted file mode 100644 index 96b06fc66c..0000000000 --- a/src/GranularSettingStore.js +++ /dev/null @@ -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(); - } -} diff --git a/src/settings/AccountSettingsHandler.js b/src/settings/AccountSettingsHandler.js new file mode 100644 index 0000000000..6352da5ccc --- /dev/null +++ b/src/settings/AccountSettingsHandler.js @@ -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; + } +} \ No newline at end of file diff --git a/src/settings/ConfigSettingsHandler.js b/src/settings/ConfigSettingsHandler.js new file mode 100644 index 0000000000..8f0cc9041b --- /dev/null +++ b/src/settings/ConfigSettingsHandler.js @@ -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 + } +} \ No newline at end of file diff --git a/src/settings/DefaultSettingsHandler.js b/src/settings/DefaultSettingsHandler.js new file mode 100644 index 0000000000..06937fd957 --- /dev/null +++ b/src/settings/DefaultSettingsHandler.js @@ -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; + } +} \ No newline at end of file diff --git a/src/settings/DeviceSettingsHandler.js b/src/settings/DeviceSettingsHandler.js new file mode 100644 index 0000000000..83cf88bcba --- /dev/null +++ b/src/settings/DeviceSettingsHandler.js @@ -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); + } +} \ No newline at end of file diff --git a/src/settings/RoomAccountSettingsHandler.js b/src/settings/RoomAccountSettingsHandler.js new file mode 100644 index 0000000000..7157d86c34 --- /dev/null +++ b/src/settings/RoomAccountSettingsHandler.js @@ -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; + } +} \ No newline at end of file diff --git a/src/settings/RoomDeviceSettingsHandler.js b/src/settings/RoomDeviceSettingsHandler.js new file mode 100644 index 0000000000..fe477564f6 --- /dev/null +++ b/src/settings/RoomDeviceSettingsHandler.js @@ -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; + } +} \ No newline at end of file diff --git a/src/settings/RoomSettingsHandler.js b/src/settings/RoomSettingsHandler.js new file mode 100644 index 0000000000..dcd7a76e87 --- /dev/null +++ b/src/settings/RoomSettingsHandler.js @@ -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; + } +} \ No newline at end of file diff --git a/src/settings/SettingsHandler.js b/src/settings/SettingsHandler.js new file mode 100644 index 0000000000..7387712367 --- /dev/null +++ b/src/settings/SettingsHandler.js @@ -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} 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; + } +} \ No newline at end of file diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js new file mode 100644 index 0000000000..eea91345d8 --- /dev/null +++ b/src/settings/SettingsStore.js @@ -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; + } +}