Skip to content
This repository has been archived by the owner on Sep 11, 2024. It is now read-only.

Granular settings #1516

Merged
merged 57 commits into from
Nov 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
8c3e5eb
Create GranularSettingStore
turt2live Oct 23, 2017
e02dcae
Change wording to better describe the class
turt2live Oct 23, 2017
c43bf33
Appease the linter
turt2live Oct 23, 2017
989bdcf
Rebuild SettingsStore to be better supported
turt2live Oct 29, 2017
23d159e
Make reading settings synchronous
turt2live Oct 29, 2017
7dda5e9
Appease the linter round 1
turt2live Oct 29, 2017
bf815f4
Support labs features
turt2live Oct 29, 2017
ae10a11
Convert synced settings to granular settings
turt2live Oct 29, 2017
e8acb0e
Use getValueAt()
turt2live Oct 29, 2017
b3d17a7
Support legacy settings on the device
turt2live Oct 29, 2017
0d3f0ea
Convert local settings to granular settings
turt2live Oct 29, 2017
52f227c
Drop the idea of "config" being automatically supported
turt2live Oct 29, 2017
b5d5c81
Add a new component to back various settings
turt2live Oct 30, 2017
9fdc1be
Make getLevelAt() return more generic responses
turt2live Oct 30, 2017
786bd87
Support URL previews (with bugs)
turt2live Oct 30, 2017
f44622b
i18n for URL previews
turt2live Oct 30, 2017
b139c8d
Generate unique IDs for SettingsCheckbox
turt2live Oct 30, 2017
c056f4f
Make URL preview checkboxes pretty again
turt2live Oct 30, 2017
99ee58d
Don't break the room settings page
turt2live Oct 30, 2017
72517f9
Don't explode if the SdkConfig isn't available
turt2live Oct 30, 2017
9c846e4
Fix URL preview options
turt2live Oct 31, 2017
3efb0fd
Support default theme at config level
turt2live Oct 31, 2017
f070604
Rename SettingsCheckbox to be a bit more generic
turt2live Oct 31, 2017
6f85230
Some documentation
turt2live Oct 31, 2017
893c39b
Merge branch 'develop' into travis/granular-settings
turt2live Nov 4, 2017
8282534
Add SettingsLevel enum; Move settings to own file.
turt2live Nov 4, 2017
257e1aa
Improve documentation of settings
turt2live Nov 4, 2017
a8169a6
Throw errors if the setting isn't known
turt2live Nov 4, 2017
bb5f7be
Check the correct event type for the room
turt2live Nov 4, 2017
358298e
Support room color in settings
turt2live Nov 4, 2017
4f1ad97
Delay URL preview saving until the save button is pressed
turt2live Nov 4, 2017
c7eee36
Fix lanugage detection
turt2live Nov 4, 2017
cb17c0a
Migrate blacklistUnverifiedDevicesPerRoom
turt2live Nov 5, 2017
b75be41
Fix split-brain for blacklistUnverifiedDevices
turt2live Nov 5, 2017
ef45d82
Fix URL preview save calls
turt2live Nov 5, 2017
45140ca
Don't assume `false` means "use state"
turt2live Nov 5, 2017
3958e32
Ensure blacklistUnverifiedDevices persists reloads
turt2live Nov 5, 2017
8351ec7
Local echo on all setting levels
turt2live Nov 5, 2017
7ce4316
Initial support for notification settings
turt2live Nov 5, 2017
e31c89d
Make the controller work for notifications
turt2live Nov 5, 2017
50807d4
Use settings in slash commands too
turt2live Nov 5, 2017
f0000f7
Update documentation
turt2live Nov 5, 2017
10519f9
Fix the tests
turt2live Nov 5, 2017
781b94c
Appease the linter
turt2live Nov 5, 2017
289b0c2
Merge branch 'develop' into travis/granular-settings
turt2live Nov 7, 2017
c7d8f39
Revert notification toolbar setting
turt2live Nov 9, 2017
f7da583
Make blacklistUnverifiedDevices override the level order
turt2live Nov 9, 2017
2711da2
Use the SettingsStore in the UnknownDeviceDialog
turt2live Nov 9, 2017
030633f
Merge remote-tracking branch 'matrix-org/develop' into travis/granula…
turt2live Nov 9, 2017
c5c346f
Merge branch 'develop' into travis/granular-settings
turt2live Nov 13, 2017
0648b3e
Merge branch 'develop' into travis/granular-settings
turt2live Nov 13, 2017
ffecb82
Groups were released
turt2live Nov 13, 2017
63bebe9
Split out avatar and display name hiding
turt2live Nov 13, 2017
d92cba2
Use the SettingsStore to get the theme
turt2live Nov 13, 2017
bd4102e
Merge branch 'develop' into travis/granular-settings
turt2live Nov 15, 2017
443bff5
Presence feature
turt2live Nov 15, 2017
f58e882
Use the correct settings store for presence
turt2live Nov 15, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions docs/settings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# 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.


## 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:
* `device` - The current user's device
* `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
* `account` - The current user's account
* `room` - A specific room (setting for all members of the room)
* `config` - Values are defined by `config.json`
* `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.


## 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,

// 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"),
}
}
```

### 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.

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

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.

##### `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.
```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

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`.


## 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.

For more information, see `src/settings/controllers/SettingController.js`.


## 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).

```javascript
SettingsStore.setValue(...).then(() => {
// The value has actually been stored at this point.
});
SettingsStore.getValue(...); // this will return the value set in `setValue` above.
```



# 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.

### 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`.

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.

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 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"`.
4 changes: 2 additions & 2 deletions src/CallHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,13 @@ limitations under the License.
*/

import MatrixClientPeg from './MatrixClientPeg';
import UserSettingsStore from './UserSettingsStore';
import PlatformPeg from './PlatformPeg';
import Modal from './Modal';
import sdk from './index';
import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk';
import dis from './dispatcher';
import SettingsStore from "./settings/SettingsStore";

global.mxCalls = {
//room_id: MatrixCall
Expand Down Expand Up @@ -246,7 +246,7 @@ function _onAction(payload) {
} else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false),
forceTURN: SettingsStore.getValue('webRtcForceTURN'),
});
placeCall(call);
} else { // > 2
Expand Down
18 changes: 8 additions & 10 deletions src/CallMediaHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
limitations under the License.
*/

import UserSettingsStore from './UserSettingsStore';
import * as Matrix from 'matrix-js-sdk';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";

export default {
getDevices: function() {
Expand Down Expand Up @@ -43,22 +43,20 @@ export default {
},

loadDevices: function() {
// this.getDevices().then((devices) => {
const localSettings = UserSettingsStore.getLocalSettings();
// // if deviceId is not found, automatic fallback is in spec
// // recall previously stored inputs if any
Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']);
Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
// });
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");

Matrix.setMatrixCallAudioInput(audioDeviceId);
Matrix.setMatrixCallVideoInput(videoDeviceId);
},

setAudioInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId);
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallAudioInput(deviceId);
},

setVideoInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId);
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallVideoInput(deviceId);
},
};
4 changes: 4 additions & 0 deletions src/Lifecycle.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,10 @@ function startMatrixClient() {
DMRoomMap.makeShared().start();

MatrixClientPeg.start();

// dispatch that we finished starting up to wire up any other bits
// of the matrix client that cannot be set prior to starting up.
dis.dispatch({action: 'client_started'});
}

/*
Expand Down
56 changes: 14 additions & 42 deletions src/Notifier.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import dis from './dispatcher';
import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";

/*
* Dispatches:
Expand Down Expand Up @@ -138,17 +139,16 @@ const Notifier = {

// make sure that we persist the current setting audio_enabled setting
// before changing anything
if (global.localStorage) {
if (global.localStorage.getItem('audio_notifications_enabled') === null) {
this.setAudioEnabled(this.isEnabled());
}
if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) {
SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled());
}

if (enable) {
// Attempt to get permission from user
plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') {
// The permission request was dismissed or denied
// TODO: Support alternative branding in messaging
const description = result === 'denied'
? _t('Riot does not have permission to send you notifications - please check your browser settings')
: _t('Riot was not given permission to send notifications - please try again');
Expand All @@ -160,10 +160,6 @@ const Notifier = {
return;
}

if (global.localStorage) {
global.localStorage.setItem('notifications_enabled', 'true');
}

if (callback) callback();
dis.dispatch({
action: "notifier_enabled",
Expand All @@ -174,8 +170,6 @@ const Notifier = {
// disabled again in the future, we will show the banner again.
this.setToolbarHidden(false);
} else {
if (!global.localStorage) return;
global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({
action: "notifier_enabled",
value: false,
Expand All @@ -184,44 +178,24 @@ const Notifier = {
},

isEnabled: function() {
return this.isPossible() && SettingsStore.getValue("notificationsEnabled");
},

isPossible: function() {
const plaf = PlatformPeg.get();
if (!plaf) return false;
if (!plaf.supportsNotifications()) return false;
if (!plaf.maySendNotifications()) return false;

if (!global.localStorage) return true;

const enabled = global.localStorage.getItem('notifications_enabled');
if (enabled === null) return true;
return enabled === 'true';
},

setBodyEnabled: function(enable) {
if (!global.localStorage) return;
global.localStorage.setItem('notifications_body_enabled', enable ? 'true' : 'false');
return true; // possible, but not necessarily enabled
},

isBodyEnabled: function() {
if (!global.localStorage) return true;
const enabled = global.localStorage.getItem('notifications_body_enabled');
// default to true if the popups are enabled
if (enabled === null) return this.isEnabled();
return enabled === 'true';
return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled");
},

setAudioEnabled: function(enable) {
if (!global.localStorage) return;
global.localStorage.setItem('audio_notifications_enabled',
enable ? 'true' : 'false');
},

isAudioEnabled: function(enable) {
if (!global.localStorage) return true;
const enabled = global.localStorage.getItem(
'audio_notifications_enabled');
// default to true if the popups are enabled
if (enabled === null) return this.isEnabled();
return enabled === 'true';
isAudioEnabled: function() {
return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled");
},

setToolbarHidden: function(hidden, persistent = true) {
Expand All @@ -238,16 +212,14 @@ const Notifier = {

// update the info to localStorage for persistent settings
if (persistent && global.localStorage) {
global.localStorage.setItem('notifications_hidden', hidden);
global.localStorage.setItem("notifications_hidden", hidden);
}
},

isToolbarHidden: function() {
// Check localStorage for any such meta data
if (global.localStorage) {
if (global.localStorage.getItem('notifications_hidden') === 'true') {
return true;
}
return global.localStorage.getItem("notifications_hidden") === "true";
}

return this.toolbarHidden;
Expand Down
5 changes: 2 additions & 3 deletions src/SlashCommands.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Tinter from "./Tinter";
import sdk from './index';
import { _t } from './languageHandler';
import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";


class Command {
Expand Down Expand Up @@ -97,9 +98,7 @@ const commands = {
colorScheme.secondary_color = matches[4];
}
return success(
MatrixClientPeg.get().setRoomAccountData(
roomId, "org.matrix.room.color_scheme", colorScheme,
),
SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
);
}
}
Expand Down
Loading