Merge pull request #5124 from matrix-org/travis/settings-v3
Settings v3: Feature flag changes
This commit is contained in:
commit
c9d98a1d19
28 changed files with 85 additions and 175 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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}`,
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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"),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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" />);
|
||||||
|
|
|
@ -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}`,
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 () => {
|
||||||
|
|
|
@ -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(', '));
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"];
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue