Update documentation

Signed-off-by: Travis Ralston <travpc@gmail.com>
This commit is contained in:
Travis Ralston 2017-11-05 14:33:40 -07:00
parent 50807d498a
commit f0000f7400
2 changed files with 101 additions and 55 deletions

View file

@ -1,6 +1,6 @@
# 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 away 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
@ -17,92 +17,135 @@ Granular Settings rely on a series of known levels in order to use the correct v
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.
## `SettingsStore` Usage
## Settings
`SettingsStore` has various utility functions exposed to deal with common tasks, such as translations, getting and setting values, and permission to change 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:
```
// 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
// levels, and it serves to demonstrate the expected type to other developers. The value isn't enforced, but it
// should be respected throughout the code. The default may be any data type.
default: false,
### Getting the value of a setting
// 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.
For most cases the easiest way to get a setting's value is through `SettingsStore.getValue("the_setting", "!curbf:matrix.org")`. The first argument is the setting name and the second argument is the room ID. The room ID should be included where possible, although it is optional.
displayName: _td("Change something"), // effectively `displayName: { "default": _td("Change something") }`
displayName: {
"room": _td("Change something for participants of this room"),
Getting values at particular levels is rare and generally only needed to display information about that level. To get a value at a particular level, use `SettingsStore.getValueAt("room", "the_setting", "!curbf:matrix.org")`. The first argument is the level to read the value at, and the remaining two arguments are just like `getValue()`. This will by default take into consideration any levels that are more generic, if this is undesired (such as when dealing with room-level settings) set the fourth argument (`isExplicit`) to `true`.
// Note: the default will be used if the level requested (such as `device`) does not have a string defined here.
"default": _td("Change something"),
}
}
```
TODO: {Travis} explain `isExplicit` better (when exactly do you use it?)
### 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.
### Checking to make sure a user can change a setting
Users often would like to change settings, however they also often need permission to do so. `SettingsStore` exposes simple permission checks to ensure the user is able to change particular settings, allowing the UI to react accordingly. To see if a user can modify a setting, use `SettingsStore.canSetValue("room", "the_setting", "!curbf:matrix.org")`. The first argument is the level to check at, and the other two arguments are just like getting values.
TODO: {Travis} Also describe how `isLevelSupported` works.
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 is as simple as calling `SettingsStore.setValue("the_setting", "!curbf:matrix.org", "room", "the_value")`. The first argument is the setting name and the second is the room ID. Much like getting values, the room ID is optional but should be supplied whenever possible. The third argument is the level to set the value at, and finally the last argument is the value to set. The value may be anything. If the value is set to `null` or `undefined`, the level will become "unset", requiring more generic levels to provide a value for the 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:
```javascript
const isSupported = SettingsStore.isLevelSupported(SettingLevel.ROOM);
if (isSupported) {
const canSetValue = SettingsStore.canSetValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM);
if (canSetValue) {
SettingsStore.setValue("mySetting", "!curbf:matrix.org", SettingLevel.ROOM, newValue);
}
}
```
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.
### Getting translated names for settings
##### `SettingsFlag` component
TODO: {Travis}
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
<SettingsFlag name="theSettingId"
level={SettingsLevel.ROOM}
roomId="!curbf:matrix.org"
label={_td("Your label here")} // optional, if falsey then the `SettingsStore` will be used
onChange={function(newValue) { }} // optional, called after saving
isExplicit={false} // this is passed along to `SettingsStore.getValueAt`, defaulting to false
manualSave={false} // if true, saving is delayed. You will need to call .save() on this component
// Options for radio buttons
group="your-radio-group" // this enables radio button support
value="yourValueHere" // the value for this particular option
/>
```
### 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`.
## Features
Features (also known as "labs settings") are major components of the SDK which may be undergoing testing before being considered stable and part of the SDK. Features are special cased in Granular Settings to ensure that users do not accidentally get features enabled when they should not be.
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`.
### 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.
### 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`.
### Adding new features
## Setting controllers
TODO: {Travis}
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`.
### Checking if a feature is enabled
## Local echo
TODO: {Travis}
`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
SettingsStore.setValue(...).then(() => {
// The value has actually been stored at this point.
});
SettingsStore.getValue(...); // this will return the value set in `setValue` above.
```
### Making features enabled
TODO: {Travis}
# 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.
### Forcing features to be enabled or disabled
### General information
TODO: {Travis}
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.
## Adding new settings
TODO: {Travis}
## `SettingsFlag` Component Usage
TODO: {Travis}
## Maintainer Documentation
TODO: {Travis}
### Handlers
TODO: {Travis}
### Level order
TODO: {Travis}
### Algorithm
TODO: {Travis}
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.
### Features
TODO: {Travis}
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": {
"feature_groups": "enable",
"feature_pinning": "disable", // the default
"feature_presence": "labs"
}
```
If `enableLabs` is true in the configuration, the default for features becomes `"labs"`.

View file

@ -161,6 +161,9 @@ export default class SettingsStore {
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, "device", value);
}
@ -261,7 +264,7 @@ export default class SettingsStore {
throw new Error("User cannot set " + settingName + " at " + level + " in " + roomId);
}
return handler.setValue(settingName, roomId, value).finally(() => {
return handler.setValue(settingName, roomId, value).then(() => {
const controller = SETTINGS[settingName].controller;
if (!controller) return;
controller.onChange(level, roomId, value);