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