From 8c3e5ebbad4ff59ce7f28f08b8942e0cf6355d89 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 22 Oct 2017 20:15:20 -0600 Subject: [PATCH 01/51] Create GranularSettingStore GranularSettingStore is a class to manage settings of varying granularity, such as URL previews at the device, room, and account levels. Signed-off-by: Travis Ralston --- src/GranularSettingStore.js | 426 ++++++++++++++++++++++++++++++++++++ 1 file changed, 426 insertions(+) create mode 100644 src/GranularSettingStore.js diff --git a/src/GranularSettingStore.js b/src/GranularSettingStore.js new file mode 100644 index 0000000000..9e8bbf093e --- /dev/null +++ b/src/GranularSettingStore.js @@ -0,0 +1,426 @@ +/* +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 Granular Settings through use of localStorage, account data, + * and room state. Granular Settings are user settings that can have overrides at + * particular levels, notably the device, account, and room level. With the topmost + * level being the preferred setting, the override procedure is: + * - localstorage (per-device) + * - room account data (per-account in room) + * - account data (per-account) + * - room state event (per-room) + * - default (defined by Riot) + * + * There are two types of settings: Account and Room. + * + * Account Settings use the same override procedure described above, but drop the room + * account data and room state event checks. Account Settings are best used for things + * like which theme the user would prefer. + * + * Room Settings use the exact override procedure described above. Room Settings are + * best suited for settings which room administrators may want to define a default + * for members of the room, such as the case is with URL previews. Room Settings may + * also elect to not allow the room state event check, allowing for per-room settings + * that are not defaulted by the room administrator. + */ +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 setting = new handler.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 setting = new handler.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 setting = new handler.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 setting = new handler.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 (let 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(); + } +} From e02dcae3b65aec1d3bb8502ef9b815254d449ebd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 22 Oct 2017 21:00:35 -0600 Subject: [PATCH 02/51] Change wording to better describe the class Signed-off-by: Travis Ralston --- src/GranularSettingStore.js | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/GranularSettingStore.js b/src/GranularSettingStore.js index 9e8bbf093e..c6e7de2b1e 100644 --- a/src/GranularSettingStore.js +++ b/src/GranularSettingStore.js @@ -49,27 +49,28 @@ const PRIORITY_MAP = [ ]; /** - * Controls and manages Granular Settings through use of localStorage, account data, - * and room state. Granular Settings are user settings that can have overrides at - * particular levels, notably the device, account, and room level. With the topmost - * level being the preferred setting, the override procedure is: - * - localstorage (per-device) - * - room account data (per-account in room) - * - account data (per-account) - * - room state event (per-room) - * - default (defined by Riot) + * 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 override procedure described above, but drop the room - * account data and room state event checks. Account Settings are best used for things - * like which theme the user would prefer. + * 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 override procedure described above. Room Settings are - * best suited for settings which room administrators may want to define a default - * for members of the room, such as the case is with URL previews. Room Settings may - * also elect to not allow the room state event check, allowing for per-room settings - * that are not defaulted by the room administrator. + * 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 { /** From c43bf336a932654003612e5e73075223be8e1dca Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 22 Oct 2017 21:07:01 -0600 Subject: [PATCH 03/51] Appease the linter Signed-off-by: Travis Ralston --- src/GranularSettingStore.js | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/GranularSettingStore.js b/src/GranularSettingStore.js index c6e7de2b1e..96b06fc66c 100644 --- a/src/GranularSettingStore.js +++ b/src/GranularSettingStore.js @@ -81,7 +81,7 @@ export default class GranularSettingStore { */ static getAccountSetting(name) { const handlers = GranularSettingStore._getHandlers('account'); - const initFn = settingClass => new settingClass('account', name); + const initFn = (SettingClass) => new SettingClass('account', name); return GranularSettingStore._iterateHandlers(handlers, initFn); } @@ -94,7 +94,7 @@ export default class GranularSettingStore { */ static getRoomSetting(name, roomId) { const handlers = GranularSettingStore._getHandlers('room'); - const initFn = settingClass => new settingClass('room', name, roomId); + const initFn = (SettingClass) => new SettingClass('room', name, roomId); return GranularSettingStore._iterateHandlers(handlers, initFn); } @@ -110,7 +110,7 @@ export default class GranularSettingStore { // 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 => { + return Promise.resolve(setting.getValue()).then((value) => { if (!value) return wrapperFn(); return value; }); @@ -131,7 +131,8 @@ export default class GranularSettingStore { const handler = GranularSettingStore._getHandler('account', level); if (!handler) throw new Error("Missing account setting handler for " + name + " at " + level); - const setting = new handler.settingClass('account', name); + const SettingClass = handler.settingClass; + const setting = new SettingClass('account', name); return Promise.resolve(setting.setValue(content)); } @@ -150,7 +151,8 @@ export default class GranularSettingStore { const handler = GranularSettingStore._getHandler('room', level); if (!handler) throw new Error("Missing room setting handler for " + name + " at " + level); - const setting = new handler.settingClass('room', name, roomId); + const SettingClass = handler.settingClass; + const setting = new SettingClass('room', name, roomId); return Promise.resolve(setting.setValue(content)); } @@ -164,7 +166,8 @@ export default class GranularSettingStore { const handler = GranularSettingStore._getHandler('account', level); if (!handler) return false; - const setting = new handler.settingClass('account', name); + const SettingClass = handler.settingClass; + const setting = new SettingClass('account', name); return setting.canSetValue(); } @@ -180,7 +183,8 @@ export default class GranularSettingStore { const handler = GranularSettingStore._getHandler('room', level); if (!handler) return false; - const setting = new handler.settingClass('room', name, roomId); + const SettingClass = handler.settingClass; + const setting = new SettingClass('room', name, roomId); return setting.canSetValue(); } @@ -219,11 +223,11 @@ export default class GranularSettingStore { } static _getHandlersAtLevel(level) { - return PRIORITY_MAP.filter(h => h.level === level && h.settingClass.isSupported()); + return PRIORITY_MAP.filter((h) => h.level === level && h.settingClass.isSupported()); } static _getHandlers(type) { - return PRIORITY_MAP.filter(h => { + return PRIORITY_MAP.filter((h) => { if (!h.types.includes(type)) return false; if (!h.settingClass.isSupported()) return false; @@ -233,7 +237,7 @@ export default class GranularSettingStore { static _getHandler(type, level) { const handlers = GranularSettingStore._getHandlers(type); - return handlers.filter(h => h.level === level)[0]; + return handlers.filter((h) => h.level === level)[0]; } } @@ -253,7 +257,7 @@ class DefaultSetting { } getValue() { - for (let setting of SETTINGS) { + for (const setting of SETTINGS) { if (setting.type === this.type && setting.name === this.name) { return setting.defaults; } From 989bdcf5fbe800f4e64fa952608aeef6cfe3b2a3 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 28 Oct 2017 19:13:06 -0600 Subject: [PATCH 04/51] 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 --- src/GranularSettingStore.js | 431 --------------------- src/settings/AccountSettingsHandler.js | 47 +++ src/settings/ConfigSettingsHandler.js | 43 ++ src/settings/DefaultSettingsHandler.js | 51 +++ src/settings/DeviceSettingsHandler.js | 90 +++++ src/settings/RoomAccountSettingsHandler.js | 52 +++ src/settings/RoomDeviceSettingsHandler.js | 52 +++ src/settings/RoomSettingsHandler.js | 56 +++ src/settings/SettingsHandler.js | 70 ++++ src/settings/SettingsStore.js | 275 +++++++++++++ 10 files changed, 736 insertions(+), 431 deletions(-) delete mode 100644 src/GranularSettingStore.js create mode 100644 src/settings/AccountSettingsHandler.js create mode 100644 src/settings/ConfigSettingsHandler.js create mode 100644 src/settings/DefaultSettingsHandler.js create mode 100644 src/settings/DeviceSettingsHandler.js create mode 100644 src/settings/RoomAccountSettingsHandler.js create mode 100644 src/settings/RoomDeviceSettingsHandler.js create mode 100644 src/settings/RoomSettingsHandler.js create mode 100644 src/settings/SettingsHandler.js create mode 100644 src/settings/SettingsStore.js 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; + } +} From 23d159e21cf2f2c623fe63dcb88468ed3b6a6ff7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 28 Oct 2017 19:45:48 -0600 Subject: [PATCH 05/51] Make reading settings synchronous Signed-off-by: Travis Ralston --- src/settings/AccountSettingsHandler.js | 14 ++++---- src/settings/ConfigSettingsHandler.js | 4 +-- src/settings/DefaultSettingsHandler.js | 4 +-- src/settings/DeviceSettingsHandler.js | 12 +++---- src/settings/RoomAccountSettingsHandler.js | 22 ++++++------- src/settings/RoomDeviceSettingsHandler.js | 5 +-- src/settings/RoomSettingsHandler.js | 26 +++++++-------- src/settings/SettingsHandler.js | 2 +- src/settings/SettingsStore.js | 38 +++++++--------------- 9 files changed, 56 insertions(+), 71 deletions(-) diff --git a/src/settings/AccountSettingsHandler.js b/src/settings/AccountSettingsHandler.js index 6352da5ccc..c505afe31d 100644 --- a/src/settings/AccountSettingsHandler.js +++ b/src/settings/AccountSettingsHandler.js @@ -24,13 +24,13 @@ import MatrixClientPeg from '../MatrixClientPeg'; */ 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); + return this._getSettings()[settingName]; } setValue(settingName, roomId, newValue) { - return MatrixClientPeg.get().setAccountData(this._getEventType(settingName), newValue); + const content = this._getSettings(); + content[settingName] = newValue; + return MatrixClientPeg.get().setAccountData("im.vector.web.settings", content); } canSetValue(settingName, roomId) { @@ -41,7 +41,9 @@ export default class AccountSettingHandler extends SettingsHandler { return !!MatrixClientPeg.get(); } - _getEventType(settingName) { - return "im.vector.setting." + settingName; + _getSettings() { + const event = MatrixClientPeg.get().getAccountData("im.vector.web.settings"); + if (!event || !event.getContent()) return {}; + return event.getContent(); } } \ No newline at end of file diff --git a/src/settings/ConfigSettingsHandler.js b/src/settings/ConfigSettingsHandler.js index 8f0cc9041b..5307a1dac1 100644 --- a/src/settings/ConfigSettingsHandler.js +++ b/src/settings/ConfigSettingsHandler.js @@ -25,8 +25,8 @@ import SdkConfig from "../SdkConfig"; 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]); + if (!settingsConfig || !settingsConfig[settingName]) return null; + return settingsConfig[settingName]; } setValue(settingName, roomId, newValue) { diff --git a/src/settings/DefaultSettingsHandler.js b/src/settings/DefaultSettingsHandler.js index 06937fd957..0a4b8d91d3 100644 --- a/src/settings/DefaultSettingsHandler.js +++ b/src/settings/DefaultSettingsHandler.js @@ -32,9 +32,7 @@ export default class DefaultSettingsHandler extends SettingsHandler { } getValue(settingName, roomId) { - const value = this._defaults[settingName]; - if (!value) return Promise.reject(); - return Promise.resolve(value); + return this._defaults[settingName]; } setValue(settingName, roomId, newValue) { diff --git a/src/settings/DeviceSettingsHandler.js b/src/settings/DeviceSettingsHandler.js index 83cf88bcba..dbb833c570 100644 --- a/src/settings/DeviceSettingsHandler.js +++ b/src/settings/DeviceSettingsHandler.js @@ -35,12 +35,13 @@ export default class DeviceSettingsHandler extends SettingsHandler { getValue(settingName, roomId) { if (this._featureNames.includes(settingName)) { - return Promise.resolve(this._readFeature(settingName)); + return this._readFeature(settingName); } const value = localStorage.getItem(this._getKey(settingName)); - if (!value) return Promise.reject(); - return Promise.resolve(value); + if (!value) return null; + + return JSON.parse(value).value; } setValue(settingName, roomId, newValue) { @@ -51,6 +52,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { if (newValue === null) { localStorage.removeItem(this._getKey(settingName)); } else { + newValue = JSON.stringify({value: newValue}); localStorage.setItem(this._getKey(settingName), newValue); } @@ -79,9 +81,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { } const value = localStorage.getItem("mx_labs_feature_" + featureName); - const enabled = value === "true"; - - return {enabled}; + return value === "true"; } _writeFeature(featureName, enabled) { diff --git a/src/settings/RoomAccountSettingsHandler.js b/src/settings/RoomAccountSettingsHandler.js index 7157d86c34..3c775f3ff0 100644 --- a/src/settings/RoomAccountSettingsHandler.js +++ b/src/settings/RoomAccountSettingsHandler.js @@ -23,18 +23,13 @@ import MatrixClientPeg from '../MatrixClientPeg'; */ 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); + return this._getSettings(roomId)[settingName]; } setValue(settingName, roomId, newValue) { - return MatrixClientPeg.get().setRoomAccountData( - roomId, this._getEventType(settingName), newValue - ); + const content = this._getSettings(roomId); + content[settingName] = newValue; + return MatrixClientPeg.get().setRoomAccountData(roomId, "im.vector.web.settings", content); } canSetValue(settingName, roomId) { @@ -46,7 +41,12 @@ export default class RoomAccountSettingsHandler extends SettingsHandler { return !!MatrixClientPeg.get(); } - _getEventType(settingName) { - return "im.vector.setting." + settingName; + _getSettings(roomId) { + const room = MatrixClientPeg.get().getRoom(roomId); + if (!room) return {}; + + const event = room.getAccountData("im.vector.settings"); + if (!event || !event.getContent()) return {}; + return event.getContent(); } } \ No newline at end of file diff --git a/src/settings/RoomDeviceSettingsHandler.js b/src/settings/RoomDeviceSettingsHandler.js index fe477564f6..3ee83f2362 100644 --- a/src/settings/RoomDeviceSettingsHandler.js +++ b/src/settings/RoomDeviceSettingsHandler.js @@ -24,14 +24,15 @@ import SettingsHandler from "./SettingsHandler"; 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); + if (!value) return null; + return JSON.parse(value).value; } setValue(settingName, roomId, newValue) { if (newValue === null) { localStorage.removeItem(this._getKey(settingName, roomId)); } else { + newValue = JSON.stringify({value: newValue}); localStorage.setItem(this._getKey(settingName, roomId), newValue); } diff --git a/src/settings/RoomSettingsHandler.js b/src/settings/RoomSettingsHandler.js index dcd7a76e87..c41a510646 100644 --- a/src/settings/RoomSettingsHandler.js +++ b/src/settings/RoomSettingsHandler.js @@ -14,7 +14,6 @@ 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'; @@ -23,34 +22,33 @@ import MatrixClientPeg from '../MatrixClientPeg'; */ 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()); + return this._getSettings(roomId)[settingName]; } setValue(settingName, roomId, newValue) { - return MatrixClientPeg.get().sendStateEvent( - roomId, this._getEventType(settingName), newValue, "" - ); + const content = this._getSettings(roomId); + content[settingName] = newValue; + return MatrixClientPeg.get().sendStateEvent(roomId, "im.vector.web.settings", content, ""); } 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()); + return room.currentState.maySendStateEvent("im.vector.web.settings", cli.getUserId()); } isSupported() { return !!MatrixClientPeg.get(); } - _getEventType(settingName) { - return "im.vector.setting." + settingName; + _getSettings(roomId) { + const room = MatrixClientPeg.get().getRoom(roomId); + if (!room) return {}; + + const event = room.currentState.getStateEvents("im.vector.web.settings"); + if (!event || !event.getContent()) return {}; + return event.getContent(); } } \ No newline at end of file diff --git a/src/settings/SettingsHandler.js b/src/settings/SettingsHandler.js index 7387712367..20f09cb1f2 100644 --- a/src/settings/SettingsHandler.js +++ b/src/settings/SettingsHandler.js @@ -42,7 +42,7 @@ export default class SettingsHandler { * 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. + * @param {*} newValue The new value for the setting, may be null. * @return {Promise} Resolves when the setting has been saved. */ setValue(settingName, roomId, newValue) { diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index eea91345d8..330c2def05 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -137,7 +137,7 @@ export default class SettingsStore { return yield SettingsStore.getValue(settingName, roomId); })(); - return value.enabled; + return value; } /** @@ -145,7 +145,7 @@ export default class SettingsStore { * 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. + * @return {*} The value, or null if not found */ static getValue(settingName, roomId) { const levelOrder = [ @@ -154,36 +154,22 @@ export default class SettingsStore { 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}); + if (configValue === "enable") return true; + if (configValue === "disable") return 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++]]; - } + for (let level of levelOrder) { + let handler = handlers[level]; + if (!handler) continue; - // 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(); + if (!value) continue; + return value; + } + return null; } /** @@ -194,7 +180,7 @@ export default class SettingsStore { * @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. + * @param {*} 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) { From 7dda5e9196710d82bd64284a9f895a1d29c46d69 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 28 Oct 2017 19:53:12 -0600 Subject: [PATCH 06/51] Appease the linter round 1 Signed-off-by: Travis Ralston --- src/settings/AccountSettingsHandler.js | 2 +- src/settings/ConfigSettingsHandler.js | 2 +- src/settings/DefaultSettingsHandler.js | 2 +- src/settings/DeviceSettingsHandler.js | 2 +- src/settings/RoomAccountSettingsHandler.js | 3 +-- src/settings/RoomDeviceSettingsHandler.js | 2 +- src/settings/RoomSettingsHandler.js | 2 +- src/settings/SettingsHandler.js | 9 +++------ src/settings/SettingsStore.js | 17 ++++++----------- 9 files changed, 16 insertions(+), 25 deletions(-) diff --git a/src/settings/AccountSettingsHandler.js b/src/settings/AccountSettingsHandler.js index c505afe31d..db25cd0737 100644 --- a/src/settings/AccountSettingsHandler.js +++ b/src/settings/AccountSettingsHandler.js @@ -46,4 +46,4 @@ export default class AccountSettingHandler extends SettingsHandler { if (!event || !event.getContent()) return {}; return event.getContent(); } -} \ No newline at end of file +} diff --git a/src/settings/ConfigSettingsHandler.js b/src/settings/ConfigSettingsHandler.js index 5307a1dac1..5cd6d22411 100644 --- a/src/settings/ConfigSettingsHandler.js +++ b/src/settings/ConfigSettingsHandler.js @@ -40,4 +40,4 @@ export default class ConfigSettingsHandler extends SettingsHandler { 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 index 0a4b8d91d3..2c3a05a18a 100644 --- a/src/settings/DefaultSettingsHandler.js +++ b/src/settings/DefaultSettingsHandler.js @@ -46,4 +46,4 @@ export default class DefaultSettingsHandler extends SettingsHandler { isSupported() { return true; } -} \ No newline at end of file +} diff --git a/src/settings/DeviceSettingsHandler.js b/src/settings/DeviceSettingsHandler.js index dbb833c570..329634a810 100644 --- a/src/settings/DeviceSettingsHandler.js +++ b/src/settings/DeviceSettingsHandler.js @@ -87,4 +87,4 @@ export default class DeviceSettingsHandler extends SettingsHandler { _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 index 3c775f3ff0..e1edaffb90 100644 --- a/src/settings/RoomAccountSettingsHandler.js +++ b/src/settings/RoomAccountSettingsHandler.js @@ -14,7 +14,6 @@ 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'; @@ -49,4 +48,4 @@ export default class RoomAccountSettingsHandler extends SettingsHandler { if (!event || !event.getContent()) return {}; return event.getContent(); } -} \ No newline at end of file +} diff --git a/src/settings/RoomDeviceSettingsHandler.js b/src/settings/RoomDeviceSettingsHandler.js index 3ee83f2362..b61e266a4a 100644 --- a/src/settings/RoomDeviceSettingsHandler.js +++ b/src/settings/RoomDeviceSettingsHandler.js @@ -50,4 +50,4 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { _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 index c41a510646..91bcff384a 100644 --- a/src/settings/RoomSettingsHandler.js +++ b/src/settings/RoomSettingsHandler.js @@ -51,4 +51,4 @@ export default class RoomSettingsHandler extends SettingsHandler { if (!event || !event.getContent()) return {}; return event.getContent(); } -} \ No newline at end of file +} diff --git a/src/settings/SettingsHandler.js b/src/settings/SettingsHandler.js index 20f09cb1f2..49265feb9a 100644 --- a/src/settings/SettingsHandler.js +++ b/src/settings/SettingsHandler.js @@ -14,8 +14,6 @@ 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. @@ -27,8 +25,7 @@ export default class SettingsHandler { * 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. + * @returns {*} The setting value, or null if not found. */ getValue(settingName, roomId) { throw new Error("Operation not possible: getValue was not overridden"); @@ -43,7 +40,7 @@ export default class SettingsHandler { * @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 {*} newValue The new value for the setting, may be null. - * @return {Promise} Resolves when the setting has been saved. + * @returns {Promise} Resolves when the setting has been saved. */ setValue(settingName, roomId, newValue) { throw new Error("Operation not possible: setValue was not overridden"); @@ -67,4 +64,4 @@ export default class SettingsHandler { isSupported() { return false; } -} \ No newline at end of file +} diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 330c2def05..e73be42887 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -59,9 +59,9 @@ const SETTINGS = { }; // Convert the above into simpler formats for the handlers -let defaultSettings = {}; -let featureNames = []; -for (let key of Object.keys(SETTINGS)) { +const defaultSettings = {}; +const featureNames = []; +for (const key of Object.keys(SETTINGS)) { defaultSettings[key] = SETTINGS[key].defaults; if (SETTINGS[key].isFeature) featureNames.push(key); } @@ -132,12 +132,7 @@ export default class SettingsStore { 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; + return SettingsStore.getValue(settingName, roomId); } /** @@ -149,7 +144,7 @@ export default class SettingsStore { */ static getValue(settingName, roomId) { const levelOrder = [ - 'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default' + 'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default', ]; if (SettingsStore.isFeature(settingName)) { @@ -161,7 +156,7 @@ export default class SettingsStore { const handlers = SettingsStore._getHandlers(settingName); - for (let level of levelOrder) { + for (const level of levelOrder) { let handler = handlers[level]; if (!handler) continue; From bf815f4be961ba09a80fde1cac1e72a6735918b8 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sat, 28 Oct 2017 20:21:34 -0600 Subject: [PATCH 07/51] Support labs features Signed-off-by: Travis Ralston --- src/UserSettingsStore.js | 76 ---------------------- src/components/structures/UserSettings.js | 10 +-- src/components/views/elements/Flair.js | 4 +- src/components/views/rooms/RoomHeader.js | 4 +- src/components/views/rooms/RoomSettings.js | 3 +- src/settings/DeviceSettingsHandler.js | 3 +- src/settings/SettingsStore.js | 74 +++++++++++++++------ 7 files changed, 68 insertions(+), 106 deletions(-) diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index ce39939bc0..2d2045d15b 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -25,50 +25,7 @@ import SdkConfig from './SdkConfig'; * TODO: Make things use this. This is all WIP - see UserSettings.js for usage. */ -const FEATURES = [ - { - id: 'feature_groups', - name: _td("Communities"), - }, - { - id: 'feature_pinning', - name: _td("Message Pinning"), - }, -]; - export default { - getLabsFeatures() { - const featuresConfig = SdkConfig.get()['features'] || {}; - - // The old flag: honoured for backwards compatibility - const enableLabs = SdkConfig.get()['enableLabs']; - - let labsFeatures; - if (enableLabs) { - labsFeatures = FEATURES; - } else { - labsFeatures = FEATURES.filter((f) => { - const sdkConfigValue = featuresConfig[f.id]; - if (sdkConfigValue === 'labs') { - return true; - } - }); - } - return labsFeatures.map((f) => { - return f.id; - }); - }, - - translatedNameForFeature(featureId) { - const feature = FEATURES.filter((f) => { - return f.id === featureId; - })[0]; - - if (feature === undefined) return null; - - return _t(feature.name); - }, - loadProfileInfo: function() { const cli = MatrixClientPeg.get(); return cli.getProfileInfo(cli.credentials.userId); @@ -213,37 +170,4 @@ export default { // FIXME: handle errors localStorage.setItem('mx_local_settings', JSON.stringify(settings)); }, - - isFeatureEnabled: function(featureId: string): boolean { - const featuresConfig = SdkConfig.get()['features']; - - // The old flag: honoured for backwards compatibility - const enableLabs = SdkConfig.get()['enableLabs']; - - let sdkConfigValue = enableLabs ? 'labs' : 'disable'; - if (featuresConfig && featuresConfig[featureId] !== undefined) { - sdkConfigValue = featuresConfig[featureId]; - } - - if (sdkConfigValue === 'enable') { - return true; - } else if (sdkConfigValue === 'disable') { - return false; - } else if (sdkConfigValue === 'labs') { - if (!MatrixClientPeg.get().isGuest()) { - // Make it explicit that guests get the defaults (although they shouldn't - // have been able to ever toggle the flags anyway) - const userValue = localStorage.getItem(`mx_labs_feature_${featureId}`); - return userValue === 'true'; - } - return false; - } else { - console.warn(`Unknown features config for ${featureId}: ${sdkConfigValue}`); - return false; - } - }, - - setFeatureEnabled: function(featureId: string, enabled: boolean) { - localStorage.setItem(`mx_labs_feature_${featureId}`, enabled); - }, }; diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 68ea932f93..9c28f7a869 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -15,6 +15,8 @@ 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 SettingsStore from "../../settings/SettingsStore"; + const React = require('react'); const ReactDOM = require('react-dom'); const sdk = require('../../index'); @@ -934,11 +936,11 @@ module.exports = React.createClass({ _renderLabs: function() { const features = []; - UserSettingsStore.getLabsFeatures().forEach((featureId) => { + SettingsStore.getLabsFeatures().forEach((featureId) => { // TODO: this ought to be a separate component so that we don't need // to rebind the onChange each time we render const onChange = (e) => { - UserSettingsStore.setFeatureEnabled(featureId, e.target.checked); + SettingsStore.setFeatureEnabled(featureId, e.target.checked); this.forceUpdate(); }; @@ -948,10 +950,10 @@ module.exports = React.createClass({ type="checkbox" id={featureId} name={featureId} - defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)} + defaultChecked={SettingsStore.isFeatureEnabled(featureId)} onChange={onChange} /> - + ); }); diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index 69d9aa35b7..2e2c7f2595 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -19,9 +19,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import {MatrixClient} from 'matrix-js-sdk'; -import UserSettingsStore from '../../../UserSettingsStore'; import FlairStore from '../../../stores/FlairStore'; import dis from '../../../dispatcher'; +import SettingsStore from "../../../settings/SettingsStore"; class FlairAvatar extends React.Component { @@ -79,7 +79,7 @@ export default class Flair extends React.Component { componentWillMount() { this._unmounted = false; - if (UserSettingsStore.isFeatureEnabled('feature_groups') && FlairStore.groupSupport()) { + if (SettingsStore.isFeatureEnabled('feature_groups') && FlairStore.groupSupport()) { this._generateAvatars(); } this.context.matrixClient.on('RoomState.events', this.onRoomStateEvents); diff --git a/src/components/views/rooms/RoomHeader.js b/src/components/views/rooms/RoomHeader.js index 4dfbdb3644..fbfe7ebe18 100644 --- a/src/components/views/rooms/RoomHeader.js +++ b/src/components/views/rooms/RoomHeader.js @@ -31,7 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix'; import AccessibleButton from '../elements/AccessibleButton'; import ManageIntegsButton from '../elements/ManageIntegsButton'; import {CancelButton} from './SimpleRoomHeader'; -import UserSettingsStore from "../../../UserSettingsStore"; +import SettingsStore from "../../../settings/SettingsStore"; linkifyMatrix(linkify); @@ -304,7 +304,7 @@ module.exports = React.createClass({ ; } - if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) { + if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) { pinnedEventsButton = diff --git a/src/components/views/rooms/RoomSettings.js b/src/components/views/rooms/RoomSettings.js index dbdcdf596a..f582cc29ef 100644 --- a/src/components/views/rooms/RoomSettings.js +++ b/src/components/views/rooms/RoomSettings.js @@ -25,6 +25,7 @@ import ObjectUtils from '../../../ObjectUtils'; import dis from '../../../dispatcher'; import UserSettingsStore from '../../../UserSettingsStore'; import AccessibleButton from '../elements/AccessibleButton'; +import SettingsStore from "../../../settings/SettingsStore"; // parse a string as an integer; if the input is undefined, or cannot be parsed @@ -671,7 +672,7 @@ module.exports = React.createClass({ const self = this; let relatedGroupsSection; - if (UserSettingsStore.isFeatureEnabled('feature_groups')) { + if (SettingsStore.isFeatureEnabled('feature_groups')) { relatedGroupsSection = SettingsStore.isFeature(s)); + + const enableLabs = SdkConfig.get()["enableLabs"]; + if (enableLabs) return possibleFeatures; + + return possibleFeatures.filter((s) => SettingsStore._getFeatureState(s) === "labs"); + } + /** * Determines if a setting is also a feature. * @param {string} settingName The setting to look up. @@ -135,6 +159,16 @@ export default class SettingsStore { return SettingsStore.getValue(settingName, roomId); } + /** + * Sets a feature as enabled or disabled on the current device. + * @param {string} settingName The name of the setting. + * @param {boolean} value True to enable the feature, false otherwise. + * @returns {Promise} Resolves when the setting has been set. + */ + static setFeatureEnabled(settingName, value) { + return SettingsStore.setValue(settingName, null, "device", value); + } + /** * 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. @@ -228,7 +262,7 @@ export default class SettingsStore { if (!SETTINGS[settingName]) return {}; const handlers = {}; - for (let level of SETTINGS[settingName].supportedLevels) { + for (const level of SETTINGS[settingName].supportedLevels) { if (!LEVEL_HANDLERS[level]) throw new Error("Unexpected level " + level); handlers[level] = LEVEL_HANDLERS[level]; } @@ -246,7 +280,7 @@ export default class SettingsStore { } const allowedStates = ['enable', 'disable', 'labs']; - if (!allowedStates.contains(featureState)) { + if (!allowedStates.includes(featureState)) { console.warn("Feature state '" + featureState + "' is invalid for " + settingName); featureState = "disable"; // to prevent accidental features. } From ae10a11ac4c1b3c96be16f630800c389117a6e50 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Sun, 29 Oct 2017 01:43:52 -0600 Subject: [PATCH 08/51] Convert synced settings to granular settings Signed-off-by: Travis Ralston --- src/Unread.js | 4 +- src/UserSettingsStore.js | 17 --- src/autocomplete/EmojiProvider.js | 4 +- src/components/structures/LoggedInView.js | 4 +- src/components/structures/MessagePanel.js | 5 +- src/components/structures/RoomView.js | 8 +- src/components/structures/TimelinePanel.js | 9 +- src/components/structures/UserSettings.js | 116 +++++------------- src/components/views/messages/MImageBody.js | 8 +- src/components/views/messages/MVideoBody.js | 4 +- src/components/views/messages/TextualBody.js | 8 +- src/components/views/rooms/AuxPanel.js | 1 - src/components/views/rooms/MessageComposer.js | 8 +- .../views/rooms/MessageComposerInput.js | 12 +- src/components/views/rooms/RoomTile.js | 1 - src/components/views/voip/VideoView.js | 4 +- src/settings/RoomSettingsHandler.js | 3 +- src/settings/SettingsStore.js | 100 ++++++++++++++- src/shouldHideEvent.js | 13 +- .../structures/MessagePanel-test.js | 8 +- .../views/rooms/MessageComposerInput-test.js | 1 - 21 files changed, 177 insertions(+), 161 deletions(-) diff --git a/src/Unread.js b/src/Unread.js index 20e876ad88..383b5c2e5a 100644 --- a/src/Unread.js +++ b/src/Unread.js @@ -15,7 +15,6 @@ limitations under the License. */ const MatrixClientPeg = require('./MatrixClientPeg'); -import UserSettingsStore from './UserSettingsStore'; import shouldHideEvent from './shouldHideEvent'; const sdk = require('./index'); @@ -64,7 +63,6 @@ module.exports = { // we have and the read receipt. We could fetch more history to try & find out, // but currently we just guess. - const syncedSettings = UserSettingsStore.getSyncedSettings(); // Loop through messages, starting with the most recent... for (let i = room.timeline.length - 1; i >= 0; --i) { const ev = room.timeline[i]; @@ -74,7 +72,7 @@ module.exports = { // that counts and we can stop looking because the user's read // this and everything before. return false; - } else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) { + } else if (!shouldHideEvent(ev) && this.eventTriggersUnreadCount(ev)) { // We've found a message that counts before we hit // the read marker, so this room is definitely unread. return true; diff --git a/src/UserSettingsStore.js b/src/UserSettingsStore.js index 2d2045d15b..591eaa1f0f 100644 --- a/src/UserSettingsStore.js +++ b/src/UserSettingsStore.js @@ -137,23 +137,6 @@ export default { }); }, - getSyncedSettings: function() { - const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings'); - return event ? event.getContent() : {}; - }, - - getSyncedSetting: function(type, defaultValue = null) { - const settings = this.getSyncedSettings(); - return settings.hasOwnProperty(type) ? settings[type] : defaultValue; - }, - - setSyncedSetting: function(type, value) { - const settings = this.getSyncedSettings(); - settings[type] = value; - // FIXME: handle errors - return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings); - }, - getLocalSettings: function() { const localSettingsString = localStorage.getItem('mx_local_settings') || '{}'; return JSON.parse(localSettingsString); diff --git a/src/autocomplete/EmojiProvider.js b/src/autocomplete/EmojiProvider.js index a5b80e3b0e..b2ec73faca 100644 --- a/src/autocomplete/EmojiProvider.js +++ b/src/autocomplete/EmojiProvider.js @@ -25,7 +25,7 @@ import {PillCompletion} from './Components'; import type {SelectionRange, Completion} from './Autocompleter'; import _uniq from 'lodash/uniq'; import _sortBy from 'lodash/sortBy'; -import UserSettingsStore from '../UserSettingsStore'; +import SettingsStore from "../settings/SettingsStore"; import EmojiData from '../stripped-emoji.json'; @@ -97,7 +97,7 @@ export default class EmojiProvider extends AutocompleteProvider { } async getCompletions(query: string, selection: SelectionRange) { - if (UserSettingsStore.getSyncedSetting("MessageComposerInput.dontSuggestEmoji")) { + if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) { return []; // don't give any suggestions if the user doesn't want them } diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 5d1d47c5b2..31f59e4849 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -19,7 +19,6 @@ limitations under the License. import * as Matrix from 'matrix-js-sdk'; import React from 'react'; -import UserSettingsStore from '../../UserSettingsStore'; import KeyCode from '../../KeyCode'; import Notifier from '../../Notifier'; import PageTypes from '../../PageTypes'; @@ -28,6 +27,7 @@ import sdk from '../../index'; import dis from '../../dispatcher'; import sessionStore from '../../stores/SessionStore'; import MatrixClientPeg from '../../MatrixClientPeg'; +import SettingsStore from "../../settings/SettingsStore"; /** * This is what our MatrixChat shows when we are logged in. The precise view is @@ -74,7 +74,7 @@ export default React.createClass({ getInitialState: function() { return { // use compact timeline view - useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'), + useCompactLayout: SettingsStore.getValue('useCompactLayout'), }; }, diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index 2331e096c0..53cc660a9b 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -17,7 +17,6 @@ limitations under the License. import React from 'react'; import ReactDOM from 'react-dom'; import classNames from 'classnames'; -import UserSettingsStore from '../../UserSettingsStore'; import shouldHideEvent from '../../shouldHideEvent'; import dis from "../../dispatcher"; import sdk from '../../index'; @@ -110,8 +109,6 @@ module.exports = React.createClass({ // Velocity requires this._readMarkerGhostNode = null; - this._syncedSettings = UserSettingsStore.getSyncedSettings(); - this._isMounted = true; }, @@ -251,7 +248,7 @@ module.exports = React.createClass({ // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; - return !shouldHideEvent(mxEv, this._syncedSettings); + return !shouldHideEvent(mxEv); }, _getEventTiles: function() { diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 9b6dbb4c27..4256c19f4d 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -29,7 +29,6 @@ const classNames = require("classnames"); const Matrix = require("matrix-js-sdk"); import { _t } from '../../languageHandler'; -const UserSettingsStore = require('../../UserSettingsStore'); const MatrixClientPeg = require("../../MatrixClientPeg"); const ContentMessages = require("../../ContentMessages"); const Modal = require("../../Modal"); @@ -48,6 +47,7 @@ import UserProvider from '../../autocomplete/UserProvider'; import RoomViewStore from '../../stores/RoomViewStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; +import SettingsStore from "../../settings/SettingsStore"; const DEBUG = false; let debuglog = function() {}; @@ -151,8 +151,6 @@ module.exports = React.createClass({ MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().on("accountData", this.onAccountData); - this._syncedSettings = UserSettingsStore.getSyncedSettings(); - // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._onRoomViewStoreUpdate(true); @@ -535,7 +533,7 @@ module.exports = React.createClass({ // update unread count when scrolled up if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { // no change - } else if (!shouldHideEvent(ev, this._syncedSettings)) { + } else if (!shouldHideEvent(ev)) { this.setState((state, props) => { return {numUnreadMessages: state.numUnreadMessages + 1}; }); @@ -1778,7 +1776,7 @@ module.exports = React.createClass({ const messagePanel = (