mirror of https://github.com/vector-im/riot-web
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 <travpc@gmail.com>pull/21833/head
parent
580d8dce19
commit
8c3e5ebbad
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue