2017-10-29 01:13:06 +00:00
|
|
|
/*
|
|
|
|
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 = {
|
2017-10-29 02:21:34 +00:00
|
|
|
// EXAMPLE SETTING:
|
|
|
|
// "my-setting": {
|
2017-10-29 07:43:52 +00:00
|
|
|
// // Required by features, optional otherwise
|
|
|
|
// isFeature: false,
|
2017-10-29 02:21:34 +00:00
|
|
|
// displayName: _td("Cool Name"),
|
2017-10-29 07:43:52 +00:00
|
|
|
//
|
|
|
|
// // Required.
|
2017-10-29 02:21:34 +00:00
|
|
|
// 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.
|
|
|
|
// ],
|
2017-10-29 07:43:52 +00:00
|
|
|
//
|
|
|
|
// // Optional. Any data type.
|
2017-10-29 02:21:34 +00:00
|
|
|
// default: {
|
|
|
|
// your: "value",
|
|
|
|
// },
|
|
|
|
// },
|
|
|
|
"feature_groups": {
|
|
|
|
isFeature: true,
|
|
|
|
displayName: _td("Communities"),
|
|
|
|
supportedLevels: LEVELS_PRESET_FEATURE,
|
|
|
|
},
|
|
|
|
"feature_pinning": {
|
|
|
|
isFeature: true,
|
|
|
|
displayName: _td("Message Pinning"),
|
|
|
|
supportedLevels: LEVELS_PRESET_FEATURE,
|
2017-10-29 01:13:06 +00:00
|
|
|
},
|
2017-10-29 07:43:52 +00:00
|
|
|
"MessageComposerInput.dontSuggestEmoji": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Disable Emoji suggestions while typing'),
|
|
|
|
},
|
|
|
|
"useCompactLayout": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Use compact timeline layout'),
|
|
|
|
},
|
|
|
|
"hideRedactions": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ROOM.concat("room"),
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Hide removed messages'),
|
|
|
|
},
|
|
|
|
"hideJoinLeaves": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ROOM.concat("room"),
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
|
|
|
|
},
|
|
|
|
"hideAvatarDisplaynameChanges": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ROOM.concat("room"),
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Hide avatar and display name changes'),
|
|
|
|
},
|
|
|
|
"hideReadReceipts": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ROOM,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Hide read receipts'),
|
|
|
|
},
|
|
|
|
"showTwelveHourTimestamps": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
|
|
|
|
},
|
|
|
|
"alwaysShowTimestamps": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Always show message timestamps'),
|
|
|
|
},
|
|
|
|
"autoplayGifsAndVideos": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Autoplay GIFs and videos'),
|
|
|
|
},
|
|
|
|
"enableSyntaxHighlightLanguageDetection": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Enable automatic language detection for syntax highlighting'),
|
|
|
|
},
|
|
|
|
"Pill.shouldHidePillAvatar": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Hide avatars in user and room mentions'),
|
|
|
|
},
|
|
|
|
"TextualBody.disableBigEmoji": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Disable big emoji in chat'),
|
|
|
|
},
|
|
|
|
"MessageComposerInput.isRichTextEnabled": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
"MessageComposer.showFormatting": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
},
|
|
|
|
"dontSendTypingNotifications": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td("Don't send typing notifications"),
|
|
|
|
},
|
|
|
|
"MessageComposerInput.autoReplaceEmoji": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Automatically replace plain text Emoji'),
|
|
|
|
},
|
|
|
|
"VideoView.flipVideoHorizontally": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Mirror local video feed'),
|
|
|
|
},
|
|
|
|
"theme": {
|
|
|
|
supportedLevels: LEVELS_PRESET_ACCOUNT,
|
|
|
|
default: "light",
|
|
|
|
},
|
2017-10-29 22:53:00 +00:00
|
|
|
"webRtcForceTURN": {
|
|
|
|
supportedLevels: ['device'],
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Disable Peer-to-Peer for 1:1 calls'),
|
|
|
|
},
|
|
|
|
"webrtc_audioinput": {
|
|
|
|
supportedLevels: ['device'],
|
|
|
|
},
|
|
|
|
"webrtc_videoinput": {
|
|
|
|
supportedLevels: ['device'],
|
|
|
|
},
|
|
|
|
"language": {
|
|
|
|
supportedLevels: ['device'],
|
|
|
|
default: "en"
|
|
|
|
},
|
|
|
|
"analyticsOptOut": {
|
|
|
|
supportedLevels: ['device'],
|
|
|
|
default: false,
|
|
|
|
displayName: _td('Opt out of analytics'),
|
|
|
|
},
|
|
|
|
"autocompleteDelay": {
|
|
|
|
supportedLevels: ['device'],
|
|
|
|
default: 200,
|
|
|
|
},
|
|
|
|
"blacklistUnverifiedDevicesPerRoom": {
|
|
|
|
// TODO: {Travis} Write a migration path to support blacklistUnverifiedDevices
|
|
|
|
supportedLevels: ['device'],
|
|
|
|
default: {},
|
|
|
|
},
|
|
|
|
"blacklistUnverifiedDevices": {
|
|
|
|
// TODO: {Travis} Write a migration path to support blacklistUnverifiedDevices
|
|
|
|
supportedLevels: ['device'],
|
|
|
|
default: false,
|
|
|
|
label: _td('Never send encrypted messages to unverified devices from this device'),
|
|
|
|
}
|
2017-10-29 01:13:06 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Convert the above into simpler formats for the handlers
|
2017-10-29 01:53:12 +00:00
|
|
|
const defaultSettings = {};
|
|
|
|
const featureNames = [];
|
|
|
|
for (const key of Object.keys(SETTINGS)) {
|
2017-10-29 02:21:34 +00:00
|
|
|
defaultSettings[key] = SETTINGS[key].default;
|
2017-10-29 01:13:06 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2017-10-29 02:21:34 +00:00
|
|
|
/**
|
|
|
|
* Returns a list of all available labs feature names
|
|
|
|
* @returns {string[]} The list of available feature names
|
|
|
|
*/
|
|
|
|
static getLabsFeatures() {
|
|
|
|
const possibleFeatures = Object.keys(SETTINGS).filter((s) => SettingsStore.isFeature(s));
|
|
|
|
|
|
|
|
const enableLabs = SdkConfig.get()["enableLabs"];
|
|
|
|
if (enableLabs) return possibleFeatures;
|
|
|
|
|
|
|
|
return possibleFeatures.filter((s) => SettingsStore._getFeatureState(s) === "labs");
|
|
|
|
}
|
|
|
|
|
2017-10-29 01:13:06 +00:00
|
|
|
/**
|
|
|
|
* 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");
|
|
|
|
}
|
|
|
|
|
2017-10-29 01:53:12 +00:00
|
|
|
return SettingsStore.getValue(settingName, roomId);
|
2017-10-29 01:13:06 +00:00
|
|
|
}
|
|
|
|
|
2017-10-29 02:21:34 +00:00
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
2017-10-29 01:13:06 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
2017-10-29 01:45:48 +00:00
|
|
|
* @return {*} The value, or null if not found
|
2017-10-29 01:13:06 +00:00
|
|
|
*/
|
2017-10-29 07:43:52 +00:00
|
|
|
static getValue(settingName, roomId = null) {
|
2017-10-29 01:13:06 +00:00
|
|
|
const levelOrder = [
|
2017-10-29 01:53:12 +00:00
|
|
|
'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default',
|
2017-10-29 01:13:06 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
if (SettingsStore.isFeature(settingName)) {
|
|
|
|
const configValue = SettingsStore._getFeatureState(settingName);
|
2017-10-29 01:45:48 +00:00
|
|
|
if (configValue === "enable") return true;
|
|
|
|
if (configValue === "disable") return false;
|
2017-10-29 01:13:06 +00:00
|
|
|
// else let it fall through the default process
|
|
|
|
}
|
|
|
|
|
|
|
|
const handlers = SettingsStore._getHandlers(settingName);
|
|
|
|
|
2017-10-29 01:53:12 +00:00
|
|
|
for (const level of levelOrder) {
|
2017-10-29 01:45:48 +00:00
|
|
|
let handler = handlers[level];
|
|
|
|
if (!handler) continue;
|
2017-10-29 01:13:06 +00:00
|
|
|
|
2017-10-29 01:45:48 +00:00
|
|
|
const value = handler.getValue(settingName, roomId);
|
2017-10-29 07:43:52 +00:00
|
|
|
if (value === null || value === undefined) continue;
|
2017-10-29 01:45:48 +00:00
|
|
|
return value;
|
|
|
|
}
|
|
|
|
return null;
|
2017-10-29 01:13:06 +00:00
|
|
|
}
|
|
|
|
|
2017-10-29 22:02:51 +00:00
|
|
|
/**
|
|
|
|
* Gets a setting's value at the given level.
|
|
|
|
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The lvel to
|
|
|
|
* look at.
|
|
|
|
* @param {string} settingName The name of the setting to read.
|
|
|
|
* @param {String} roomId The room ID to read the setting value in, may be null.
|
|
|
|
* @return {*} The value, or null if not found.
|
|
|
|
*/
|
|
|
|
static getValueAt(level, settingName, roomId=null) {
|
|
|
|
// We specifically handle features as they have the possibility of being forced on.
|
|
|
|
if (SettingsStore.isFeature(settingName)) {
|
|
|
|
const configValue = SettingsStore._getFeatureState(settingName);
|
|
|
|
if (configValue === "enable") return true;
|
|
|
|
if (configValue === "disable") return false;
|
|
|
|
// else let it fall through the default process
|
|
|
|
}
|
|
|
|
|
|
|
|
const handler = SettingsStore._getHandler(settingName, level);
|
|
|
|
if (!handler) return null;
|
|
|
|
return handler.getValue(settingName, roomId);
|
|
|
|
}
|
|
|
|
|
2017-10-29 01:13:06 +00:00
|
|
|
/**
|
|
|
|
* 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.
|
2017-10-29 01:45:48 +00:00
|
|
|
* @param {*} value The new value of the setting, may be null.
|
2017-10-29 01:13:06 +00:00
|
|
|
* @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 = {};
|
2017-10-29 02:21:34 +00:00
|
|
|
for (const level of SETTINGS[settingName].supportedLevels) {
|
2017-10-29 01:13:06 +00:00
|
|
|
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'];
|
2017-10-29 02:21:34 +00:00
|
|
|
if (!allowedStates.includes(featureState)) {
|
2017-10-29 01:13:06 +00:00
|
|
|
console.warn("Feature state '" + featureState + "' is invalid for " + settingName);
|
|
|
|
featureState = "disable"; // to prevent accidental features.
|
|
|
|
}
|
|
|
|
|
|
|
|
return featureState;
|
|
|
|
}
|
|
|
|
}
|