Make the settings documentation fit within 120 characters per line
This commit is contained in:
parent
93673eff12
commit
fbffd3e97e
1 changed files with 101 additions and 31 deletions
132
docs/settings.md
132
docs/settings.md
|
@ -1,11 +1,15 @@
|
||||||
# Settings Reference
|
# Settings Reference
|
||||||
|
|
||||||
This document serves as developer documentation for using "Granular Settings". Granular Settings allow users to specify different values for a setting at particular levels of interest. For example, a user may say that in a particular room they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity of dealing with the different levels and exposes easy to use getters and setters.
|
This document serves as developer documentation for using "Granular Settings". Granular Settings allow users to specify
|
||||||
|
different values for a setting at particular levels of interest. For example, a user may say that in a particular room
|
||||||
|
they want URL previews off, but in all other rooms they want them enabled. The `SettingsStore` helps mask the complexity
|
||||||
|
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 order of prioirty, are:
|
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:
|
||||||
* `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
|
||||||
|
@ -14,12 +18,14 @@ Granular Settings rely on a series of known levels in order to use the correct v
|
||||||
* `config` - Values are defined by `config.json`
|
* `config` - Values are defined by `config.json`
|
||||||
* `default` - The hardcoded default for the settings
|
* `default` - The hardcoded default for the settings
|
||||||
|
|
||||||
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure that room administrators cannot force account-only settings upon participants.
|
Individual settings may control which levels are appropriate for them as part of the defaults. This is often to ensure
|
||||||
|
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 `src/settings/Settings.js` under the `SETTINGS` constant and have the following minimum requirements:
|
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:
|
||||||
```
|
```
|
||||||
// The ID is used to reference the setting throughout the application. This must be unique.
|
// The ID is used to reference the setting throughout the application. This must be unique.
|
||||||
"theSettingId": {
|
"theSettingId": {
|
||||||
|
@ -47,13 +53,21 @@ Settings are the different options a user may set or experience in the applicati
|
||||||
|
|
||||||
### Getting values for a setting
|
### Getting values for a setting
|
||||||
|
|
||||||
After importing `SettingsStore`, simply make a call to `SettingsStore.getValue`. The `roomId` parameter should always be supplied where possible, even if the setting does not have a per-room level value. This is to ensure that the value returned is best represented in the room, particularly if the setting ever gets a per-room level in the future.
|
After importing `SettingsStore`, simply make a call to `SettingsStore.getValue`. The `roomId` parameter should always
|
||||||
|
be supplied where possible, even if the setting does not have a per-room level value. This is to ensure that the value
|
||||||
|
returned is best represented in the room, particularly if the setting ever gets a per-room level in the future.
|
||||||
|
|
||||||
In settings pages it is often desired to have the value at a particular level instead of getting the calculated value. Call `SettingsStore.getValueAt` to get the value of a setting at a particular level, and optionally make it explicitly at that level. By default `getValueAt` will traverse the tree starting at the provided level; making it explicit means it will not go beyond the provided level. When using `getValueAt`, please be sure to use `SettingLevel` to represent the target level.
|
In settings pages it is often desired to have the value at a particular level instead of getting the calculated value.
|
||||||
|
Call `SettingsStore.getValueAt` to get the value of a setting at a particular level, and optionally make it explicitly
|
||||||
|
at that level. By default `getValueAt` will traverse the tree starting at the provided level; making it explicit means
|
||||||
|
it will not go beyond the provided level. When using `getValueAt`, please be sure to use `SettingLevel` to represent the
|
||||||
|
target level.
|
||||||
|
|
||||||
### Setting values for a setting
|
### Setting values for a setting
|
||||||
|
|
||||||
Values are defined at particular levels and should be done in a safe manner. There are two checks to perform to ensure a clean save: is the level supported and can the user actually set the value. In most cases, neither should be an issue although there are circumstances where this changes. An example of a safe call is:
|
Values are defined at particular levels and should be done in a safe manner. There are two checks to perform to ensure a
|
||||||
|
clean save: is the level supported and can the user actually set the value. In most cases, neither should be an issue
|
||||||
|
although there are circumstances where this changes. An example of a safe call is:
|
||||||
```javascript
|
```javascript
|
||||||
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
|
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
|
||||||
if (isSupported) {
|
if (isSupported) {
|
||||||
|
@ -64,11 +78,13 @@ if (isSupported) {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
These checks may also be performed in different areas of the application to avoid the verbose example above. For instance, the component which allows changing the setting may be hidden conditionally on the above conditions.
|
These checks may also be performed in different areas of the application to avoid the verbose example above. For
|
||||||
|
instance, the component which allows changing the setting may be hidden conditionally on the above conditions.
|
||||||
|
|
||||||
##### `SettingsFlag` component
|
##### `SettingsFlag` component
|
||||||
|
|
||||||
Where possible, the `SettingsFlag` component should be used to set simple "flip-a-bit" (true/false) settings. The `SettingsFlag` also supports simple radio button options, such as the theme the user would like to use.
|
Where possible, the `SettingsFlag` component should be used to set simple "flip-a-bit" (true/false) settings. The
|
||||||
|
`SettingsFlag` also supports simple radio button options, such as the theme the user would like to use.
|
||||||
```html
|
```html
|
||||||
<SettingsFlag name="theSettingId"
|
<SettingsFlag name="theSettingId"
|
||||||
level={SettingsLevel.ROOM}
|
level={SettingsLevel.ROOM}
|
||||||
|
@ -86,42 +102,65 @@ Where possible, the `SettingsFlag` component should be used to set simple "flip-
|
||||||
|
|
||||||
### Getting the display name for a setting
|
### Getting the display name for a setting
|
||||||
|
|
||||||
Simply call `SettingsStore.getDisplayName`. The appropriate display name will be returned and automatically translated for you. If a display name cannot be found, it will return `null`.
|
Simply call `SettingsStore.getDisplayName`. The appropriate display name will be returned and automatically translated
|
||||||
|
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 commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting definition and should go through the helper functions on `SettingsStore`.
|
Occasionally some parts of the application may be undergoing testing and are not quite production ready. These are
|
||||||
|
commonly known to be behind a "labs flag". Features behind lab flags must go through the granular settings system, and
|
||||||
|
look and act very much normal settings. The exception is that they must supply `isFeature: true` as part of the setting
|
||||||
|
definition and should go through the helper functions on `SettingsStore`.
|
||||||
|
|
||||||
Although features have levels and a default value, the calculation of those options is blocked by the feature's state. 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:
|
Although features have levels and a default value, the calculation of those options is blocked by the feature's state.
|
||||||
|
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": "labs"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
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.
|
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 checks. This would enable the feature implicitly as it is part of the application now.
|
Once a feature flag has served its purpose, it is generally recommended to remove it and the associated feature flag
|
||||||
|
checks. This would enable the feature implicitly as it is part of the application now.
|
||||||
|
|
||||||
### 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 required calculations to determine if the feature is enabled based upon the configuration and user selection.
|
A simple call to `SettingsStore.isFeatureEnabled` will tell you if the feature is enabled. This will perform all the
|
||||||
|
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 of features in the `labs` state, call `SettingsStore.getLabsFeatures`. To set the value, call `SettingsStore.setFeatureEnabled`.
|
Features can only be enabled if the feature is in the `labs` state, otherwise this is a no-op. To find the current set
|
||||||
|
of features in the `labs` state, call `SettingsStore.getLabsFeatures`. To set the value, call
|
||||||
|
`SettingsStore.setFeatureEnabled`.
|
||||||
|
|
||||||
|
|
||||||
## Setting controllers
|
## Setting controllers
|
||||||
|
|
||||||
Settings may have environmental factors that affect their value or need additional code to be called when they are modified. A setting controller is able to override the calculated value for a setting and react to changes in that setting. Controllers are not a replacement for the level handlers and should only be used to ensure the environment is kept up to date with the setting where it is otherwise not possible. An example of this is the notification settings: they can only be considered enabled if the platform supports notifications, and enabling notifications requires additional steps to actually enable notifications.
|
Settings may have environmental factors that affect their value or need additional code to be called when they are
|
||||||
|
modified. A setting controller is able to override the calculated value for a setting and react to changes in that
|
||||||
|
setting. Controllers are not a replacement for the level handlers and should only be used to ensure the environment is
|
||||||
|
kept up to date with the setting where it is otherwise not possible. An example of this is the notification settings:
|
||||||
|
they can only be considered enabled if the platform supports notifications, and enabling notifications requires
|
||||||
|
additional steps to actually enable notifications.
|
||||||
|
|
||||||
For more information, see `src/settings/controllers/SettingController.js`.
|
For more information, see `src/settings/controllers/SettingController.js`.
|
||||||
|
|
||||||
|
|
||||||
## Local echo
|
## Local echo
|
||||||
|
|
||||||
`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a split-brain scenario. As mentioned in the "Setting values for a setting" section, the appropriate checks should be done to ensure that the user is allowed to set the value. The local echo system assumes that the user has permission and that the request will go through successfully. The local echo only takes effect until the request to save a setting has completed (either successfully or otherwise).
|
`SettingsStore` will perform local echo on all settings to ensure that immediately getting values does not cause a
|
||||||
|
split-brain scenario. As mentioned in the "Setting values for a setting" section, the appropriate checks should be done
|
||||||
|
to ensure that the user is allowed to set the value. The local echo system assumes that the user has permission and that
|
||||||
|
the request will go through successfully. The local echo only takes effect until the request to save a setting has
|
||||||
|
completed (either successfully or otherwise).
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
SettingsStore.setValue(...).then(() => {
|
SettingsStore.setValue(...).then(() => {
|
||||||
|
@ -133,7 +172,12 @@ SettingsStore.getValue(...); // this will return the value set in `setValue` abo
|
||||||
|
|
||||||
## Watching for changes
|
## Watching for changes
|
||||||
|
|
||||||
Most use cases do not need to set up a watcher because they are able to react to changes as they are made, or the changes which are made are not significant enough for it to matter. Watchers are intended to be used in scenarios where it is important to react to changes made by other logged in devices. Typically, this would be done within the component itself, however the component should not be aware of the intricacies of setting inversion or remapping to particular data structures. Instead, a generic watcher interface is provided on `SettingsStore` to watch (and subsequently unwatch) for changes in a setting.
|
Most use cases do not need to set up a watcher because they are able to react to changes as they are made, or the
|
||||||
|
changes which are made are not significant enough for it to matter. Watchers are intended to be used in scenarios where
|
||||||
|
it is important to react to changes made by other logged in devices. Typically, this would be done within the component
|
||||||
|
itself, however the component should not be aware of the intricacies of setting inversion or remapping to particular
|
||||||
|
data structures. Instead, a generic watcher interface is provided on `SettingsStore` to watch (and subsequently unwatch)
|
||||||
|
for changes in a setting.
|
||||||
|
|
||||||
An example of a watcher in action would be:
|
An example of a watcher in action would be:
|
||||||
|
|
||||||
|
@ -143,9 +187,10 @@ class MyComponent extends React.Component {
|
||||||
settingWatcherRef = null;
|
settingWatcherRef = null;
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", (settingName, roomId, level, newValAtLevel, newVal) => {
|
const callback = (settingName, roomId, level, newValAtLevel, newVal) => {
|
||||||
this.setState({color: newVal});
|
this.setState({color: newVal});
|
||||||
});
|
};
|
||||||
|
this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -157,21 +202,37 @@ class MyComponent extends React.Component {
|
||||||
|
|
||||||
# Maintainers Reference
|
# Maintainers Reference
|
||||||
|
|
||||||
The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is supposed to work.
|
The granular settings system has a few complex parts to power it. This section is to document how the `SettingsStore` is
|
||||||
|
supposed to work.
|
||||||
|
|
||||||
### General information
|
### General information
|
||||||
|
|
||||||
The `SettingsStore` uses the hardcoded `LEVEL_ORDER` constant to ensure that it is using the correct override procedure. 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`.
|
The `SettingsStore` uses the hardcoded `LEVEL_ORDER` constant to ensure that it is using the correct override procedure.
|
||||||
|
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`.
|
||||||
|
|
||||||
Handlers (`src/settings/handlers/SettingsHandler.js`) 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 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 their level (for example, a setting being renamed or using a different key from other settings in the underlying store). 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.
|
Handlers (`src/settings/handlers/SettingsHandler.js`) 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
|
||||||
|
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
|
||||||
|
their level (for example, a setting being renamed or using a different key from other settings in the underlying store).
|
||||||
|
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.
|
||||||
|
|
||||||
Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.js` 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 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.
|
Local echo is achieved through `src/settings/handlers/LocalEchoWrapper.js` 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
|
||||||
|
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.
|
||||||
|
|
||||||
Controllers are notified of changes by the `SettingsStore`, and are given the opportunity to override values after the `SettingsStore` has deemed the value calculated. Controllers are invoked as the last possible step in the code.
|
Controllers are notified of changes by the `SettingsStore`, and are given the opportunity to override values after the
|
||||||
|
`SettingsStore` has deemed the value calculated. Controllers are invoked as the last possible step in the code.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
Features automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enable_labs` is 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 automatically get considered as `disabled` if they are not listed in the `SdkConfig` or `enable_labs` is
|
||||||
|
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": {
|
"features": {
|
||||||
|
@ -185,9 +246,18 @@ If `enableLabs` is true in the configuration, the default for features becomes `
|
||||||
|
|
||||||
### Watchers
|
### Watchers
|
||||||
|
|
||||||
Watchers can appear complicated under the hood: there is a central `WatchManager` which handles the actual invocation of callbacks, and callbacks are managed by the SettingsStore by redirecting the caller's callback to a dedicated callback. This is done so that the caller can reuse the same function as their callback without worrying about whether or not it'll unsubscribe all watchers.
|
Watchers can appear complicated under the hood: there is a central `WatchManager` which handles the actual invocation
|
||||||
|
of callbacks, and callbacks are managed by the SettingsStore by redirecting the caller's callback to a dedicated
|
||||||
|
callback. This is done so that the caller can reuse the same function as their callback without worrying about whether
|
||||||
|
or not it'll unsubscribe all watchers.
|
||||||
|
|
||||||
Setting changes are emitted into the default `WatchManager`, which calculates the new value for the setting. Ideally, we'd also try and suppress updates which don't have a consequence on this value, however there's not an easy way to do this. Instead, we just dispatch an update for all changes and leave it up to the consumer to deduplicate.
|
Setting changes are emitted into the default `WatchManager`, which calculates the new value for the setting. Ideally,
|
||||||
|
we'd also try and suppress updates which don't have a consequence on this value, however there's not an easy way to do
|
||||||
|
this. Instead, we just dispatch an update for all changes and leave it up to the consumer to deduplicate.
|
||||||
|
|
||||||
In practice, handlers which rely on remote changes (account data, room events, etc) will always attach a listener to the `MatrixClient`. They then watch for changes to events they care about and send off appropriate updates to the generalized `WatchManager` - a class specifically designed to deduplicate the logic of managing watchers. The handlers which are localized to the local client (device) generally just trigger the `WatchManager` when they manipulate the setting themselves as there's nothing to really 'watch'.
|
In practice, handlers which rely on remote changes (account data, room events, etc) will always attach a listener to the
|
||||||
|
`MatrixClient`. They then watch for changes to events they care about and send off appropriate updates to the
|
||||||
|
generalized `WatchManager` - a class specifically designed to deduplicate the logic of managing watchers. The handlers
|
||||||
|
which are localized to the local client (device) generally just trigger the `WatchManager` when they manipulate the
|
||||||
|
setting themselves as there's nothing to really 'watch'.
|
||||||
|
|
Loading…
Reference in a new issue