Merge pull request #5124 from matrix-org/travis/settings-v3

Settings v3: Feature flag changes
This commit is contained in:
Travis Ralston 2020-08-19 12:00:48 -06:00 committed by GitHub
commit c9d98a1d19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 85 additions and 175 deletions

View file

@ -9,7 +9,7 @@ of dealing with the different levels and exposes easy to use getters and setters
## Levels ## Levels
Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in Granular Settings rely on a series of known levels in order to use the correct value for the scenario. These levels, in
order of prioirty, are: order of priority, are:
* `device` - The current user's device * `device` - The current user's device
* `room-device` - The current user's device, but only when in a specific room * `room-device` - The current user's device, but only when in a specific room
* `room-account` - The current user's account, but only when in a specific room * `room-account` - The current user's account, but only when in a specific room
@ -25,33 +25,10 @@ that room administrators cannot force account-only settings upon participants.
## Settings ## Settings
Settings are the different options a user may set or experience in the application. These are pre-defined in Settings are the different options a user may set or experience in the application. These are pre-defined in
`src/settings/Settings.js` under the `SETTINGS` constant and have the following minimum requirements: `src/settings/Settings.ts` under the `SETTINGS` constant, and match the `ISetting` interface as defined there.
```
// The ID is used to reference the setting throughout the application. This must be unique.
"theSettingId": {
// The levels this setting supports is required. In `src/settings/Settings.js` there are various pre-set arrays
// for this option - they should be used where possible to avoid copy/pasting arrays across settings.
supportedLevels: [...],
// The default for this setting serves two purposes: It provides a value if the setting is not defined at other Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some
// levels, and it serves to demonstrate the expected type to other developers. The value isn't enforced, but it settings, like the "theme" setting, are special cased in the config file):
// should be respected throughout the code. The default may be any data type.
default: false,
// The display name has two notations: string and object. The object notation allows for different translatable
// strings to be used for different levels, while the string notation represents the string for all levels.
displayName: _td("Change something"), // effectively `displayName: { "default": _td("Change something") }`
displayName: {
"room": _td("Change something for participants of this room"),
// Note: the default will be used if the level requested (such as `device`) does not have a string defined here.
"default": _td("Change something"),
}
}
```
Settings that support the config level can be set in the config file under the `settingDefaults` key (note that some settings, like the "theme" setting, are special cased in the config file):
```json ```json
{ {
... ...
@ -119,38 +96,29 @@ for you. If a display name cannot be found, it will return `null`.
## Features ## Features
Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are Feature flags are just like regular settings with some underlying semantics for how they are meant to be used. Usually
commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and a feature flag is used when a portion of the application is under development or not ready for full release yet, such
look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting as new functionality or experimental ideas. In these cases, the feature name *should* be named with the `feature_*`
definition and should go through the helper functions on `SettingsStore`. convention and must be tagged with `isFeature: true` in the setting definition. By doing so, the feature will automatically
appear in the "labs" section of the user's settings.
Although features have levels and a default value, the calculation of those options is blocked by the feature's state. Features can be controlled at the config level using the following structure:
A feature's state is determined from the `SdkConfig` and is a little complex. If `enableLabs` (a legacy flag) is `true`
then the feature's state is `labs`, if it is `false`, the state is `disable`. If `enableLabs` is not set then the state
is determined from the `features` config, such as in the following:
```json ```json
"features": { "features": {
"feature_lazyloading": "labs" "feature_lazyloading": true
} }
``` ```
In this example, `feature_lazyloading` is in the `labs` state. It may also be in the `enable` or `disable` state with a
similar approach. If the state is invalid, the feature is in the `disable` state. A feature's levels are only calculated
if it is in the `labs` state, therefore the default only applies in that scenario. If the state is `enable`, the feature
is always-on.
Once a feature flag has served its purpose, it is generally recommended to remove it and the associated feature flag When `true`, the user will see the feature as enabled. Similarly, when `false` the user will see the feature as disabled.
checks. This would enable the feature implicitly as it is part of the application now. The user will only be able to change/see these states if `showLabsSettings: true` is in the config.
### Determining if a feature is enabled ### Determining if a feature is enabled
A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the Call `SettingsStore.getValue()` as you would for any other setting.
required calculations to determine if the feature is enabled based upon the configuration and user selection.
### Enabling a feature ### Enabling a feature
Features can only be enabled if the feature is in the `labs` state, otherwise this is a no-op. To find the current set Call `SettingsStore.setValue("feature_name", null, SettingLevel.DEVICE, true)`.
of features in the `labs` state, call `SettingsStore.getLabsFeatures`. To set the value, call
`SettingsStore.setFeatureEnabled`.
## Setting controllers ## Setting controllers
@ -162,7 +130,7 @@ kept up to date with the setting where it is otherwise not possible. An example
they can only be considered enabled if the platform supports notifications, and enabling notifications requires they can only be considered enabled if the platform supports notifications, and enabling notifications requires
additional steps to actually enable notifications. additional steps to actually enable notifications.
For more information, see `src/settings/controllers/SettingController.js`. For more information, see `src/settings/controllers/SettingController.ts`.
## Local echo ## Local echo
@ -222,7 +190,7 @@ The `SettingsStore` uses the hardcoded `LEVEL_ORDER` constant to ensure that it
The array is checked from left to right, simulating the behaviour of overriding values from the higher levels. Each The array is checked from left to right, simulating the behaviour of overriding values from the higher levels. Each
level should be defined in this array, including `default`. level should be defined in this array, including `default`.
Handlers (`src/settings/handlers/SettingsHandler.js`) represent a single level and are responsible for getting and Handlers (`src/settings/handlers/SettingsHandler.ts`) represent a single level and are responsible for getting and
setting values at that level. Handlers also provide additional information to the `SettingsStore` such as if the level setting values at that level. Handlers also provide additional information to the `SettingsStore` such as if the level
is supported or if the current user may set values at the level. The `SettingsStore` will use the handler to enforce is supported or if the current user may set values at the level. The `SettingsStore` will use the handler to enforce
checks and manipulate settings. Handlers are also responsible for dealing with migration patterns or legacy settings for checks and manipulate settings. Handlers are also responsible for dealing with migration patterns or legacy settings for
@ -230,7 +198,7 @@ their level (for example, a setting being renamed or using a different key from
Handlers are provided to the `SettingsStore` via the `LEVEL_HANDLERS` constant. `SettingsStore` will optimize lookups by Handlers are provided to the `SettingsStore` via the `LEVEL_HANDLERS` constant. `SettingsStore` will optimize lookups by
only considering handlers that are supported on the platform. only considering handlers that are supported on the platform.
Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.js` which acts as a wrapper around a given Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.ts` which acts as a wrapper around a given
handler. This is automatically applied to all defined `LEVEL_HANDLERS` and proxies the calls to the wrapped handler handler. This is automatically applied to all defined `LEVEL_HANDLERS` and proxies the calls to the wrapped handler
where possible. The echo is achieved by a simple object cache stored within the class itself. The cache is invalidated where possible. The echo is achieved by a simple object cache stored within the class itself. The cache is invalidated
immediately upon the proxied save call succeeding or failing. immediately upon the proxied save call succeeding or failing.
@ -240,20 +208,7 @@ Controllers are notified of changes by the `SettingsStore`, and are given the op
### Features ### Features
Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enableLabs` is See above for feature reference.
false/not set. Features are always checked against the configuration before going through the level order as they have
the option of being forced-on or forced-off for the application. This is done by the `features` section and looks
something like this:
```
"features": {
"feature_groups": "enable",
"feature_pinning": "disable", // the default
"feature_presence": "labs"
}
```
If `enableLabs` is true in the configuration, the default for features becomes `"labs"`.
### Watchers ### Watchers

View file

@ -197,7 +197,7 @@ export default class FromWidgetPostMessageApi {
const integId = (data && data.integId) ? data.integId : null; const integId = (data && data.integId) ? data.integId : null;
// TODO: Open the right integration manager for the widget // TODO: Open the right integration manager for the widget
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll( IntegrationManagers.sharedInstance().openAll(
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()), MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
`type_${integType}`, `type_${integType}`,

View file

@ -378,7 +378,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const tagPanel = !this.state.showTagPanel ? null : ( const tagPanel = !this.state.showTagPanel ? null : (
<div className="mx_LeftPanel_tagPanelContainer"> <div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/> <TagPanel/>
{SettingsStore.isFeatureEnabled("feature_custom_tags") ? <CustomRoomTagPanel /> : null} {SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
</div> </div>
); );

View file

@ -53,7 +53,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) { if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) {
throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user"); throw new Error("Cannot use MemberStatusMessageAvatar on anyone but the logged in user");
} }
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { if (!SettingsStore.getValue("feature_custom_status")) {
return; return;
} }
const { user } = this.props.member; const { user } = this.props.member;
@ -105,7 +105,7 @@ export default class MemberStatusMessageAvatar extends React.Component {
resizeMethod={this.props.resizeMethod} resizeMethod={this.props.resizeMethod}
/>; />;
if (!SettingsStore.isFeatureEnabled("feature_custom_status")) { if (!SettingsStore.getValue("feature_custom_status")) {
return avatar; return avatar;
} }

View file

@ -81,7 +81,7 @@ export default createReactClass({
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli); let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality // HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
if (!SettingsStore.isFeatureEnabled("feature_pinning")) canPin = false; if (!SettingsStore.getValue("feature_pinning")) canPin = false;
this.setState({canRedact, canPin}); this.setState({canRedact, canPin});
}, },

View file

@ -87,7 +87,7 @@ export default class RoomSettingsDialog extends React.Component {
<NotificationSettingsTab roomId={this.props.roomId} />, <NotificationSettingsTab roomId={this.props.roomId} />,
)); ));
if (SettingsStore.isFeatureEnabled("feature_bridge_state")) { if (SettingsStore.getValue("feature_bridge_state")) {
tabs.push(new Tab( tabs.push(new Tab(
ROOM_BRIDGES_TAB, ROOM_BRIDGES_TAB,
_td("Bridges"), _td("Bridges"),

View file

@ -54,7 +54,7 @@ export default class UserSettingsDialog extends React.Component {
super(); super();
this.state = { this.state = {
mjolnirEnabled: SettingsStore.isFeatureEnabled("feature_mjolnir"), mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
}; };
} }
@ -116,7 +116,7 @@ export default class UserSettingsDialog extends React.Component {
"mx_UserSettingsDialog_securityIcon", "mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />, <SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
)); ));
if (SdkConfig.get()['showLabsSettings'] || SettingsStore.getLabsFeatures().length > 0) { if (SdkConfig.get()['showLabsSettings']) {
tabs.push(new Tab( tabs.push(new Tab(
USER_LABS_TAB, USER_LABS_TAB,
_td("Labs"), _td("Labs"),

View file

@ -311,7 +311,7 @@ export default class AppTile extends React.Component {
this.props.onEditClick(); this.props.onEditClick();
} else { } else {
// TODO: Open the right manager for the widget // TODO: Open the right manager for the widget
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll( IntegrationManagers.sharedInstance().openAll(
this.props.room, this.props.room,
'type_' + this.props.app.type, 'type_' + this.props.app.type,

View file

@ -28,7 +28,7 @@ export default createReactClass({
const imgClass = this.props.imgClassName || ""; const imgClass = this.props.imgClassName || "";
let imageSource; let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { if (SettingsStore.getValue('feature_new_spinner')) {
imageSource = require("../../../../res/img/spinner.svg"); imageSource = require("../../../../res/img/spinner.svg");
} else { } else {
imageSource = require("../../../../res/img/spinner.gif"); imageSource = require("../../../../res/img/spinner.gif");

View file

@ -34,7 +34,7 @@ export default class ManageIntegsButton extends React.Component {
if (!managers.hasManager()) { if (!managers.hasManager()) {
managers.openNoManagerDialog(); managers.openNoManagerDialog();
} else { } else {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { if (SettingsStore.getValue("feature_many_integration_managers")) {
managers.openAll(this.props.room); managers.openAll(this.props.room);
} else { } else {
managers.getPrimaryManager().open(this.props.room); managers.getPrimaryManager().open(this.props.room);

View file

@ -22,7 +22,7 @@ import SettingsStore from "../../../settings/SettingsStore";
const Spinner = ({w = 32, h = 32, imgClassName, message}) => { const Spinner = ({w = 32, h = 32, imgClassName, message}) => {
let imageSource; let imageSource;
if (SettingsStore.isFeatureEnabled('feature_new_spinner')) { if (SettingsStore.getValue('feature_new_spinner')) {
imageSource = require("../../../../res/img/spinner.svg"); imageSource = require("../../../../res/img/spinner.svg");
} else { } else {
imageSource = require("../../../../res/img/spinner.gif"); imageSource = require("../../../../res/img/spinner.gif");

View file

@ -95,7 +95,7 @@ export default createReactClass({
} }
} }
if (SettingsStore.isFeatureEnabled("feature_mjolnir")) { if (SettingsStore.getValue("feature_mjolnir")) {
const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`; const key = `mx_mjolnir_render_${this.props.mxEvent.getRoomId()}__${this.props.mxEvent.getId()}`;
const allowRender = localStorage.getItem(key) === "true"; const allowRender = localStorage.getItem(key) === "true";

View file

@ -1428,7 +1428,7 @@ const UserInfoHeader = ({onClose, member, e2eStatus}) => {
presenceLastActiveAgo = member.user.lastActiveAgo; presenceLastActiveAgo = member.user.lastActiveAgo;
presenceCurrentlyActive = member.user.currentlyActive; presenceCurrentlyActive = member.user.currentlyActive;
if (SettingsStore.isFeatureEnabled("feature_custom_status")) { if (SettingsStore.getValue("feature_custom_status")) {
statusMessage = member.user._unstable_statusMessage; statusMessage = member.user._unstable_statusMessage;
} }
} }

View file

@ -130,7 +130,7 @@ export default createReactClass({
}, },
_launchManageIntegrations: function() { _launchManageIntegrations: function() {
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll(); IntegrationManagers.sharedInstance().openAll();
} else { } else {
IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ'); IntegrationManagers.sharedInstance().getPrimaryManager().open(this.props.room, 'add_integ');

View file

@ -104,7 +104,7 @@ export default createReactClass({
}, },
_rateLimitedUpdate: new RateLimitedFunc(function() { _rateLimitedUpdate: new RateLimitedFunc(function() {
if (SettingsStore.isFeatureEnabled("feature_state_counters")) { if (SettingsStore.getValue("feature_state_counters")) {
this.setState({counters: this._computeCounters()}); this.setState({counters: this._computeCounters()});
} }
}, 500), }, 500),
@ -112,7 +112,7 @@ export default createReactClass({
_computeCounters: function() { _computeCounters: function() {
let counters = []; let counters = [];
if (this.props.room && SettingsStore.isFeatureEnabled("feature_state_counters")) { if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter'); const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
stateEvs.sort((a, b) => { stateEvs.sort((a, b) => {
return a.getStateKey() < b.getStateKey(); return a.getStateKey() < b.getStateKey();
@ -206,7 +206,7 @@ export default createReactClass({
/>; />;
let stateViews = null; let stateViews = null;
if (this.state.counters && SettingsStore.isFeatureEnabled("feature_state_counters")) { if (this.state.counters && SettingsStore.getValue("feature_state_counters")) {
let counters = []; let counters = [];
this.state.counters.forEach((counter, idx) => { this.state.counters.forEach((counter, idx) => {

View file

@ -50,7 +50,7 @@ export default createReactClass({
componentDidMount() { componentDidMount() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (SettingsStore.isFeatureEnabled("feature_custom_status")) { if (SettingsStore.getValue("feature_custom_status")) {
const { user } = this.props.member; const { user } = this.props.member;
if (user) { if (user) {
user.on("User._unstable_statusMessage", this._onStatusMessageCommitted); user.on("User._unstable_statusMessage", this._onStatusMessageCommitted);
@ -209,7 +209,7 @@ export default createReactClass({
const presenceState = member.user ? member.user.presence : null; const presenceState = member.user ? member.user.presence : null;
let statusMessage = null; let statusMessage = null;
if (member.user && SettingsStore.isFeatureEnabled("feature_custom_status")) { if (member.user && SettingsStore.getValue("feature_custom_status")) {
statusMessage = this.state.statusMessage; statusMessage = this.state.statusMessage;
} }

View file

@ -226,7 +226,7 @@ export default createReactClass({
title={_t("Settings")} />; title={_t("Settings")} />;
} }
if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) { if (this.props.onPinnedClick && SettingsStore.getValue('feature_pinning')) {
let pinsIndicator = null; let pinsIndicator = null;
if (this._hasUnreadPins()) { if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />); pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);

View file

@ -363,7 +363,7 @@ export default class Stickerpicker extends React.Component {
*/ */
_launchManageIntegrations() { _launchManageIntegrations() {
// TODO: Open the right integration manager for the widget // TODO: Open the right integration manager for the widget
if (SettingsStore.isFeatureEnabled("feature_many_integration_managers")) { if (SettingsStore.getValue("feature_many_integration_managers")) {
IntegrationManagers.sharedInstance().openAll( IntegrationManagers.sharedInstance().openAll(
this.props.room, this.props.room,
`type_${WidgetType.STICKERPICKER.preferred}`, `type_${WidgetType.STICKERPICKER.preferred}`,

View file

@ -237,7 +237,7 @@ export default class AppearanceUserSettingsTab extends React.Component<IProps, I
} }
let customThemeForm: JSX.Element; let customThemeForm: JSX.Element;
if (SettingsStore.isFeatureEnabled("feature_custom_themes")) { if (SettingsStore.getValue("feature_custom_themes")) {
let messageElement = null; let messageElement = null;
if (this.state.customThemeMessage.text) { if (this.state.customThemeMessage.text) {
if (this.state.customThemeMessage.isError) { if (this.state.customThemeMessage.isError) {

View file

@ -28,14 +28,15 @@ export class LabsSettingToggle extends React.Component {
}; };
_onChange = async (checked) => { _onChange = async (checked) => {
await SettingsStore.setFeatureEnabled(this.props.featureId, checked); await SettingsStore.setValue(this.props.featureId, null, SettingLevel.DEVICE, checked);
this.forceUpdate(); this.forceUpdate();
}; };
render() { render() {
const label = SettingsStore.getDisplayName(this.props.featureId); const label = SettingsStore.getDisplayName(this.props.featureId);
const value = SettingsStore.isFeatureEnabled(this.props.featureId); const value = SettingsStore.getValue(this.props.featureId);
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} />; const canChange = SettingsStore.canSetValue(this.props.featureId, null, SettingLevel.DEVICE);
return <LabelledToggleSwitch value={value} label={label} onChange={this._onChange} disabled={!canChange} />;
} }
} }
@ -46,7 +47,7 @@ export default class LabsUserSettingsTab extends React.Component {
render() { render() {
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag"); const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
const flags = SettingsStore.getLabsFeatures().map(f => <LabsSettingToggle featureId={f} key={f} />); const flags = SettingsStore.getFeatureSettingNames().map(f => <LabsSettingToggle featureId={f} key={f} />);
return ( return (
<div className="mx_SettingsTab"> <div className="mx_SettingsTab">
<div className="mx_SettingsTab_heading">{_t("Labs")}</div> <div className="mx_SettingsTab_heading">{_t("Labs")}</div>

View file

@ -36,11 +36,11 @@ export const useSettingValue = (settingName: string, roomId: string = null, excl
// Hook to fetch whether a feature is enabled and dynamically update when that changes // Hook to fetch whether a feature is enabled and dynamically update when that changes
export const useFeatureEnabled = (featureName: string, roomId: string = null) => { export const useFeatureEnabled = (featureName: string, roomId: string = null) => {
const [enabled, setEnabled] = useState(SettingsStore.isFeatureEnabled(featureName, roomId)); const [enabled, setEnabled] = useState(SettingsStore.getValue(featureName, roomId));
useEffect(() => { useEffect(() => {
const ref = SettingsStore.watchSetting(featureName, roomId, () => { const ref = SettingsStore.watchSetting(featureName, roomId, () => {
setEnabled(SettingsStore.isFeatureEnabled(featureName, roomId)); setEnabled(SettingsStore.getValue(featureName, roomId));
}); });
// clean-up // clean-up
return () => { return () => {

View file

@ -123,7 +123,7 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) {
} }
// add labs options // add labs options
const enabledLabs = SettingsStore.getLabsFeatures().filter(f => SettingsStore.isFeatureEnabled(f)); const enabledLabs = SettingsStore.getFeatureSettingNames().filter(f => SettingsStore.getValue(f));
if (enabledLabs.length) { if (enabledLabs.length) {
body.append('enabled_labs', enabledLabs.join(', ')); body.append('enabled_labs', enabledLabs.join(', '));
} }

View file

@ -23,7 +23,6 @@ import AccountSettingsHandler from "./handlers/AccountSettingsHandler";
import RoomSettingsHandler from "./handlers/RoomSettingsHandler"; import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler"; import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import SdkConfig from "../SdkConfig";
import dis from '../dispatcher/dispatcher'; import dis from '../dispatcher/dispatcher';
import { ISetting, SETTINGS } from "./Settings"; import { ISetting, SETTINGS } from "./Settings";
import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
@ -53,7 +52,7 @@ const LEVEL_HANDLERS = {
[SettingLevel.ROOM_ACCOUNT]: new RoomAccountSettingsHandler(defaultWatchManager), [SettingLevel.ROOM_ACCOUNT]: new RoomAccountSettingsHandler(defaultWatchManager),
[SettingLevel.ACCOUNT]: new AccountSettingsHandler(defaultWatchManager), [SettingLevel.ACCOUNT]: new AccountSettingsHandler(defaultWatchManager),
[SettingLevel.ROOM]: new RoomSettingsHandler(defaultWatchManager), [SettingLevel.ROOM]: new RoomSettingsHandler(defaultWatchManager),
[SettingLevel.CONFIG]: new ConfigSettingsHandler(), [SettingLevel.CONFIG]: new ConfigSettingsHandler(featureNames),
[SettingLevel.DEFAULT]: new DefaultSettingsHandler(defaultSettings, invertedDefaultSettings), [SettingLevel.DEFAULT]: new DefaultSettingsHandler(defaultSettings, invertedDefaultSettings),
}; };
@ -124,6 +123,14 @@ export default class SettingsStore {
// Counter used for generation of watcher IDs // Counter used for generation of watcher IDs
private static watcherCount = 1; private static watcherCount = 1;
/**
* Gets all the feature-style setting names.
* @returns {string[]} The names of the feature settings.
*/
public static getFeatureSettingNames(): string[] {
return Object.keys(SETTINGS).filter(n => SettingsStore.isFeature(n));
}
/** /**
* Watches for changes in a particular setting. This is done without any local echo * Watches for changes in a particular setting. This is done without any local echo
* wrapping and fires whenever a change is detected in a setting's value, at any level. * wrapping and fires whenever a change is detected in a setting's value, at any level.
@ -240,19 +247,6 @@ export default class SettingsStore {
return _t(displayName as string); return _t(displayName as string);
} }
/**
* Returns a list of all available labs feature names
* @returns {string[]} The list of available feature names
*/
public static getLabsFeatures(): string[] {
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");
}
/** /**
* Determines if a setting is also a feature. * Determines if a setting is also a feature.
* @param {string} settingName The setting to look up. * @param {string} settingName The setting to look up.
@ -263,39 +257,6 @@ export default class SettingsStore {
return SETTINGS[settingName].isFeature; 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
*/
public static isFeatureEnabled(settingName: string, roomId: string = null) {
if (!SettingsStore.isFeature(settingName)) {
throw new Error("Setting " + settingName + " is not a feature");
}
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.
*/
public static setFeatureEnabled(settingName: string, value: any): Promise<void> {
// Verify that the setting is actually a setting
if (!SETTINGS[settingName]) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
}
if (!SettingsStore.isFeature(settingName)) {
throw new Error("Setting " + settingName + " is not a feature");
}
return SettingsStore.setValue(settingName, null, SettingLevel.DEVICE, value);
}
/** /**
* Gets the value of a setting. The room ID is optional if the setting is not to * 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. * be applied to any particular room, otherwise it should be supplied.
@ -346,13 +307,6 @@ export default class SettingsStore {
const minIndex = levelOrder.indexOf(level); const minIndex = levelOrder.indexOf(level);
if (minIndex === -1) throw new Error("Level " + level + " is not prioritized"); if (minIndex === -1) throw new Error("Level " + level + " is not prioritized");
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 handlers = SettingsStore.getHandlers(settingName); const handlers = SettingsStore.getHandlers(settingName);
// Check if we need to invert the setting at all. Do this after we get the setting // Check if we need to invert the setting at all. Do this after we get the setting
@ -480,6 +434,12 @@ export default class SettingsStore {
throw new Error("Setting '" + settingName + "' does not appear to be a setting."); throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
} }
// When features are specified in the config.json, we force them as enabled or disabled.
if (SettingsStore.isFeature(settingName)) {
const configVal = SettingsStore.getValueAt(SettingLevel.CONFIG, settingName, roomId, true, true);
if (configVal === true || configVal === false) return false;
}
const handler = SettingsStore.getHandler(settingName, level); const handler = SettingsStore.getHandler(settingName, level);
if (!handler) return false; if (!handler) return false;
return handler.canSetValue(settingName, roomId); return handler.canSetValue(settingName, roomId);
@ -611,24 +571,6 @@ export default class SettingsStore {
return handlers; return handlers;
} }
private static getFeatureState(settingName: string): LabsFeatureState {
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.includes(featureState)) {
console.warn("Feature state '" + featureState + "' is invalid for " + settingName);
featureState = "disable"; // to prevent accidental features.
}
return featureState;
}
} }
// For debugging purposes // For debugging purposes

View file

@ -24,9 +24,24 @@ import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
* roomId parameter. * roomId parameter.
*/ */
export default class ConfigSettingsHandler extends SettingsHandler { export default class ConfigSettingsHandler extends SettingsHandler {
public constructor(private featureNames: string[]) {
super();
}
public getValue(settingName: string, roomId: string): any { public getValue(settingName: string, roomId: string): any {
const config = SdkConfig.get() || {}; const config = SdkConfig.get() || {};
if (this.featureNames.includes(settingName)) {
const labsConfig = config["features"] || {};
const val = labsConfig[settingName];
if (isNullOrUndefined(val)) return null; // no definition at this level
if (val === true || val === false) return val; // new style: mapped as a boolean
if (val === "enable") return true; // backwards compat
if (val === "disable") return false; // backwards compat
if (val === "labs") return null; // backwards compat, no override
return null; // fallback in the case of invalid input
}
// Special case themes // Special case themes
if (settingName === "theme") { if (settingName === "theme") {
return config["default_theme"]; return config["default_theme"];

View file

@ -137,7 +137,7 @@ class CustomRoomTagStore extends EventEmitter {
} }
_getUpdatedTags() { _getUpdatedTags() {
if (!SettingsStore.isFeatureEnabled("feature_custom_tags")) { if (!SettingsStore.getValue("feature_custom_tags")) {
return {}; // none return {}; // none
} }

View file

@ -136,7 +136,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
} }
private async readAndCacheSettingsFromStore() { private async readAndCacheSettingsFromStore() {
const tagsEnabled = SettingsStore.isFeatureEnabled("feature_custom_tags"); const tagsEnabled = SettingsStore.getValue("feature_custom_tags");
await this.updateState({ await this.updateState({
tagsEnabled, tagsEnabled,
}); });

View file

@ -24,8 +24,8 @@ import DMRoomMap from "../../../utils/DMRoomMap";
export class ReactionEventPreview implements IPreview { export class ReactionEventPreview implements IPreview {
public getTextFor(event: MatrixEvent, tagId?: TagID): string { public getTextFor(event: MatrixEvent, tagId?: TagID): string {
const showDms = SettingsStore.isFeatureEnabled("feature_roomlist_preview_reactions_dms"); const showDms = SettingsStore.getValue("feature_roomlist_preview_reactions_dms");
const showAll = SettingsStore.isFeatureEnabled("feature_roomlist_preview_reactions_all"); const showAll = SettingsStore.getValue("feature_roomlist_preview_reactions_all");
if (!showAll && (!showDms || DMRoomMap.shared().getUserIdForRoomId(event.getRoomId()))) return null; if (!showAll && (!showDms || DMRoomMap.shared().getUserIdForRoomId(event.getRoomId()))) return null;

View file

@ -9,10 +9,7 @@
"integrations_ui_url": "https://scalar.vector.im/", "integrations_ui_url": "https://scalar.vector.im/",
"integrations_rest_url": "https://scalar.vector.im/api", "integrations_rest_url": "https://scalar.vector.im/api",
"bug_report_endpoint_url": "https://riot.im/bugreports/submit", "bug_report_endpoint_url": "https://riot.im/bugreports/submit",
"features": { "showLabsSettings": true,
"feature_groups": "labs",
"feature_pinning": "labs"
},
"default_federate": true, "default_federate": true,
"welcomePageUrl": "home.html", "welcomePageUrl": "home.html",
"default_theme": "light", "default_theme": "light",