Merge remote-tracking branch 'origin/develop' into dbkr/udd_no_auto_show

This commit is contained in:
David Baker 2017-11-15 12:11:33 +00:00
commit f8fc6dc83e
69 changed files with 3101 additions and 1068 deletions

View file

@ -1,3 +1,267 @@
Changes in [0.11.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0) (2017-11-15)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.3...v0.11.0)
Changes in [0.11.0-rc.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.3) (2017-11-14)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.2...v0.11.0-rc.3)
Changes in [0.11.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.2) (2017-11-10)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.11.0-rc.1...v0.11.0-rc.2)
* Make groups a fully-fleged baked-in feature
[\#1603](https://github.com/matrix-org/matrix-react-sdk/pull/1603)
Changes in [0.11.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.11.0-rc.1) (2017-11-10)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7...v0.11.0-rc.1)
* Improve widget rendering on prop updates
[\#1548](https://github.com/matrix-org/matrix-react-sdk/pull/1548)
* Display group member profile (avatar/displayname) in ConfirmUserActionDialog
[\#1595](https://github.com/matrix-org/matrix-react-sdk/pull/1595)
* Don't crash if there isn't a room notif rule
[\#1602](https://github.com/matrix-org/matrix-react-sdk/pull/1602)
* Show group name in flair tooltip if one is set
[\#1596](https://github.com/matrix-org/matrix-react-sdk/pull/1596)
* Convert group avatar URL to HTTP before handing to BaseAvatar
[\#1597](https://github.com/matrix-org/matrix-react-sdk/pull/1597)
* Add group features as starting points for ILAG
[\#1601](https://github.com/matrix-org/matrix-react-sdk/pull/1601)
* Modify the group room visibility API to reflect the js-sdk changes
[\#1598](https://github.com/matrix-org/matrix-react-sdk/pull/1598)
* Update from Weblate.
[\#1599](https://github.com/matrix-org/matrix-react-sdk/pull/1599)
* Revert "UnknownDeviceDialog: get devices from SDK"
[\#1594](https://github.com/matrix-org/matrix-react-sdk/pull/1594)
* Order users in the group member list with admins first
[\#1591](https://github.com/matrix-org/matrix-react-sdk/pull/1591)
* Fetch group members after accepting an invite
[\#1592](https://github.com/matrix-org/matrix-react-sdk/pull/1592)
* Improve address picker for rooms
[\#1589](https://github.com/matrix-org/matrix-react-sdk/pull/1589)
* Fix FlairStore getPublicisedGroupsCached to give the correct, existing
promise
[\#1590](https://github.com/matrix-org/matrix-react-sdk/pull/1590)
* Use the getProfileInfo API for group inviter profile
[\#1585](https://github.com/matrix-org/matrix-react-sdk/pull/1585)
* Add checkbox to GroupAddressPicker for determining visibility of group rooms
[\#1587](https://github.com/matrix-org/matrix-react-sdk/pull/1587)
* Alter group member api
[\#1581](https://github.com/matrix-org/matrix-react-sdk/pull/1581)
* Improve group creation UX
[\#1580](https://github.com/matrix-org/matrix-react-sdk/pull/1580)
* Disable RoomDetailList in GroupView when editing
[\#1583](https://github.com/matrix-org/matrix-react-sdk/pull/1583)
* Default to no read pins if there is no applicable account data
[\#1586](https://github.com/matrix-org/matrix-react-sdk/pull/1586)
* UnknownDeviceDialog: get devices from SDK
[\#1584](https://github.com/matrix-org/matrix-react-sdk/pull/1584)
* Add a small indicator for when a new event is pinned
[\#1486](https://github.com/matrix-org/matrix-react-sdk/pull/1486)
* Implement tooltip for group rooms
[\#1582](https://github.com/matrix-org/matrix-react-sdk/pull/1582)
* Room notifs in autocomplete & composer
[\#1577](https://github.com/matrix-org/matrix-react-sdk/pull/1577)
* Ignore img tags in HTML if src is not specified
[\#1579](https://github.com/matrix-org/matrix-react-sdk/pull/1579)
* Indicate admins in the group member list with a sheriff badge
[\#1578](https://github.com/matrix-org/matrix-react-sdk/pull/1578)
* Remember whether widget drawer was hidden per-room
[\#1533](https://github.com/matrix-org/matrix-react-sdk/pull/1533)
* Throw an error when trying to create a group store with falsey groupId
[\#1576](https://github.com/matrix-org/matrix-react-sdk/pull/1576)
* Fixes React warning
[\#1571](https://github.com/matrix-org/matrix-react-sdk/pull/1571)
* Fix Flair not appearing due to missing this._usersInFlight
[\#1575](https://github.com/matrix-org/matrix-react-sdk/pull/1575)
* Use, if possible, a room's canonical or first alias when viewing the …
[\#1574](https://github.com/matrix-org/matrix-react-sdk/pull/1574)
* Add CSS classes to group ID input in CreateGroupDialog
[\#1573](https://github.com/matrix-org/matrix-react-sdk/pull/1573)
* Give autocomplete providers the room they're in
[\#1568](https://github.com/matrix-org/matrix-react-sdk/pull/1568)
* Fix multiple pills on one line
[\#1572](https://github.com/matrix-org/matrix-react-sdk/pull/1572)
* Fix group invites such that they look similar to room invites
[\#1570](https://github.com/matrix-org/matrix-react-sdk/pull/1570)
* Add a GeminiScrollbar to Your Communities
[\#1569](https://github.com/matrix-org/matrix-react-sdk/pull/1569)
* Fix multiple requests for publicised groups of given user
[\#1567](https://github.com/matrix-org/matrix-react-sdk/pull/1567)
* Add toggle to alter visibility of a room-group association
[\#1566](https://github.com/matrix-org/matrix-react-sdk/pull/1566)
* Pillify room notifs in the timeline
[\#1564](https://github.com/matrix-org/matrix-react-sdk/pull/1564)
* Implement simple GroupRoomInfo
[\#1563](https://github.com/matrix-org/matrix-react-sdk/pull/1563)
* turn NPE on flair resolution errors into a logged error
[\#1565](https://github.com/matrix-org/matrix-react-sdk/pull/1565)
* Less translation in parts
[\#1484](https://github.com/matrix-org/matrix-react-sdk/pull/1484)
* Redact group IDs from analytics
[\#1562](https://github.com/matrix-org/matrix-react-sdk/pull/1562)
* Display whether the group summary/room list is loading
[\#1560](https://github.com/matrix-org/matrix-react-sdk/pull/1560)
* Change client-side validation of group IDs to match synapse
[\#1558](https://github.com/matrix-org/matrix-react-sdk/pull/1558)
* Prevent non-members from opening group settings
[\#1559](https://github.com/matrix-org/matrix-react-sdk/pull/1559)
* Alter UI for disinviting a group member
[\#1556](https://github.com/matrix-org/matrix-react-sdk/pull/1556)
* Only show admin tools to privileged users
[\#1555](https://github.com/matrix-org/matrix-react-sdk/pull/1555)
* Try lowercase username on login
[\#1550](https://github.com/matrix-org/matrix-react-sdk/pull/1550)
* Don't refresh page on password change prompt
[\#1554](https://github.com/matrix-org/matrix-react-sdk/pull/1554)
* Fix initial in GroupAvatar in GroupView
[\#1553](https://github.com/matrix-org/matrix-react-sdk/pull/1553)
* Use "crop" method to scale group avatars in MyGroups
[\#1549](https://github.com/matrix-org/matrix-react-sdk/pull/1549)
* Lowercase all usernames
[\#1547](https://github.com/matrix-org/matrix-react-sdk/pull/1547)
* Add sensible missing entry generator for MELS tests
[\#1546](https://github.com/matrix-org/matrix-react-sdk/pull/1546)
* Fix prompt to re-use chat room
[\#1545](https://github.com/matrix-org/matrix-react-sdk/pull/1545)
* Add unregiseterListener to GroupStore
[\#1544](https://github.com/matrix-org/matrix-react-sdk/pull/1544)
* Fix groups invited users err for non members
[\#1543](https://github.com/matrix-org/matrix-react-sdk/pull/1543)
* Add Mention button to MemberInfo
[\#1532](https://github.com/matrix-org/matrix-react-sdk/pull/1532)
* Only show group settings cog to members
[\#1541](https://github.com/matrix-org/matrix-react-sdk/pull/1541)
* Use correct icon for group room deletion and make themeable
[\#1540](https://github.com/matrix-org/matrix-react-sdk/pull/1540)
* Add invite button to MemberInfo if user has left or wasn't in room
[\#1534](https://github.com/matrix-org/matrix-react-sdk/pull/1534)
* Add option to mirror local video feed
[\#1539](https://github.com/matrix-org/matrix-react-sdk/pull/1539)
* Use the correct userId when displaying who redacted a message
[\#1538](https://github.com/matrix-org/matrix-react-sdk/pull/1538)
* Only show editing UI for aliases/related_groups for users /w power
[\#1529](https://github.com/matrix-org/matrix-react-sdk/pull/1529)
* Swap from `ui_opacity` to `panel_disabled`
[\#1535](https://github.com/matrix-org/matrix-react-sdk/pull/1535)
* Fix room address picker tiles default name
[\#1536](https://github.com/matrix-org/matrix-react-sdk/pull/1536)
* T3chguy/hide level change on 50
[\#1531](https://github.com/matrix-org/matrix-react-sdk/pull/1531)
* fix missing date sep caused by hidden event at start of day
[\#1537](https://github.com/matrix-org/matrix-react-sdk/pull/1537)
* Add a delete confirmation dialog for widgets
[\#1520](https://github.com/matrix-org/matrix-react-sdk/pull/1520)
* When dispatching view_[my_]group[s], reset RoomViewStore
[\#1530](https://github.com/matrix-org/matrix-react-sdk/pull/1530)
* Prevent editing of UI requiring user privilege if user unprivileged
[\#1528](https://github.com/matrix-org/matrix-react-sdk/pull/1528)
* Use the correct property of the API room objects
[\#1526](https://github.com/matrix-org/matrix-react-sdk/pull/1526)
* Don't include the |other in the translation value
[\#1527](https://github.com/matrix-org/matrix-react-sdk/pull/1527)
* Re-run gen-i18n after fixing https://github.com/matrix-org/matrix-react-
sdk/pull/1521
[\#1525](https://github.com/matrix-org/matrix-react-sdk/pull/1525)
* Fix some react warnings in GroupMemberList
[\#1522](https://github.com/matrix-org/matrix-react-sdk/pull/1522)
* Fix bug with gen-i18n/js when adding new plurals
[\#1521](https://github.com/matrix-org/matrix-react-sdk/pull/1521)
* Make GroupStoreCache global for cross-package access
[\#1524](https://github.com/matrix-org/matrix-react-sdk/pull/1524)
* Add fields needed by RoomDetailList to groupRoomFromApiObject
[\#1523](https://github.com/matrix-org/matrix-react-sdk/pull/1523)
* Only show flair for groups with avatars set
[\#1519](https://github.com/matrix-org/matrix-react-sdk/pull/1519)
* Refresh group member lists after inviting users
[\#1518](https://github.com/matrix-org/matrix-react-sdk/pull/1518)
* Invalidate the user's public groups cache when changing group publicity
[\#1517](https://github.com/matrix-org/matrix-react-sdk/pull/1517)
* Make the gen-i18n script validate _t calls
[\#1515](https://github.com/matrix-org/matrix-react-sdk/pull/1515)
* Add placeholder to MyGroups page, adjust CSS classes
[\#1514](https://github.com/matrix-org/matrix-react-sdk/pull/1514)
* Rxl881/parallelshell
[\#1338](https://github.com/matrix-org/matrix-react-sdk/pull/1338)
* Run prunei18n
[\#1513](https://github.com/matrix-org/matrix-react-sdk/pull/1513)
* Update from Weblate.
[\#1512](https://github.com/matrix-org/matrix-react-sdk/pull/1512)
* Add script to prune unused translations
[\#1502](https://github.com/matrix-org/matrix-react-sdk/pull/1502)
* Fix creation of DM rooms
[\#1510](https://github.com/matrix-org/matrix-react-sdk/pull/1510)
* Group create dialog: only enter localpart
[\#1507](https://github.com/matrix-org/matrix-react-sdk/pull/1507)
* Improve MyGroups UI
[\#1509](https://github.com/matrix-org/matrix-react-sdk/pull/1509)
* Use object URLs to load Files in to images
[\#1508](https://github.com/matrix-org/matrix-react-sdk/pull/1508)
* Add clientside error for non-alphanumeric group ID
[\#1506](https://github.com/matrix-org/matrix-react-sdk/pull/1506)
* Fix invites to groups without names
[\#1505](https://github.com/matrix-org/matrix-react-sdk/pull/1505)
* Add warning when adding group rooms/users
[\#1504](https://github.com/matrix-org/matrix-react-sdk/pull/1504)
* More Groups->Communities
[\#1503](https://github.com/matrix-org/matrix-react-sdk/pull/1503)
* Groups -> Communities
[\#1501](https://github.com/matrix-org/matrix-react-sdk/pull/1501)
* Factor out Flair cache into FlairStore
[\#1500](https://github.com/matrix-org/matrix-react-sdk/pull/1500)
* Add i18n script to package.json
[\#1499](https://github.com/matrix-org/matrix-react-sdk/pull/1499)
* Make gen-i18n support 'HTML'
[\#1498](https://github.com/matrix-org/matrix-react-sdk/pull/1498)
* fix editing visuals on groupview header
[\#1497](https://github.com/matrix-org/matrix-react-sdk/pull/1497)
* Script to generate the translations base file
[\#1493](https://github.com/matrix-org/matrix-react-sdk/pull/1493)
* Update from Weblate.
[\#1495](https://github.com/matrix-org/matrix-react-sdk/pull/1495)
* Attempt to relate a group to a room when adding it
[\#1494](https://github.com/matrix-org/matrix-react-sdk/pull/1494)
* Shuffle GroupView UI
[\#1490](https://github.com/matrix-org/matrix-react-sdk/pull/1490)
* Fix bug preventing partial group profile
[\#1491](https://github.com/matrix-org/matrix-react-sdk/pull/1491)
* Don't show room IDs when picking rooms
[\#1492](https://github.com/matrix-org/matrix-react-sdk/pull/1492)
* Only show invited section if there are invited group members
[\#1489](https://github.com/matrix-org/matrix-react-sdk/pull/1489)
* Show "Invited" section in the user list
[\#1488](https://github.com/matrix-org/matrix-react-sdk/pull/1488)
* Refactor class names for an entity tile being hovered over
[\#1487](https://github.com/matrix-org/matrix-react-sdk/pull/1487)
* Modify GroupView UI
[\#1475](https://github.com/matrix-org/matrix-react-sdk/pull/1475)
* Message/event pinning
[\#1439](https://github.com/matrix-org/matrix-react-sdk/pull/1439)
* Remove duplicate declaration that breaks the build
[\#1483](https://github.com/matrix-org/matrix-react-sdk/pull/1483)
* Include magnet scheme in sanitize HTML params
[\#1301](https://github.com/matrix-org/matrix-react-sdk/pull/1301)
* Add a way to jump to a user's Read Receipt from MemberInfo
[\#1454](https://github.com/matrix-org/matrix-react-sdk/pull/1454)
* Use standard subsitution syntax in _tJsx
[\#1462](https://github.com/matrix-org/matrix-react-sdk/pull/1462)
* Don't suggest grey as a color scheme for a room
[\#1442](https://github.com/matrix-org/matrix-react-sdk/pull/1442)
* allow hiding of notification body for privacy reasons
[\#1362](https://github.com/matrix-org/matrix-react-sdk/pull/1362)
* Suggest to invite people when speaking in an empty room
[\#1466](https://github.com/matrix-org/matrix-react-sdk/pull/1466)
* Buttons to remove room/self avatar
[\#1478](https://github.com/matrix-org/matrix-react-sdk/pull/1478)
* T3chguy/fix memberlist
[\#1480](https://github.com/matrix-org/matrix-react-sdk/pull/1480)
* add option to disable BigEmoji
[\#1481](https://github.com/matrix-org/matrix-react-sdk/pull/1481)
Changes in [0.10.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7) (2017-10-16) Changes in [0.10.7](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v0.10.7) (2017-10-16)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.3...v0.10.7) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v0.10.7-rc.3...v0.10.7)

151
docs/settings.md Normal file
View file

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

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "0.10.7", "version": "0.11.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -71,7 +71,7 @@
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.3", "linkifyjs": "^2.1.3",
"lodash": "^4.13.1", "lodash": "^4.13.1",
"matrix-js-sdk": "0.9.0-rc.1", "matrix-js-sdk": "0.9.0",
"optimist": "^0.6.1", "optimist": "^0.6.1",
"prop-types": "^15.5.8", "prop-types": "^15.5.8",
"querystring": "^0.2.0", "querystring": "^0.2.0",

View file

@ -52,7 +52,6 @@ limitations under the License.
*/ */
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import UserSettingsStore from './UserSettingsStore';
import PlatformPeg from './PlatformPeg'; import PlatformPeg from './PlatformPeg';
import Modal from './Modal'; import Modal from './Modal';
import sdk from './index'; import sdk from './index';
@ -60,6 +59,7 @@ import { _t } from './languageHandler';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import dis from './dispatcher'; import dis from './dispatcher';
import { getUnknownDevicesForRoom } from './cryptodevices'; import { getUnknownDevicesForRoom } from './cryptodevices';
import SettingsStore from "./settings/SettingsStore";
global.mxCalls = { global.mxCalls = {
//room_id: MatrixCall //room_id: MatrixCall
@ -262,7 +262,7 @@ function _onAction(payload) {
} else if (members.length === 2) { } else if (members.length === 2) {
console.log("Place %s call in %s", payload.type, payload.room_id); console.log("Place %s call in %s", payload.type, payload.room_id);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, { const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id, {
forceTURN: UserSettingsStore.getLocalSetting('webRtcForceTURN', false), forceTURN: SettingsStore.getValue('webRtcForceTURN'),
}); });
placeCall(call); placeCall(call);
} else { // > 2 } else { // > 2

View file

@ -14,8 +14,8 @@
limitations under the License. limitations under the License.
*/ */
import UserSettingsStore from './UserSettingsStore';
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
export default { export default {
getDevices: function() { getDevices: function() {
@ -43,22 +43,20 @@ export default {
}, },
loadDevices: function() { loadDevices: function() {
// this.getDevices().then((devices) => { const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const localSettings = UserSettingsStore.getLocalSettings(); const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
// // if deviceId is not found, automatic fallback is in spec
// // recall previously stored inputs if any Matrix.setMatrixCallAudioInput(audioDeviceId);
Matrix.setMatrixCallAudioInput(localSettings['webrtc_audioinput']); Matrix.setMatrixCallVideoInput(videoDeviceId);
Matrix.setMatrixCallVideoInput(localSettings['webrtc_videoinput']);
// });
}, },
setAudioInput: function(deviceId) { setAudioInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_audioinput', deviceId); SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallAudioInput(deviceId); Matrix.setMatrixCallAudioInput(deviceId);
}, },
setVideoInput: function(deviceId) { setVideoInput: function(deviceId) {
UserSettingsStore.setLocalSetting('webrtc_videoinput', deviceId); SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
Matrix.setMatrixCallVideoInput(deviceId); Matrix.setMatrixCallVideoInput(deviceId);
}, },
}; };

View file

@ -436,6 +436,10 @@ function startMatrixClient() {
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
MatrixClientPeg.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'});
} }
/* /*

View file

@ -204,6 +204,12 @@ export default class Login {
} }
throw originalLoginError; throw originalLoginError;
}).catch((error) => { }).catch((error) => {
// We apparently squash case at login serverside these days:
// https://github.com/matrix-org/synapse/blob/1189be43a2479f5adf034613e8d10e3f4f452eb9/synapse/handlers/auth.py#L475
// so this wasn't needed after all. Keeping the code around in case the
// the situation changes...
/*
if ( if (
error.httpStatus === 403 && error.httpStatus === 403 &&
loginParams.identifier.type === 'm.id.user' && loginParams.identifier.type === 'm.id.user' &&
@ -211,6 +217,7 @@ export default class Login {
) { ) {
return tryLowercaseUsername(originalLoginError); return tryLowercaseUsername(originalLoginError);
} }
*/
throw originalLoginError; throw originalLoginError;
}).catch((error) => { }).catch((error) => {
console.log("Login failed", error); console.log("Login failed", error);

View file

@ -93,6 +93,7 @@ class MatrixClientPeg {
const opts = utils.deepCopy(this.opts); const opts = utils.deepCopy(this.opts);
// the react sdk doesn't work without this, so don't allow // the react sdk doesn't work without this, so don't allow
opts.pendingEventOrdering = "detached"; opts.pendingEventOrdering = "detached";
opts.disablePresence = true; // we do this manually
try { try {
const promise = this.matrixClient.store.startup(); const promise = this.matrixClient.store.startup();

View file

@ -25,6 +25,7 @@ import dis from './dispatcher';
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
/* /*
* Dispatches: * Dispatches:
@ -138,10 +139,8 @@ const Notifier = {
// make sure that we persist the current setting audio_enabled setting // make sure that we persist the current setting audio_enabled setting
// before changing anything // before changing anything
if (global.localStorage) { if (SettingsStore.isLevelSupported(SettingLevel.DEVICE)) {
if (global.localStorage.getItem('audio_notifications_enabled') === null) { SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, this.isEnabled());
this.setAudioEnabled(this.isEnabled());
}
} }
if (enable) { if (enable) {
@ -149,6 +148,7 @@ const Notifier = {
plaf.requestNotificationPermission().done((result) => { plaf.requestNotificationPermission().done((result) => {
if (result !== 'granted') { if (result !== 'granted') {
// The permission request was dismissed or denied // The permission request was dismissed or denied
// TODO: Support alternative branding in messaging
const description = result === 'denied' const description = result === 'denied'
? _t('Riot does not have permission to send you notifications - please check your browser settings') ? _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'); : _t('Riot was not given permission to send notifications - please try again');
@ -160,10 +160,6 @@ const Notifier = {
return; return;
} }
if (global.localStorage) {
global.localStorage.setItem('notifications_enabled', 'true');
}
if (callback) callback(); if (callback) callback();
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
@ -174,8 +170,6 @@ const Notifier = {
// disabled again in the future, we will show the banner again. // disabled again in the future, we will show the banner again.
this.setToolbarHidden(false); this.setToolbarHidden(false);
} else { } else {
if (!global.localStorage) return;
global.localStorage.setItem('notifications_enabled', 'false');
dis.dispatch({ dis.dispatch({
action: "notifier_enabled", action: "notifier_enabled",
value: false, value: false,
@ -184,44 +178,24 @@ const Notifier = {
}, },
isEnabled: function() { isEnabled: function() {
return this.isPossible() && SettingsStore.getValue("notificationsEnabled");
},
isPossible: function() {
const plaf = PlatformPeg.get(); const plaf = PlatformPeg.get();
if (!plaf) return false; if (!plaf) return false;
if (!plaf.supportsNotifications()) return false; if (!plaf.supportsNotifications()) return false;
if (!plaf.maySendNotifications()) return false; if (!plaf.maySendNotifications()) return false;
if (!global.localStorage) return true; return true; // possible, but not necessarily enabled
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');
}, },
isBodyEnabled: function() { isBodyEnabled: function() {
if (!global.localStorage) return true; return this.isEnabled() && SettingsStore.getValue("notificationBodyEnabled");
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';
}, },
setAudioEnabled: function(enable) { isAudioEnabled: function() {
if (!global.localStorage) return; return this.isEnabled() && SettingsStore.getValue("audioNotificationsEnabled");
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';
}, },
setToolbarHidden: function(hidden, persistent = true) { setToolbarHidden: function(hidden, persistent = true) {
@ -238,16 +212,14 @@ const Notifier = {
// update the info to localStorage for persistent settings // update the info to localStorage for persistent settings
if (persistent && global.localStorage) { if (persistent && global.localStorage) {
global.localStorage.setItem('notifications_hidden', hidden); global.localStorage.setItem("notifications_hidden", hidden);
} }
}, },
isToolbarHidden: function() { isToolbarHidden: function() {
// Check localStorage for any such meta data // Check localStorage for any such meta data
if (global.localStorage) { if (global.localStorage) {
if (global.localStorage.getItem('notifications_hidden') === 'true') { return global.localStorage.getItem("notifications_hidden") === "true";
return true;
}
} }
return this.toolbarHidden; return this.toolbarHidden;

View file

@ -56,13 +56,27 @@ class Presence {
return this.state; return this.state;
} }
/**
* Get the current status message.
* @returns {String} the status message, may be null
*/
getStatusMessage() {
return this.statusMessage;
}
/** /**
* Set the presence state. * Set the presence state.
* If the state has changed, the Home Server will be notified. * If the state has changed, the Home Server will be notified.
* @param {string} newState the new presence state (see PRESENCE enum) * @param {string} newState the new presence state (see PRESENCE enum)
* @param {String} statusMessage an optional status message for the presence
* @param {boolean} maintain true to have this status maintained by this tracker
*/ */
setState(newState) { setState(newState, statusMessage=null, maintain=false) {
if (newState === this.state) { if (this.maintain) {
// Don't update presence if we're maintaining a particular status
return;
}
if (newState === this.state && statusMessage === this.statusMessage) {
return; return;
} }
if (PRESENCE_STATES.indexOf(newState) === -1) { if (PRESENCE_STATES.indexOf(newState) === -1) {
@ -72,21 +86,37 @@ class Presence {
return; return;
} }
const old_state = this.state; const old_state = this.state;
const old_message = this.statusMessage;
this.state = newState; this.state = newState;
this.statusMessage = statusMessage;
this.maintain = maintain;
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
return; // don't try to set presence when a guest; it won't work. return; // don't try to set presence when a guest; it won't work.
} }
const updateContent = {
presence: this.state,
status_msg: this.statusMessage ? this.statusMessage : '',
};
const self = this; const self = this;
MatrixClientPeg.get().setPresence(this.state).done(function() { MatrixClientPeg.get().setPresence(updateContent).done(function() {
console.log("Presence: %s", newState); console.log("Presence: %s", newState);
// We have to dispatch because the js-sdk is unreliable at telling us about our own presence
dis.dispatch({action: "self_presence_updated", statusInfo: updateContent});
}, function(err) { }, function(err) {
console.error("Failed to set presence: %s", err); console.error("Failed to set presence: %s", err);
self.state = old_state; self.state = old_state;
self.statusMessage = old_message;
}); });
} }
stopMaintainingStatus() {
this.maintain = false;
}
/** /**
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms. * Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
* @private * @private
@ -95,7 +125,8 @@ class Presence {
this.setState("unavailable"); this.setState("unavailable");
} }
_onUserActivity() { _onUserActivity(payload) {
if (payload.action === "sync_state" || payload.action === "self_presence_updated") return;
this._resetTimer(); this._resetTimer();
} }

View file

@ -15,19 +15,20 @@ limitations under the License.
*/ */
import { _t } from './languageHandler'; import { _t } from './languageHandler';
export function levelRoleMap() { export function levelRoleMap(usersDefault) {
return { return {
undefined: _t('Default'), undefined: _t('Default'),
0: _t('User'), 0: _t('Restricted'),
[usersDefault]: _t('Default'),
50: _t('Moderator'), 50: _t('Moderator'),
100: _t('Admin'), 100: _t('Admin'),
}; };
} }
export function textualPowerLevel(level, userDefault) { export function textualPowerLevel(level, usersDefault) {
const LEVEL_ROLE_MAP = this.levelRoleMap(); const LEVEL_ROLE_MAP = this.levelRoleMap(usersDefault);
if (LEVEL_ROLE_MAP[level]) { if (LEVEL_ROLE_MAP[level]) {
return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${userDefault})`); return LEVEL_ROLE_MAP[level] + (level !== undefined ? ` (${level})` : ` (${usersDefault})`);
} else { } else {
return level; return level;
} }

View file

@ -26,7 +26,7 @@ const DEFAULTS = {
class SdkConfig { class SdkConfig {
static get() { static get() {
return global.mxReactSdkConfig; return global.mxReactSdkConfig || {};
} }
static put(cfg) { static put(cfg) {

View file

@ -20,6 +20,7 @@ import Tinter from "./Tinter";
import sdk from './index'; import sdk from './index';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import Modal from './Modal'; import Modal from './Modal';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
class Command { class Command {
@ -97,9 +98,7 @@ const commands = {
colorScheme.secondary_color = matches[4]; colorScheme.secondary_color = matches[4];
} }
return success( return success(
MatrixClientPeg.get().setRoomAccountData( SettingsStore.setValue("roomColor", roomId, SettingLevel.ROOM_ACCOUNT, colorScheme),
roomId, "org.matrix.room.color_scheme", colorScheme,
),
); );
} }
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015 OpenMarket Ltd Copyright 2015 OpenMarket Ltd
Copyright 2017 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,70 +15,259 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// FIXME: these vars should be bundled up and attached to
// module.exports otherwise this will break when included by both
// react-sdk and apps layered on top.
const DEBUG = 0; const DEBUG = 0;
// The colour keys to be replaced as referred to in CSS // utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
const keyRgb = [ function colorToRgb(color) {
if (color[0] === '#') {
color = color.slice(1);
if (color.length === 3) {
color = color[0] + color[0] +
color[1] + color[1] +
color[2] + color[2];
}
const val = parseInt(color, 16);
const r = (val >> 16) & 255;
const g = (val >> 8) & 255;
const b = val & 255;
return [r, g, b];
}
else {
let match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
if (match) {
return [ parseInt(match[1]),
parseInt(match[2]),
parseInt(match[3]) ];
}
}
return [0,0,0];
}
// utility to turn [red,green,blue] into #rrggbb
function rgbToColor(rgb) {
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
return '#' + (0x1000000 + val).toString(16).slice(1);
}
class Tinter {
constructor() {
// The default colour keys to be replaced as referred to in CSS
// (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
this.keyRgb = [
"rgb(118, 207, 166)", // Vector Green "rgb(118, 207, 166)", // Vector Green
"rgb(234, 245, 240)", // Vector Light Green "rgb(234, 245, 240)", // Vector Light Green
"rgb(211, 239, 225)", // BottomLeftMenu overlay (20% Vector Green) "rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
]; ];
// Some algebra workings for calculating the tint % of Vector Green & Light Green // Some algebra workings for calculating the tint % of Vector Green & Light Green
// x * 118 + (1 - x) * 255 = 234 // x * 118 + (1 - x) * 255 = 234
// x * 118 + 255 - 255 * x = 234 // x * 118 + 255 - 255 * x = 234
// x * 118 - x * 255 = 234 - 255 // x * 118 - x * 255 = 234 - 255
// (255 - 118) x = 255 - 234 // (255 - 118) x = 255 - 234
// x = (255 - 234) / (255 - 118) = 0.16 // x = (255 - 234) / (255 - 118) = 0.16
// The colour keys to be replaced as referred to in SVGs // The colour keys to be replaced as referred to in SVGs
const keyHex = [ this.keyHex = [
"#76CFA6", // Vector Green "#76CFA6", // Vector Green
"#EAF5F0", // Vector Light Green "#EAF5F0", // Vector Light Green
"#D3EFE1", // BottomLeftMenu overlay (20% Vector Green overlaid on Vector Light Green) "#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme) "#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
]; ];
// cache of our replacement colours // track the replacement colours actually being used
// defaults to our keys. // defaults to our keys.
const colors = [ this.colors = [
keyHex[0], this.keyHex[0],
keyHex[1], this.keyHex[1],
keyHex[2], this.keyHex[2],
keyHex[3], this.keyHex[3],
]; ];
const cssFixups = [ // track the most current tint request inputs (which may differ from the
// { // end result stored in this.colors
this.currentTint = [
undefined,
undefined,
undefined,
undefined,
];
this.cssFixups = [
// { theme: {
// style: a style object that should be fixed up taken from a stylesheet // style: a style object that should be fixed up taken from a stylesheet
// attr: name of the attribute to be clobbered, e.g. 'color' // attr: name of the attribute to be clobbered, e.g. 'color'
// index: ordinal of primary, secondary or tertiary // index: ordinal of primary, secondary or tertiary
// },
// } // }
]; ];
// CSS attributes to be fixed up // CSS attributes to be fixed up
const cssAttrs = [ this.cssAttrs = [
"color", "color",
"backgroundColor", "backgroundColor",
"borderColor", "borderColor",
"borderTopColor", "borderTopColor",
"borderBottomColor", "borderBottomColor",
"borderLeftColor", "borderLeftColor",
]; ];
const svgAttrs = [ this.svgAttrs = [
"fill", "fill",
"stroke", "stroke",
]; ];
let cached = false; // List of functions to call when the tint changes.
this.tintables = [];
// the currently loaded theme (if any)
this.theme = undefined;
// whether to force a tint (e.g. after changing theme)
this.forceTint = false;
}
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
*
* It's not possible to unregister a tintable callback. So this can only be
* used to register a static callback. If a set of tintables will change
* over time then the best bet is to register a single callback for the
* entire set.
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable(tintable) {
this.tintables.push(tintable);
}
getKeyRgb() {
return this.keyRgb;
}
tint(primaryColor, secondaryColor, tertiaryColor) {
this.currentTint[0] = primaryColor;
this.currentTint[1] = secondaryColor;
this.currentTint[2] = tertiaryColor;
this.calcCssFixups();
if (DEBUG) console.log("Tinter.tint(" + primaryColor + ", " +
secondaryColor + ", " +
tertiaryColor + ")");
if (!primaryColor) {
primaryColor = this.keyRgb[0];
secondaryColor = this.keyRgb[1];
tertiaryColor = this.keyRgb[2];
}
if (!secondaryColor) {
const x = 0.16; // average weighting factor calculated from vector green & light green
const rgb = colorToRgb(primaryColor);
rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255;
rgb[2] = x * rgb[2] + (1 - x) * 255;
secondaryColor = rgbToColor(rgb);
}
if (!tertiaryColor) {
const x = 0.19;
const rgb1 = colorToRgb(primaryColor);
const rgb2 = colorToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
tertiaryColor = rgbToColor(rgb1);
}
if (this.forceTint == false &&
this.colors[0] === primaryColor &&
this.colors[1] === secondaryColor &&
this.colors[2] === tertiaryColor) {
return;
}
this.forceTint = false;
this.colors[0] = primaryColor;
this.colors[1] = secondaryColor;
this.colors[2] = tertiaryColor;
if (DEBUG) console.log("Tinter.tint final: (" + primaryColor + ", " +
secondaryColor + ", " +
tertiaryColor + ")");
// go through manually fixing up the stylesheets.
this.applyCssFixups();
// tell all the SVGs to go fix themselves up
// we don't do this as a dispatch otherwise it will visually lag
this.tintables.forEach(function(tintable) {
tintable();
});
}
tintSvgWhite(whiteColor) {
this.currentTint[3] = whiteColor;
if (!whiteColor) {
whiteColor = this.colors[3];
}
if (this.colors[3] === whiteColor) {
return;
}
this.colors[3] = whiteColor;
this.tintables.forEach(function(tintable) {
tintable();
});
}
setTheme(theme) {
console.trace("setTheme " + theme);
this.theme = theme;
// update keyRgb from the current theme CSS itself, if it defines it
if (document.getElementById('mx_theme_accentColor')) {
this.keyRgb[0] = window.getComputedStyle(
document.getElementById('mx_theme_accentColor')
).color;
}
if (document.getElementById('mx_theme_secondaryAccentColor')) {
this.keyRgb[1] = window.getComputedStyle(
document.getElementById('mx_theme_secondaryAccentColor')
).color;
}
if (document.getElementById('mx_theme_tertiaryAccentColor')) {
this.keyRgb[2] = window.getComputedStyle(
document.getElementById('mx_theme_tertiaryAccentColor')
).color;
}
this.calcCssFixups();
this.forceTint = true;
this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
if (theme === 'dark') {
// abuse the tinter to change all the SVG's #fff to #2d2d2d
// XXX: obviously this shouldn't be hardcoded here.
this.tintSvgWhite('#2d2d2d');
} else {
this.tintSvgWhite('#ffffff');
}
}
calcCssFixups() {
// cache our fixups
if (this.cssFixups[this.theme]) return;
if (DEBUG) console.debug("calcCssFixups start for " + this.theme + " (checking " +
document.styleSheets.length +
" stylesheets)");
this.cssFixups[this.theme] = [];
function calcCssFixups() {
if (DEBUG) console.log("calcSvgFixups start");
for (let i = 0; i < document.styleSheets.length; i++) { for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i]; const ss = document.styleSheets[i];
if (!ss) continue; // well done safari >:( if (!ss) continue; // well done safari >:(
@ -100,18 +290,29 @@ function calcCssFixups() {
// Iterating through the CSS looking for matches to hack on feels // Iterating through the CSS looking for matches to hack on feels
// pretty horrible anyway. And what if the application skin doesn't use // pretty horrible anyway. And what if the application skin doesn't use
// Vector Green as its primary color? // Vector Green as its primary color?
// --richvdh
if (ss.href && !ss.href.match(/\/bundle.*\.css$/)) continue; // Yes, tinting assumes that you are using the Riot skin for now.
// The right solution will be to move the CSS over to react-sdk.
// And yes, the default assets for the base skin might as well use
// Vector Green as any other colour.
// --matthew
if (ss.href && !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
if (ss.disabled) continue;
if (!ss.cssRules) continue; if (!ss.cssRules) continue;
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
for (let j = 0; j < ss.cssRules.length; j++) { for (let j = 0; j < ss.cssRules.length; j++) {
const rule = ss.cssRules[j]; const rule = ss.cssRules[j];
if (!rule.style) continue; if (!rule.style) continue;
for (let k = 0; k < cssAttrs.length; k++) { if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
const attr = cssAttrs[k]; for (let k = 0; k < this.cssAttrs.length; k++) {
for (let l = 0; l < keyRgb.length; l++) { const attr = this.cssAttrs[k];
if (rule.style[attr] === keyRgb[l]) { for (let l = 0; l < this.keyRgb.length; l++) {
cssFixups.push({ if (rule.style[attr] === this.keyRgb[l]) {
this.cssFixups[this.theme].push({
style: rule.style, style: rule.style,
attr: attr, attr: attr,
index: l, index: l,
@ -121,125 +322,34 @@ function calcCssFixups() {
} }
} }
} }
if (DEBUG) console.log("calcSvgFixups end"); if (DEBUG) console.log("calcCssFixups end (" +
} this.cssFixups[this.theme].length +
" fixups)");
}
function applyCssFixups() { applyCssFixups() {
if (DEBUG) console.log("applyCssFixups start"); if (DEBUG) console.log("applyCssFixups start (" +
for (let i = 0; i < cssFixups.length; i++) { this.cssFixups[this.theme].length +
const cssFixup = cssFixups[i]; " fixups)");
cssFixup.style[cssFixup.attr] = colors[cssFixup.index]; for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
const cssFixup = this.cssFixups[this.theme][i];
try {
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
}
catch (e) {
// Firefox Quantum explodes if you manually edit the CSS in the
// inspector and then try to do a tint, as apparently all the
// fixups are then stale.
console.error("Failed to apply cssFixup in Tinter! ", e.name);
}
} }
if (DEBUG) console.log("applyCssFixups end"); if (DEBUG) console.log("applyCssFixups end");
}
function hexToRgb(color) {
if (color[0] === '#') color = color.slice(1);
if (color.length === 3) {
color = color[0] + color[0] +
color[1] + color[1] +
color[2] + color[2];
} }
const val = parseInt(color, 16);
const r = (val >> 16) & 255;
const g = (val >> 8) & 255;
const b = val & 255;
return [r, g, b];
}
function rgbToHex(rgb) {
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
return '#' + (0x1000000 + val).toString(16).slice(1);
}
// List of functions to call when the tint changes.
const tintables = [];
module.exports = {
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
*
* It's not possible to unregister a tintable callback. So this can only be
* used to register a static callback. If a set of tintables will change
* over time then the best bet is to register a single callback for the
* entire set.
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable: function(tintable) {
tintables.push(tintable);
},
tint: function(primaryColor, secondaryColor, tertiaryColor) {
if (!cached) {
calcCssFixups();
cached = true;
}
if (!primaryColor) {
primaryColor = "#76CFA6"; // Vector green
secondaryColor = "#EAF5F0"; // Vector light green
}
if (!secondaryColor) {
const x = 0.16; // average weighting factor calculated from vector green & light green
const rgb = hexToRgb(primaryColor);
rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255;
rgb[2] = x * rgb[2] + (1 - x) * 255;
secondaryColor = rgbToHex(rgb);
}
if (!tertiaryColor) {
const x = 0.19;
const rgb1 = hexToRgb(primaryColor);
const rgb2 = hexToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
tertiaryColor = rgbToHex(rgb1);
}
if (colors[0] === primaryColor &&
colors[1] === secondaryColor &&
colors[2] === tertiaryColor) {
return;
}
colors[0] = primaryColor;
colors[1] = secondaryColor;
colors[2] = tertiaryColor;
if (DEBUG) console.log("Tinter.tint");
// go through manually fixing up the stylesheets.
applyCssFixups();
// tell all the SVGs to go fix themselves up
// we don't do this as a dispatch otherwise it will visually lag
tintables.forEach(function(tintable) {
tintable();
});
},
tintSvgWhite: function(whiteColor) {
if (!whiteColor) {
whiteColor = colors[3];
}
if (colors[3] === whiteColor) {
return;
}
colors[3] = whiteColor;
tintables.forEach(function(tintable) {
tintable();
});
},
// XXX: we could just move this all into TintableSvg, but as it's so similar // XXX: we could just move this all into TintableSvg, but as it's so similar
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now. // keeping it here for now.
calcSvgFixups: function(svgs) { calcSvgFixups(svgs) {
// go through manually fixing up SVG colours. // go through manually fixing up SVG colours.
// we could do this by stylesheets, but keeping the stylesheets // we could do this by stylesheets, but keeping the stylesheets
// updated would be a PITA, so just brute-force search for the // updated would be a PITA, so just brute-force search for the
@ -265,10 +375,12 @@ module.exports = {
const tags = svgDoc.getElementsByTagName("*"); const tags = svgDoc.getElementsByTagName("*");
for (let j = 0; j < tags.length; j++) { for (let j = 0; j < tags.length; j++) {
const tag = tags[j]; const tag = tags[j];
for (let k = 0; k < svgAttrs.length; k++) { for (let k = 0; k < this.svgAttrs.length; k++) {
const attr = svgAttrs[k]; const attr = this.svgAttrs[k];
for (let l = 0; l < keyHex.length; l++) { for (let l = 0; l < this.keyHex.length; l++) {
if (tag.getAttribute(attr) && tag.getAttribute(attr).toUpperCase() === keyHex[l]) { if (tag.getAttribute(attr) &&
tag.getAttribute(attr).toUpperCase() === this.keyHex[l])
{
fixups.push({ fixups.push({
node: tag, node: tag,
attr: attr, attr: attr,
@ -282,14 +394,19 @@ module.exports = {
if (DEBUG) console.log("calcSvgFixups end"); if (DEBUG) console.log("calcSvgFixups end");
return fixups; return fixups;
}, }
applySvgFixups: function(fixups) { applySvgFixups(fixups) {
if (DEBUG) console.log("applySvgFixups start for " + fixups); if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (let i = 0; i < fixups.length; i++) { for (let i = 0; i < fixups.length; i++) {
const svgFixup = fixups[i]; const svgFixup = fixups[i];
svgFixup.node.setAttribute(svgFixup.attr, colors[svgFixup.index]); svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
} }
if (DEBUG) console.log("applySvgFixups end"); if (DEBUG) console.log("applySvgFixups end");
}, }
}; }
if (global.singletonTinter === undefined) {
global.singletonTinter = new Tinter();
}
export default global.singletonTinter;

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
const MatrixClientPeg = require('./MatrixClientPeg'); const MatrixClientPeg = require('./MatrixClientPeg');
import UserSettingsStore from './UserSettingsStore';
import shouldHideEvent from './shouldHideEvent'; import shouldHideEvent from './shouldHideEvent';
const sdk = require('./index'); const sdk = require('./index');
@ -64,7 +63,6 @@ module.exports = {
// we have and the read receipt. We could fetch more history to try & find out, // we have and the read receipt. We could fetch more history to try & find out,
// but currently we just guess. // but currently we just guess.
const syncedSettings = UserSettingsStore.getSyncedSettings();
// Loop through messages, starting with the most recent... // Loop through messages, starting with the most recent...
for (let i = room.timeline.length - 1; i >= 0; --i) { for (let i = room.timeline.length - 1; i >= 0; --i) {
const ev = room.timeline[i]; const ev = room.timeline[i];
@ -74,7 +72,7 @@ module.exports = {
// that counts and we can stop looking because the user's read // that counts and we can stop looking because the user's read
// this and everything before. // this and everything before.
return false; return false;
} else if (!shouldHideEvent(ev, syncedSettings) && this.eventTriggersUnreadCount(ev)) { } else if (!shouldHideEvent(ev) && this.eventTriggersUnreadCount(ev)) {
// We've found a message that counts before we hit // We've found a message that counts before we hit
// the read marker, so this room is definitely unread. // the read marker, so this room is definitely unread.
return true; return true;

View file

@ -17,54 +17,11 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import MatrixClientPeg from './MatrixClientPeg'; import MatrixClientPeg from './MatrixClientPeg';
import Notifier from './Notifier';
import { _t, _td } from './languageHandler';
import SdkConfig from './SdkConfig';
/* /*
* TODO: Make things use this. This is all WIP - see UserSettings.js for usage. * TODO: Make things use this. This is all WIP - see UserSettings.js for usage.
*/ */
const FEATURES = [
{
id: 'feature_pinning',
name: _td("Message Pinning"),
},
];
export default { export default {
getLabsFeatures() {
const featuresConfig = SdkConfig.get()['features'] || {};
// The old flag: honoured for backwards compatibility
const enableLabs = SdkConfig.get()['enableLabs'];
let labsFeatures;
if (enableLabs) {
labsFeatures = FEATURES;
} else {
labsFeatures = FEATURES.filter((f) => {
const sdkConfigValue = featuresConfig[f.id];
if (sdkConfigValue === 'labs') {
return true;
}
});
}
return labsFeatures.map((f) => {
return f.id;
});
},
translatedNameForFeature(featureId) {
const feature = FEATURES.filter((f) => {
return f.id === featureId;
})[0];
if (feature === undefined) return null;
return _t(feature.name);
},
loadProfileInfo: function() { loadProfileInfo: function() {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
return cli.getProfileInfo(cli.credentials.userId); return cli.getProfileInfo(cli.credentials.userId);
@ -87,36 +44,6 @@ export default {
// TODO // TODO
}, },
getEnableNotifications: function() {
return Notifier.isEnabled();
},
setEnableNotifications: function(enable) {
if (!Notifier.supportsDesktopNotifications()) {
return;
}
Notifier.setEnabled(enable);
},
getEnableNotificationBody: function() {
return Notifier.isBodyEnabled();
},
setEnableNotificationBody: function(enable) {
if (!Notifier.supportsDesktopNotifications()) {
return;
}
Notifier.setBodyEnabled(enable);
},
getEnableAudioNotifications: function() {
return Notifier.isAudioEnabled();
},
setEnableAudioNotifications: function(enable) {
Notifier.setAudioEnabled(enable);
},
changePassword: function(oldPassword, newPassword) { changePassword: function(oldPassword, newPassword) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -163,83 +90,4 @@ export default {
append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address append: true, // We always append for email pushers since we don't want to stop other accounts notifying to the same email address
}); });
}, },
getUrlPreviewsDisabled: function() {
const event = MatrixClientPeg.get().getAccountData('org.matrix.preview_urls');
return (event && event.getContent().disable);
},
setUrlPreviewsDisabled: function(disabled) {
// FIXME: handle errors
return MatrixClientPeg.get().setAccountData('org.matrix.preview_urls', {
disable: disabled,
});
},
getSyncedSettings: function() {
const event = MatrixClientPeg.get().getAccountData('im.vector.web.settings');
return event ? event.getContent() : {};
},
getSyncedSetting: function(type, defaultValue = null) {
const settings = this.getSyncedSettings();
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setSyncedSetting: function(type, value) {
const settings = this.getSyncedSettings();
settings[type] = value;
// FIXME: handle errors
return MatrixClientPeg.get().setAccountData('im.vector.web.settings', settings);
},
getLocalSettings: function() {
const localSettingsString = localStorage.getItem('mx_local_settings') || '{}';
return JSON.parse(localSettingsString);
},
getLocalSetting: function(type, defaultValue = null) {
const settings = this.getLocalSettings();
return settings.hasOwnProperty(type) ? settings[type] : defaultValue;
},
setLocalSetting: function(type, value) {
const settings = this.getLocalSettings();
settings[type] = value;
// FIXME: handle errors
localStorage.setItem('mx_local_settings', JSON.stringify(settings));
},
isFeatureEnabled: function(featureId: string): boolean {
const featuresConfig = SdkConfig.get()['features'];
// The old flag: honoured for backwards compatibility
const enableLabs = SdkConfig.get()['enableLabs'];
let sdkConfigValue = enableLabs ? 'labs' : 'disable';
if (featuresConfig && featuresConfig[featureId] !== undefined) {
sdkConfigValue = featuresConfig[featureId];
}
if (sdkConfigValue === 'enable') {
return true;
} else if (sdkConfigValue === 'disable') {
return false;
} else if (sdkConfigValue === 'labs') {
if (!MatrixClientPeg.get().isGuest()) {
// Make it explicit that guests get the defaults (although they shouldn't
// have been able to ever toggle the flags anyway)
const userValue = localStorage.getItem(`mx_labs_feature_${featureId}`);
return userValue === 'true';
}
return false;
} else {
console.warn(`Unknown features config for ${featureId}: ${sdkConfigValue}`);
return false;
}
},
setFeatureEnabled: function(featureId: string, enabled: boolean) {
localStorage.setItem(`mx_labs_feature_${featureId}`, enabled);
},
}; };

View file

@ -26,7 +26,7 @@ import {PillCompletion} from './Components';
import type {SelectionRange, Completion} from './Autocompleter'; import type {SelectionRange, Completion} from './Autocompleter';
import _uniq from 'lodash/uniq'; import _uniq from 'lodash/uniq';
import _sortBy from 'lodash/sortBy'; import _sortBy from 'lodash/sortBy';
import UserSettingsStore from '../UserSettingsStore'; import SettingsStore from "../settings/SettingsStore";
import EmojiData from '../stripped-emoji.json'; import EmojiData from '../stripped-emoji.json';
@ -96,7 +96,7 @@ export default class EmojiProvider extends AutocompleteProvider {
} }
async getCompletions(query: string, selection: SelectionRange) { async getCompletions(query: string, selection: SelectionRange) {
if (UserSettingsStore.getSyncedSetting("MessageComposerInput.dontSuggestEmoji")) { if (SettingsStore.getValue("MessageComposerInput.dontSuggestEmoji")) {
return []; // don't give any suggestions if the user doesn't want them return []; // don't give any suggestions if the user doesn't want them
} }

View file

@ -33,6 +33,7 @@ module.exports = {
menuHeight: React.PropTypes.number, menuHeight: React.PropTypes.number,
chevronOffset: React.PropTypes.number, chevronOffset: React.PropTypes.number,
menuColour: React.PropTypes.string, menuColour: React.PropTypes.string,
chevronFace: React.PropTypes.string, // top, bottom, left, right
}, },
getOrCreateContainer: function() { getOrCreateContainer: function() {
@ -58,12 +59,30 @@ module.exports = {
} }
}; };
const position = { const position = {};
top: props.top, let chevronFace = null;
};
if (props.top) {
position.top = props.top;
} else {
position.bottom = props.bottom;
}
if (props.left) {
position.left = props.left;
chevronFace = 'left';
} else {
position.right = props.right;
chevronFace = 'right';
}
const chevronOffset = {}; const chevronOffset = {};
if (props.chevronOffset) { if (props.chevronFace) {
chevronFace = props.chevronFace;
}
if (chevronFace === 'top' || chevronFace === 'bottom') {
chevronOffset.left = props.chevronOffset;
} else {
chevronOffset.top = props.chevronOffset; chevronOffset.top = props.chevronOffset;
} }
@ -74,28 +93,27 @@ module.exports = {
.mx_ContextualMenu_chevron_left:after { .mx_ContextualMenu_chevron_left:after {
border-right-color: ${props.menuColour}; border-right-color: ${props.menuColour};
} }
.mx_ContextualMenu_chevron_right:after { .mx_ContextualMenu_chevron_right:after {
border-left-color: ${props.menuColour}; border-left-color: ${props.menuColour};
} }
.mx_ContextualMenu_chevron_top:after {
border-left-color: ${props.menuColour};
}
.mx_ContextualMenu_chevron_bottom:after {
border-left-color: ${props.menuColour};
}
`; `;
} }
let chevron = null; const chevron = <div style={chevronOffset} className={"mx_ContextualMenu_chevron_" + chevronFace}></div>;
if (props.left) {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_left"></div>;
position.left = props.left;
} else {
chevron = <div style={chevronOffset} className="mx_ContextualMenu_chevron_right"></div>;
position.right = props.right;
}
const className = 'mx_ContextualMenu_wrapper'; const className = 'mx_ContextualMenu_wrapper';
const menuClasses = classNames({ const menuClasses = classNames({
'mx_ContextualMenu': true, 'mx_ContextualMenu': true,
'mx_ContextualMenu_left': props.left, 'mx_ContextualMenu_left': chevronFace === 'left',
'mx_ContextualMenu_right': !props.left, 'mx_ContextualMenu_right': chevronFace === 'right',
'mx_ContextualMenu_top': chevronFace === 'top',
'mx_ContextualMenu_bottom': chevronFace === 'bottom',
}); });
const menuStyle = {}; const menuStyle = {};

View file

@ -19,7 +19,6 @@ limitations under the License.
import * as Matrix from 'matrix-js-sdk'; import * as Matrix from 'matrix-js-sdk';
import React from 'react'; import React from 'react';
import UserSettingsStore from '../../UserSettingsStore';
import KeyCode from '../../KeyCode'; import KeyCode from '../../KeyCode';
import Notifier from '../../Notifier'; import Notifier from '../../Notifier';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
@ -28,6 +27,7 @@ import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import sessionStore from '../../stores/SessionStore'; import sessionStore from '../../stores/SessionStore';
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
/** /**
* This is what our MatrixChat shows when we are logged in. The precise view is * This is what our MatrixChat shows when we are logged in. The precise view is
@ -74,7 +74,7 @@ export default React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
// use compact timeline view // use compact timeline view
useCompactLayout: UserSettingsStore.getSyncedSetting('useCompactLayout'), useCompactLayout: SettingsStore.getValue('useCompactLayout'),
}; };
}, },

View file

@ -22,7 +22,6 @@ import React from 'react';
import Matrix from "matrix-js-sdk"; import Matrix from "matrix-js-sdk";
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
import UserSettingsStore from '../../UserSettingsStore';
import MatrixClientPeg from "../../MatrixClientPeg"; import MatrixClientPeg from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
@ -43,6 +42,7 @@ import PageTypes from '../../PageTypes';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import KeyRequestHandler from '../../KeyRequestHandler'; import KeyRequestHandler from '../../KeyRequestHandler';
import { _t, getCurrentLanguage } from '../../languageHandler'; import { _t, getCurrentLanguage } from '../../languageHandler';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
const VIEWS = { const VIEWS = {
@ -223,7 +223,7 @@ module.exports = React.createClass({
componentWillMount: function() { componentWillMount: function() {
SdkConfig.put(this.props.config); SdkConfig.put(this.props.config);
if (!UserSettingsStore.getLocalSetting('analyticsOptOut', false)) Analytics.enable(); if (!SettingsStore.getValue("analyticsOptOut")) Analytics.enable();
// Used by _viewRoom before getting state from sync // Used by _viewRoom before getting state from sync
this.firstSyncComplete = false; this.firstSyncComplete = false;
@ -286,6 +286,11 @@ module.exports = React.createClass({
this._windowWidth = 10000; this._windowWidth = 10000;
this.handleResize(); this.handleResize();
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
// check we have the right tint applied for this theme.
// N.B. we don't call the whole of setTheme() here as we may be
// racing with the theme CSS download finishing from index.js
Tinter.tint();
}, },
componentDidMount: function() { componentDidMount: function() {
@ -583,6 +588,9 @@ module.exports = React.createClass({
this._onWillStartClient(); this._onWillStartClient();
}); });
break; break;
case 'client_started':
this._onClientStarted();
break;
case 'new_version': case 'new_version':
this.onVersion( this.onVersion(
payload.currentVersion, payload.newVersion, payload.currentVersion, payload.newVersion,
@ -880,7 +888,7 @@ module.exports = React.createClass({
*/ */
_onSetTheme: function(theme) { _onSetTheme: function(theme) {
if (!theme) { if (!theme) {
theme = 'light'; theme = this.props.config.default_theme || 'light';
} }
// look for the stylesheet elements. // look for the stylesheet elements.
@ -903,18 +911,49 @@ module.exports = React.createClass({
// disable all of them first, then enable the one we want. Chrome only // disable all of them first, then enable the one we want. Chrome only
// bothers to do an update on a true->false transition, so this ensures // bothers to do an update on a true->false transition, so this ensures
// that we get exactly one update, at the right time. // that we get exactly one update, at the right time.
//
// ^ This comment was true when we used to use alternative stylesheets
// for the CSS. Nowadays we just set them all as disabled in index.html
// and enable them as needed. It might be cleaner to disable them all
// at the same time to prevent loading two themes simultaneously and
// having them interact badly... but this causes a flash of unstyled app
// which is even uglier. So we don't.
Object.values(styleElements).forEach((a) => {
a.disabled = true;
});
styleElements[theme].disabled = false; styleElements[theme].disabled = false;
if (theme === 'dark') { const switchTheme = function() {
// abuse the tinter to change all the SVG's #fff to #2d2d2d // we re-enable our theme here just in case we raced with another
// XXX: obviously this shouldn't be hardcoded here. // theme set request as per https://github.com/vector-im/riot-web/issues/5601.
Tinter.tintSvgWhite('#2d2d2d'); // We could alternatively lock or similar to stop the race, but
} else { // this is probably good enough for now.
Tinter.tintSvgWhite('#ffffff'); styleElements[theme].disabled = false;
Object.values(styleElements).forEach((a) => {
if (a == styleElements[theme]) return;
a.disabled = true;
});
Tinter.setTheme(theme);
};
// turns out that Firefox preloads the CSS for link elements with
// the disabled attribute, but Chrome doesn't.
let cssLoaded = false;
styleElements[theme].onload = () => {
switchTheme();
};
for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i];
if (ss && ss.href === styleElements[theme].href) {
cssLoaded = true;
break;
}
}
if (cssLoaded) {
styleElements[theme].onload = undefined;
switchTheme();
} }
}, },
@ -1085,6 +1124,34 @@ module.exports = React.createClass({
cli.on("crypto.roomKeyRequestCancellation", (req) => { cli.on("crypto.roomKeyRequestCancellation", (req) => {
krh.handleKeyRequestCancellation(req); krh.handleKeyRequestCancellation(req);
}); });
cli.on("Room", (room) => {
if (MatrixClientPeg.get().isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(
SettingLevel.ROOM_DEVICE,
"blacklistUnverifiedDevices",
room.roomId,
/*explicit=*/true,
);
room.setBlacklistUnverifiedDevices(blacklistEnabled);
}
});
},
/**
* Called shortly after the matrix client has started. Useful for
* setting up anything that requires the client to be started.
* @private
*/
_onClientStarted: function() {
const cli = MatrixClientPeg.get();
if (cli.isCryptoEnabled()) {
const blacklistEnabled = SettingsStore.getValueAt(
SettingLevel.DEVICE,
"blacklistUnverifiedDevices",
);
cli.setGlobalBlacklistUnverifiedDevices(blacklistEnabled);
}
}, },
showScreen: function(screen, params) { showScreen: function(screen, params) {

View file

@ -17,7 +17,6 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import UserSettingsStore from '../../UserSettingsStore';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import dis from "../../dispatcher"; import dis from "../../dispatcher";
import sdk from '../../index'; import sdk from '../../index';
@ -110,8 +109,6 @@ module.exports = React.createClass({
// Velocity requires // Velocity requires
this._readMarkerGhostNode = null; this._readMarkerGhostNode = null;
this._syncedSettings = UserSettingsStore.getSyncedSettings();
this._isMounted = true; this._isMounted = true;
}, },
@ -251,7 +248,7 @@ module.exports = React.createClass({
// Always show highlighted event // Always show highlighted event
if (this.props.highlightedEventId === mxEv.getId()) return true; if (this.props.highlightedEventId === mxEv.getId()) return true;
return !shouldHideEvent(mxEv, this._syncedSettings); return !shouldHideEvent(mxEv);
}, },
_getEventTiles: function() { _getEventTiles: function() {

View file

@ -28,7 +28,6 @@ import Promise from 'bluebird';
const classNames = require("classnames"); const classNames = require("classnames");
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
const UserSettingsStore = require('../../UserSettingsStore');
const MatrixClientPeg = require("../../MatrixClientPeg"); const MatrixClientPeg = require("../../MatrixClientPeg");
const ContentMessages = require("../../ContentMessages"); const ContentMessages = require("../../ContentMessages");
const Modal = require("../../Modal"); const Modal = require("../../Modal");
@ -44,6 +43,7 @@ import KeyCode from '../../KeyCode';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore'; import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import SettingsStore from "../../settings/SettingsStore";
const DEBUG = false; const DEBUG = false;
let debuglog = function() {}; let debuglog = function() {};
@ -146,8 +146,6 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership); MatrixClientPeg.get().on("RoomMember.membership", this.onRoomMemberMembership);
MatrixClientPeg.get().on("accountData", this.onAccountData); MatrixClientPeg.get().on("accountData", this.onAccountData);
this._syncedSettings = UserSettingsStore.getSyncedSettings();
// Start listening for RoomViewStore updates // Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true); this._onRoomViewStoreUpdate(true);
@ -533,7 +531,7 @@ module.exports = React.createClass({
// update unread count when scrolled up // update unread count when scrolled up
if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { if (!this.state.searchResults && this.state.atEndOfLiveTimeline) {
// no change // no change
} else if (!shouldHideEvent(ev, this._syncedSettings)) { } else if (!shouldHideEvent(ev)) {
this.setState((state, props) => { this.setState((state, props) => {
return {numUnreadMessages: state.numUnreadMessages + 1}; return {numUnreadMessages: state.numUnreadMessages + 1};
}); });
@ -607,38 +605,8 @@ module.exports = React.createClass({
}, },
_updatePreviewUrlVisibility: function(room) { _updatePreviewUrlVisibility: function(room) {
// console.log("_updatePreviewUrlVisibility");
// check our per-room overrides
const roomPreviewUrls = room.getAccountData("org.matrix.room.preview_urls");
if (roomPreviewUrls && roomPreviewUrls.getContent().disable !== undefined) {
this.setState({ this.setState({
showUrlPreview: !roomPreviewUrls.getContent().disable, showUrlPreview: SettingsStore.getValue("urlPreviewsEnabled", room.roomId),
});
return;
}
// check our global disable override
const userRoomPreviewUrls = MatrixClientPeg.get().getAccountData("org.matrix.preview_urls");
if (userRoomPreviewUrls && userRoomPreviewUrls.getContent().disable) {
this.setState({
showUrlPreview: false,
});
return;
}
// check the room state event
const roomStatePreviewUrls = room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
if (roomStatePreviewUrls && roomStatePreviewUrls.getContent().disable) {
this.setState({
showUrlPreview: false,
});
return;
}
// otherwise, we assume they're on.
this.setState({
showUrlPreview: true,
}); });
}, },
@ -657,12 +625,7 @@ module.exports = React.createClass({
const room = this.state.room; const room = this.state.room;
if (!room) return; if (!room) return;
const color_scheme_event = room.getAccountData("org.matrix.room.color_scheme"); const color_scheme = SettingsStore.getValue("roomColor", room.room_id);
let color_scheme = {};
if (color_scheme_event) {
color_scheme = color_scheme_event.getContent();
// XXX: we should validate the event
}
console.log("Tinter.tint from updateTint"); console.log("Tinter.tint from updateTint");
Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color); Tinter.tint(color_scheme.primary_color, color_scheme.secondary_color);
}, },
@ -1722,7 +1685,7 @@ module.exports = React.createClass({
const messagePanel = ( const messagePanel = (
<TimelinePanel ref={this._gatherTimelinePanelRef} <TimelinePanel ref={this._gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
showReadReceipts={!UserSettingsStore.getSyncedSetting('hideReadReceipts', false)} showReadReceipts={!SettingsStore.getValue('hideReadReceipts')}
manageReadReceipts={!this.state.isPeeking} manageReadReceipts={!this.state.isPeeking}
manageReadMarkers={!this.state.isPeeking} manageReadMarkers={!this.state.isPeeking}
hidden={hideMessagePanel} hidden={hideMessagePanel}

View file

@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import SettingsStore from "../../settings/SettingsStore";
const React = require('react'); const React = require('react');
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
import Promise from 'bluebird'; import Promise from 'bluebird';
@ -30,7 +32,6 @@ const ObjectUtils = require('../../ObjectUtils');
const Modal = require("../../Modal"); const Modal = require("../../Modal");
const UserActivity = require("../../UserActivity"); const UserActivity = require("../../UserActivity");
const KeyCode = require('../../KeyCode'); const KeyCode = require('../../KeyCode');
import UserSettingsStore from '../../UserSettingsStore';
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -129,8 +130,6 @@ var TimelinePanel = React.createClass({
} }
} }
const syncedSettings = UserSettingsStore.getSyncedSettings();
return { return {
events: [], events: [],
timelineLoading: true, // track whether our room timeline is loading timelineLoading: true, // track whether our room timeline is loading
@ -175,10 +174,10 @@ var TimelinePanel = React.createClass({
clientSyncState: MatrixClientPeg.get().getSyncState(), clientSyncState: MatrixClientPeg.get().getSyncState(),
// should the event tiles have twelve hour times // should the event tiles have twelve hour times
isTwelveHour: syncedSettings.showTwelveHourTimestamps, isTwelveHour: SettingsStore.getValue("showTwelveHourTimestamps"),
// always show timestamps on event tiles? // always show timestamps on event tiles?
alwaysShowTimestamps: syncedSettings.alwaysShowTimestamps, alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
}; };
}, },

View file

@ -15,6 +15,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
const React = require('react'); const React = require('react');
const ReactDOM = require('react-dom'); const ReactDOM = require('react-dom');
const sdk = require('../../index'); const sdk = require('../../index');
@ -56,133 +58,64 @@ const gHVersionLabel = function(repo, token='') {
return <a target="_blank" rel="noopener" href={url}>{ token }</a>; return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
}; };
// Enumerate some simple 'flip a bit' UI settings (if any). // Enumerate some simple 'flip a bit' UI settings (if any). The strings provided here
// 'id' gives the key name in the im.vector.web.settings account data event // must be settings defined in SettingsStore.
// 'label' is how we describe it in the UI. const SIMPLE_SETTINGS = [
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json, { id: "urlPreviewsEnabled" },
// since they will be translated when rendered. { id: "autoplayGifsAndVideos" },
const SETTINGS_LABELS = [ { id: "hideReadReceipts" },
{ { id: "dontSendTypingNotifications" },
id: 'autoplayGifsAndVideos', { id: "alwaysShowTimestamps" },
label: _td('Autoplay GIFs and videos'), { id: "showTwelveHourTimestamps" },
}, { id: "hideJoinLeaves" },
{ { id: "hideAvatarChanges" },
id: 'hideReadReceipts', { id: "hideDisplaynameChanges" },
label: _td('Hide read receipts'), { id: "useCompactLayout" },
}, { id: "hideRedactions" },
{ { id: "enableSyntaxHighlightLanguageDetection" },
id: 'dontSendTypingNotifications', { id: "MessageComposerInput.autoReplaceEmoji" },
label: _td("Don't send typing notifications"), { id: "MessageComposerInput.dontSuggestEmoji" },
}, { id: "Pill.shouldHidePillAvatar" },
{ { id: "TextualBody.disableBigEmoji" },
id: 'alwaysShowTimestamps', { id: "VideoView.flipVideoHorizontally" },
label: _td('Always show message timestamps'),
},
{
id: 'showTwelveHourTimestamps',
label: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
},
{
id: 'hideJoinLeaves',
label: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
},
{
id: 'hideAvatarDisplaynameChanges',
label: _td('Hide avatar and display name changes'),
},
{
id: 'useCompactLayout',
label: _td('Use compact timeline layout'),
},
{
id: 'hideRedactions',
label: _td('Hide removed messages'),
},
{
id: 'enableSyntaxHighlightLanguageDetection',
label: _td('Enable automatic language detection for syntax highlighting'),
},
{
id: 'MessageComposerInput.autoReplaceEmoji',
label: _td('Automatically replace plain text Emoji'),
},
{
id: 'MessageComposerInput.dontSuggestEmoji',
label: _td('Disable Emoji suggestions while typing'),
},
{
id: 'Pill.shouldHidePillAvatar',
label: _td('Hide avatars in user and room mentions'),
},
{
id: 'TextualBody.disableBigEmoji',
label: _td('Disable big emoji in chat'),
},
{
id: 'VideoView.flipVideoHorizontally',
label: _td('Mirror local video feed'),
},
/*
{
id: 'useFixedWidthFont',
label: 'Use fixed width font',
},
*/
]; ];
const ANALYTICS_SETTINGS_LABELS = [ // These settings must be defined in SettingsStore
const ANALYTICS_SETTINGS = [
{ {
id: 'analyticsOptOut', id: 'analyticsOptOut',
label: _td('Opt out of analytics'),
fn: function(checked) { fn: function(checked) {
Analytics[checked ? 'disable' : 'enable'](); Analytics[checked ? 'disable' : 'enable']();
}, },
}, },
]; ];
const WEBRTC_SETTINGS_LABELS = [ // These settings must be defined in SettingsStore
{ const WEBRTC_SETTINGS = [
id: 'webRtcForceTURN', { id: 'webRtcForceTURN' },
label: _td('Disable Peer-to-Peer for 1:1 calls'),
},
]; ];
// Warning: Each "label" string below must be added to i18n/strings/en_EN.json, // These settings must be defined in SettingsStore
// since they will be translated when rendered. const CRYPTO_SETTINGS = [
const CRYPTO_SETTINGS_LABELS = [
{ {
id: 'blacklistUnverifiedDevices', id: 'blacklistUnverifiedDevices',
label: _td('Never send encrypted messages to unverified devices from this device'),
fn: function(checked) { fn: function(checked) {
MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked); MatrixClientPeg.get().setGlobalBlacklistUnverifiedDevices(checked);
}, },
}, },
// XXX: this is here for documentation; the actual setting is managed via RoomSettings
// {
// id: 'blacklistUnverifiedDevicesPerRoom'
// label: 'Never send encrypted messages to unverified devices in this room',
// }
]; ];
// Enumerate the available themes, with a nice human text label. // Enumerate the available themes, with a nice human text label.
// 'id' gives the key name in the im.vector.web.settings account data event
// 'value' is the value for that key in the event
// 'label' is how we describe it in the UI. // 'label' is how we describe it in the UI.
// 'value' is the value for the theme setting
// //
// XXX: Ideally we would have a theme manifest or something and they'd be nicely // XXX: Ideally we would have a theme manifest or something and they'd be nicely
// packaged up in a single directory, and/or located at the application layer. // packaged up in a single directory, and/or located at the application layer.
// But for now for expedience we just hardcode them here. // But for now for expedience we just hardcode them here.
const THEMES = [ const THEMES = [
{ { label: _td('Light theme'), value: 'light' },
id: 'theme', { label: _td('Dark theme'), value: 'dark' },
label: _td('Light theme'), { label: _td('Status.im theme'), value: 'status' },
value: 'light',
},
{
id: 'theme',
label: _td('Dark theme'),
value: 'dark',
},
]; ];
const IgnoredUser = React.createClass({ const IgnoredUser = React.createClass({
@ -204,7 +137,7 @@ const IgnoredUser = React.createClass({
render: function() { render: function() {
return ( return (
<li> <li>
<AccessibleButton onClick={this._onUnignoreClick} className="mx_UserSettings_button mx_UserSettings_buttonSmall"> <AccessibleButton onClick={this._onUnignoreClick} className="mx_textButton">
{ _t("Unignore") } { _t("Unignore") }
</AccessibleButton> </AccessibleButton>
{ this.props.userId } { this.props.userId }
@ -281,14 +214,6 @@ module.exports = React.createClass({
}); });
this._refreshFromServer(); this._refreshFromServer();
const syncedSettings = UserSettingsStore.getSyncedSettings();
if (!syncedSettings.theme) {
syncedSettings.theme = 'light';
}
this._syncedSettings = syncedSettings;
this._localSettings = UserSettingsStore.getLocalSettings();
if (PlatformPeg.get().isElectron()) { if (PlatformPeg.get().isElectron()) {
const {ipcRenderer} = require('electron'); const {ipcRenderer} = require('electron');
@ -359,8 +284,8 @@ module.exports = React.createClass({
if (this._unmounted) return; if (this._unmounted) return;
this.setState({ this.setState({
mediaDevices, mediaDevices,
activeAudioInput: this._localSettings['webrtc_audioinput'], activeAudioInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_audioinput'),
activeVideoInput: this._localSettings['webrtc_videoinput'], activeVideoInput: SettingsStore.getValueAt(SettingLevel.DEVICE, 'webrtc_videoinput'),
}); });
}); });
}, },
@ -492,10 +417,6 @@ module.exports = React.createClass({
dis.dispatch({action: 'password_changed'}); dis.dispatch({action: 'password_changed'});
}, },
onEnableNotificationsChange: function(event) {
UserSettingsStore.setEnableNotifications(event.target.checked);
},
_onAddEmailEditFinished: function(value, shouldSubmit) { _onAddEmailEditFinished: function(value, shouldSubmit) {
if (!shouldSubmit) return; if (!shouldSubmit) return;
this._addEmail(); this._addEmail();
@ -692,7 +613,8 @@ module.exports = React.createClass({
onLanguageChange: function(newLang) { onLanguageChange: function(newLang) {
if(this.state.language !== newLang) { if(this.state.language !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang); // We intentionally promote this to the account level at this point
SettingsStore.setValue("language", null, SettingLevel.ACCOUNT, newLang);
this.setState({ this.setState({
language: newLang, language: newLang,
}); });
@ -715,14 +637,13 @@ module.exports = React.createClass({
// TODO: this ought to be a separate component so that we don't need // TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render // to rebind the onChange each time we render
const onChange = (e) => const onChange = (e) =>
UserSettingsStore.setLocalSetting('autocompleteDelay', + e.target.value); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
return ( return (
<div> <div>
<h3>{ _t("User Interface") }</h3> <h3>{ _t("User Interface") }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ this._renderUrlPreviewSelector() } { SIMPLE_SETTINGS.map( this._renderAccountSetting ) }
{ SETTINGS_LABELS.map( this._renderSyncedSetting ) } { THEMES.map( this._renderThemeOption ) }
{ THEMES.map( this._renderThemeSelector ) }
<table> <table>
<tbody> <tbody>
<tr> <tr>
@ -730,7 +651,7 @@ module.exports = React.createClass({
<td> <td>
<input <input
type="number" type="number"
defaultValue={UserSettingsStore.getLocalSetting('autocompleteDelay', 200)} defaultValue={SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay")}
onChange={onChange} onChange={onChange}
/> />
</td> </td>
@ -743,69 +664,31 @@ module.exports = React.createClass({
); );
}, },
_renderUrlPreviewSelector: function() { _renderAccountSetting: function(setting) {
return <div className="mx_UserSettings_toggle"> const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
<input id="urlPreviewsDisabled" return (
type="checkbox" <div className="mx_UserSettings_toggle" key={setting.id}>
defaultChecked={UserSettingsStore.getUrlPreviewsDisabled()} <SettingsFlag name={setting.id}
onChange={this._onPreviewsDisabledChanged} label={setting.label}
/> level={SettingLevel.ACCOUNT}
<label htmlFor="urlPreviewsDisabled"> onChange={setting.fn} />
{ _t("Disable inline URL previews by default") } </div>
</label> );
</div>;
}, },
_onPreviewsDisabledChanged: function(e) { _renderThemeOption: function(setting) {
UserSettingsStore.setUrlPreviewsDisabled(e.target.checked); const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
}, const onChange = (v) => dis.dispatch({action: 'set_theme', value: setting.value});
return (
_renderSyncedSetting: function(setting) { <div className="mx_UserSettings_toggle" key={setting.id + '_' + setting.value}>
// TODO: this ought to be a separate component so that we don't need <SettingsFlag name="theme"
// to rebind the onChange each time we render label={setting.label}
level={SettingLevel.ACCOUNT}
const onChange = (e) => {
UserSettingsStore.setSyncedSetting(setting.id, e.target.checked);
if (setting.fn) setting.fn(e.target.checked);
};
return <div className="mx_UserSettings_toggle" key={setting.id}>
<input id={setting.id}
type="checkbox"
defaultChecked={this._syncedSettings[setting.id]}
onChange={onChange} onChange={onChange}
/> group="theme"
<label htmlFor={setting.id}> value={setting.value} />
{ _t(setting.label) } </div>
</label> );
</div>;
},
_renderThemeSelector: function(setting) {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = (e) => {
if (e.target.checked) {
this._syncedSettings[setting.id] = setting.value;
UserSettingsStore.setSyncedSetting(setting.id, setting.value);
}
dis.dispatch({
action: 'set_theme',
value: setting.value,
});
};
return <div className="mx_UserSettings_toggle" key={setting.id + "_" + setting.value}>
<input id={setting.id + "_" + setting.value}
type="radio"
name={setting.id}
value={setting.value}
checked={this._syncedSettings[setting.id] === setting.value}
onChange={onChange}
/>
<label htmlFor={setting.id + "_" + setting.value}>
{ _t(setting.label) }
</label>
</div>;
}, },
_renderCryptoInfo: function() { _renderCryptoInfo: function() {
@ -847,7 +730,7 @@ module.exports = React.createClass({
{ importExportButtons } { importExportButtons }
</div> </div>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ CRYPTO_SETTINGS_LABELS.map( this._renderLocalSetting ) } { CRYPTO_SETTINGS.map( this._renderDeviceSetting ) }
</div> </div>
</div> </div>
); );
@ -873,24 +756,16 @@ module.exports = React.createClass({
} else return (<div />); } else return (<div />);
}, },
_renderLocalSetting: function(setting) { _renderDeviceSetting: function(setting) {
// TODO: this ought to be a separate component so that we don't need const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
// to rebind the onChange each time we render return (
const onChange = (e) => { <div className="mx_UserSettings_toggle" key={setting.id}>
UserSettingsStore.setLocalSetting(setting.id, e.target.checked); <SettingsFlag name={setting.id}
if (setting.fn) setting.fn(e.target.checked); label={setting.label}
}; level={SettingLevel.DEVICE}
onChange={setting.fn} />
return <div className="mx_UserSettings_toggle" key={setting.id}> </div>
<input id={setting.id} );
type="checkbox"
defaultChecked={this._localSettings[setting.id]}
onChange={onChange}
/>
<label htmlFor={setting.id}>
{ _t(setting.label) }
</label>
</div>;
}, },
_renderDevicesPanel: function() { _renderDevicesPanel: function() {
@ -927,18 +802,18 @@ module.exports = React.createClass({
<h3>{ _t('Analytics') }</h3> <h3>{ _t('Analytics') }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ _t('Riot collects anonymous analytics to allow us to improve the application.') } { _t('Riot collects anonymous analytics to allow us to improve the application.') }
{ ANALYTICS_SETTINGS_LABELS.map( this._renderLocalSetting ) } { ANALYTICS_SETTINGS.map( this._renderDeviceSetting ) }
</div> </div>
</div>; </div>;
}, },
_renderLabs: function() { _renderLabs: function() {
const features = []; const features = [];
UserSettingsStore.getLabsFeatures().forEach((featureId) => { SettingsStore.getLabsFeatures().forEach((featureId) => {
// TODO: this ought to be a separate component so that we don't need // TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render // to rebind the onChange each time we render
const onChange = (e) => { const onChange = (e) => {
UserSettingsStore.setFeatureEnabled(featureId, e.target.checked); SettingsStore.setFeatureEnabled(featureId, e.target.checked);
this.forceUpdate(); this.forceUpdate();
}; };
@ -948,10 +823,10 @@ module.exports = React.createClass({
type="checkbox" type="checkbox"
id={featureId} id={featureId}
name={featureId} name={featureId}
defaultChecked={UserSettingsStore.isFeatureEnabled(featureId)} defaultChecked={SettingsStore.isFeatureEnabled(featureId)}
onChange={onChange} onChange={onChange}
/> />
<label htmlFor={featureId}>{ UserSettingsStore.translatedNameForFeature(featureId) }</label> <label htmlFor={featureId}>{ SettingsStore.getDisplayName(featureId) }</label>
</div>); </div>);
}); });
@ -1044,6 +919,8 @@ module.exports = React.createClass({
const settings = this.state.electron_settings; const settings = this.state.electron_settings;
if (!settings) return; if (!settings) return;
// TODO: This should probably be a granular setting, but it only applies to electron
// and ends up being get/set outside of matrix anyways (local system setting).
return <div> return <div>
<h3>{ _t('Desktop specific') }</h3> <h3>{ _t('Desktop specific') }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
@ -1166,7 +1043,7 @@ module.exports = React.createClass({
return <div> return <div>
<h3>{ _t('VoIP') }</h3> <h3>{ _t('VoIP') }</h3>
<div className="mx_UserSettings_section"> <div className="mx_UserSettings_section">
{ WEBRTC_SETTINGS_LABELS.map(this._renderLocalSetting) } { WEBRTC_SETTINGS.map(this._renderDeviceSetting) }
{ this._renderWebRtcDeviceSettings() } { this._renderWebRtcDeviceSettings() }
</div> </div>
</div>; </div>;

View file

@ -17,13 +17,13 @@ limitations under the License.
'use strict'; 'use strict';
const React = require('react'); import React from 'react';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
const sdk = require('../../../index'); import sdk from '../../../index';
const Modal = require("../../../Modal"); import Modal from "../../../Modal";
const MatrixClientPeg = require('../../../MatrixClientPeg'); import MatrixClientPeg from "../../../MatrixClientPeg";
const PasswordReset = require("../../../PasswordReset"); import PasswordReset from "../../../PasswordReset";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'ForgotPassword', displayName: 'ForgotPassword',
@ -154,6 +154,7 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const LoginPage = sdk.getComponent("login.LoginPage");
const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter"); const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig"); const ServerConfig = sdk.getComponent("login.ServerConfig");
@ -165,7 +166,7 @@ module.exports = React.createClass({
resetPasswordJsx = <Spinner />; resetPasswordJsx = <Spinner />;
} else if (this.state.progress === "sent_email") { } else if (this.state.progress === "sent_email") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div className="mx_Login_prompt">
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) } { _t("An email has been sent to %(emailAddress)s. Once you've followed the link it contains, click below.", { emailAddress: this.state.email }) }
<br /> <br />
<input className="mx_Login_submit" type="button" onClick={this.onVerify} <input className="mx_Login_submit" type="button" onClick={this.onVerify}
@ -174,7 +175,7 @@ module.exports = React.createClass({
); );
} else if (this.state.progress === "complete") { } else if (this.state.progress === "complete") {
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div className="mx_Login_prompt">
<p>{ _t('Your password has been reset') }.</p> <p>{ _t('Your password has been reset') }.</p>
<p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p> <p>{ _t('You have been logged out of all devices and will no longer receive push notifications. To re-enable notifications, sign in again on each device') }.</p>
<input className="mx_Login_submit" type="button" onClick={this.props.onComplete} <input className="mx_Login_submit" type="button" onClick={this.props.onComplete}
@ -182,6 +183,20 @@ module.exports = React.createClass({
</div> </div>
); );
} else { } else {
let serverConfigSection;
if (!config.disable_custom_urls) {
serverConfigSection = (
<ServerConfig ref="serverConfig"
withToggleButton={true}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
);
}
resetPasswordJsx = ( resetPasswordJsx = (
<div> <div>
<div className="mx_Login_prompt"> <div className="mx_Login_prompt">
@ -209,16 +224,7 @@ module.exports = React.createClass({
<br /> <br />
<input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} /> <input className="mx_Login_submit" type="submit" value={_t('Send Reset Email')} />
</form> </form>
<ServerConfig ref="serverConfig" { serverConfigSection }
withToggleButton={true}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={0} />
<div className="mx_Login_error">
</div>
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> <a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ _t('Return to login screen') } { _t('Return to login screen') }
</a> </a>
@ -233,12 +239,12 @@ module.exports = React.createClass({
return ( return (
<div className="mx_Login"> <LoginPage>
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
{ resetPasswordJsx } { resetPasswordJsx }
</div> </div>
</div> </LoginPage>
); );
}, },
}); });

View file

@ -22,8 +22,9 @@ import { _t, _tJsx } from '../../../languageHandler';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import sdk from '../../../index'; import sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
import UserSettingsStore from '../../../UserSettingsStore';
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import SdkConfig from '../../../SdkConfig';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9\(\)\-\s]*$/;
@ -105,7 +106,22 @@ module.exports = React.createClass({
if (error.httpStatus == 400 && usingEmail) { if (error.httpStatus == 400 && usingEmail) {
errorText = _t('This Home Server does not support login using email address.'); errorText = _t('This Home Server does not support login using email address.');
} else if (error.httpStatus === 401 || error.httpStatus === 403) { } else if (error.httpStatus === 401 || error.httpStatus === 403) {
if (SdkConfig.get().disable_custom_urls) {
errorText = (
<div>
<div>{ _t('Incorrect username and/or password.') }</div>
<div className="mx_Login_smallError">
{ _t('Please note you are logging into the %(hs)s server, not matrix.org.',
{
hs: this.props.defaultHsUrl.replace(/^https?:\/\//, '')
})
}
</div>
</div>
);
} else {
errorText = _t('Incorrect username and/or password.'); errorText = _t('Incorrect username and/or password.');
}
} else { } else {
// other errors, not specific to doing a password login // other errors, not specific to doing a password login
errorText = this._errorTextFromError(error); errorText = this._errorTextFromError(error);
@ -312,7 +328,7 @@ module.exports = React.createClass({
_onLanguageChange: function(newLang) { _onLanguageChange: function(newLang) {
if(languageHandler.getCurrentLanguage() !== newLang) { if(languageHandler.getCurrentLanguage() !== newLang) {
UserSettingsStore.setLocalSetting('language', newLang); SettingsStore.setValue("language", null, SettingLevel.DEVICE, newLang);
PlatformPeg.get().reload(); PlatformPeg.get().reload();
} }
}, },
@ -329,6 +345,7 @@ module.exports = React.createClass({
render: function() { render: function() {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
const LoginPage = sdk.getComponent("login.LoginPage");
const LoginHeader = sdk.getComponent("login.LoginHeader"); const LoginHeader = sdk.getComponent("login.LoginHeader");
const LoginFooter = sdk.getComponent("login.LoginFooter"); const LoginFooter = sdk.getComponent("login.LoginFooter");
const ServerConfig = sdk.getComponent("login.ServerConfig"); const ServerConfig = sdk.getComponent("login.ServerConfig");
@ -343,43 +360,69 @@ module.exports = React.createClass({
} }
let returnToAppJsx; let returnToAppJsx;
/*
// with the advent of ILAG I don't think we need this any more
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = returnToAppJsx =
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
{ _t('Return to app') } { _t('Return to app') }
</a>; </a>;
} }
*/
return ( let serverConfig;
<div className="mx_Login"> let header;
<div className="mx_Login_box">
<LoginHeader /> if (!SdkConfig.get().disable_custom_urls) {
<div> serverConfig = <ServerConfig ref="serverConfig"
<h2>{ _t('Sign in') }
{ loader }
</h2>
{ this.componentForStep(this.state.currentFlow) }
<ServerConfig ref="serverConfig"
withToggleButton={true} withToggleButton={true}
customHsUrl={this.props.customHsUrl} customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl} customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl} defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl} defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange} onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000} /> delayTimeMs={1000} />;
}
// FIXME: remove status.im theme tweaks
const theme = SettingsStore.getValue("theme");
if (theme !== "status") {
header = <h2>{ _t('Sign in') }</h2>;
}
else {
if (!this.state.errorText) {
header = <h2>{ _t('Sign in to get started') }</h2>;
}
}
let errorTextSection;
if (this.state.errorText) {
errorTextSection = (
<div className="mx_Login_error"> <div className="mx_Login_error">
{ this.state.errorText } { this.state.errorText }
</div> </div>
);
}
return (
<LoginPage>
<div className="mx_Login_box">
<LoginHeader />
<div>
{ header }
{ errorTextSection }
{ this.componentForStep(this.state.currentFlow) }
{ serverConfig }
<a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#"> <a className="mx_Login_create" onClick={this.props.onRegisterClick} href="#">
{ _t('Create an account') } { _t('Create an account') }
</a> </a>
{ loginAsGuestJsx } { loginAsGuestJsx }
{ returnToAppJsx } { returnToAppJsx }
{ this._renderLanguageSetting() } { !SdkConfig.get().disable_login_language_selector ? this._renderLanguageSetting() : '' }
<LoginFooter /> <LoginFooter />
</div> </div>
</div> </div>
</div> </LoginPage>
); );
}, },
}); });

View file

@ -59,9 +59,10 @@ module.exports = React.createClass({
render: function() { render: function() {
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName'); const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar'); const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const LoginPage = sdk.getComponent('login.LoginPage');
const LoginHeader = sdk.getComponent('login.LoginHeader'); const LoginHeader = sdk.getComponent('login.LoginHeader');
return ( return (
<div className="mx_Login"> <LoginPage>
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader /> <LoginHeader />
<div className="mx_Login_profile"> <div className="mx_Login_profile">
@ -74,7 +75,7 @@ module.exports = React.createClass({
{ this.state.errorString } { this.state.errorString }
</div> </div>
</div> </div>
</div> </LoginPage>
); );
}, },
}); });

View file

@ -26,6 +26,8 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import RegistrationForm from '../../views/login/RegistrationForm'; import RegistrationForm from '../../views/login/RegistrationForm';
import RtsClient from '../../../RtsClient'; import RtsClient from '../../../RtsClient';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
const MIN_PASSWORD_LENGTH = 6; const MIN_PASSWORD_LENGTH = 6;
@ -322,10 +324,13 @@ module.exports = React.createClass({
render: function() { render: function() {
const LoginHeader = sdk.getComponent('login.LoginHeader'); const LoginHeader = sdk.getComponent('login.LoginHeader');
const LoginFooter = sdk.getComponent('login.LoginFooter'); const LoginFooter = sdk.getComponent('login.LoginFooter');
const LoginPage = sdk.getComponent('login.LoginPage');
const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth');
const Spinner = sdk.getComponent("elements.Spinner"); const Spinner = sdk.getComponent("elements.Spinner");
const ServerConfig = sdk.getComponent('views.login.ServerConfig'); const ServerConfig = sdk.getComponent('views.login.ServerConfig');
const theme = SettingsStore.getValue("theme");
let registerBody; let registerBody;
if (this.state.doingUIAuth) { if (this.state.doingUIAuth) {
registerBody = ( registerBody = (
@ -344,9 +349,19 @@ module.exports = React.createClass({
} else if (this.state.busy || this.state.teamServerBusy) { } else if (this.state.busy || this.state.teamServerBusy) {
registerBody = <Spinner />; registerBody = <Spinner />;
} else { } else {
let errorSection; let serverConfigSection;
if (this.state.errorText) { if (!SdkConfig.get().disable_custom_urls) {
errorSection = <div className="mx_Login_error">{ this.state.errorText }</div>; serverConfigSection = (
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
);
} }
registerBody = ( registerBody = (
<div> <div>
@ -362,21 +377,14 @@ module.exports = React.createClass({
onRegisterClick={this.onFormSubmit} onRegisterClick={this.onFormSubmit}
onTeamSelected={this.onTeamSelected} onTeamSelected={this.onTeamSelected}
/> />
{ errorSection } { serverConfigSection }
<ServerConfig ref="serverConfig"
withToggleButton={true}
customHsUrl={this.props.customHsUrl}
customIsUrl={this.props.customIsUrl}
defaultHsUrl={this.props.defaultHsUrl}
defaultIsUrl={this.props.defaultIsUrl}
onServerConfigChange={this.onServerConfigChange}
delayTimeMs={1000}
/>
</div> </div>
); );
} }
let returnToAppJsx; let returnToAppJsx;
/*
// with the advent of ILAG I don't think we need this any more
if (this.props.onCancelClick) { if (this.props.onCancelClick) {
returnToAppJsx = ( returnToAppJsx = (
<a className="mx_Login_create" onClick={this.props.onCancelClick} href="#"> <a className="mx_Login_create" onClick={this.props.onCancelClick} href="#">
@ -384,8 +392,32 @@ module.exports = React.createClass({
</a> </a>
); );
} }
*/
let header;
let errorText;
// FIXME: remove hardcoded Status team tweaks at some point
if (theme === 'status' && this.state.errorText) {
header = <div className="mx_Login_error">{ this.state.errorText }</div>;
}
else {
header = <h2>{ _t('Create an account') }</h2>;
if (this.state.errorText) {
errorText = <div className="mx_Login_error">{ this.state.errorText }</div>;
}
}
let signIn;
if (!this.state.doingUIAuth) {
signIn = (
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#">
{ theme === 'status' ? _t('Sign in') : _t('I already have an account') }
</a>
);
}
return ( return (
<div className="mx_Login"> <LoginPage>
<div className="mx_Login_box"> <div className="mx_Login_box">
<LoginHeader <LoginHeader
icon={this.state.teamSelected ? icon={this.state.teamSelected ?
@ -393,15 +425,14 @@ module.exports = React.createClass({
this.state.teamSelected.domain + "/icon.png" : this.state.teamSelected.domain + "/icon.png" :
null} null}
/> />
<h2>{ _t('Create an account') }</h2> { header }
{ registerBody } { registerBody }
<a className="mx_Login_create" onClick={this.props.onLoginClick} href="#"> { signIn }
{ _t('I already have an account') } { errorText }
</a>
{ returnToAppJsx } { returnToAppJsx }
<LoginFooter /> <LoginFooter />
</div> </div>
</div> </LoginPage>
); );
}, },
}); });

View file

@ -0,0 +1,157 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import React from "react";
import * as sdk from "../../../index";
import MatrixClientPeg from "../../../MatrixClientPeg";
import AccessibleButton from '../elements/AccessibleButton';
import Presence from "../../../Presence";
import dispatcher from "../../../dispatcher";
import * as ContextualMenu from "../../structures/ContextualMenu";
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
displayName: 'MemberPresenceAvatar',
propTypes: {
member: React.PropTypes.object.isRequired,
width: React.PropTypes.number,
height: React.PropTypes.number,
resizeMethod: React.PropTypes.string,
},
getDefaultProps: function() {
return {
width: 40,
height: 40,
resizeMethod: 'crop',
};
},
getInitialState: function() {
const presenceState = this.props.member.user.presence;
const presenceMessage = this.props.member.user.presenceStatusMsg;
return {
status: presenceState,
message: presenceMessage,
};
},
componentWillMount: function() {
MatrixClientPeg.get().on("User.presence", this.onUserPresence);
this.dispatcherRef = dispatcher.register(this.onAction);
},
componentWillUnmount: function() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("User.presence", this.onUserPresence);
}
dispatcher.unregister(this.dispatcherRef);
},
onAction: function(payload) {
if (payload.action !== "self_presence_updated") return;
if (this.props.member.userId !== MatrixClientPeg.get().getUserId()) return;
this.setState({
status: payload.statusInfo.presence,
message: payload.statusInfo.status_msg,
});
},
onUserPresence: function(event, user) {
if (user.userId !== MatrixClientPeg.get().getUserId()) return;
this.setState({
status: user.presence,
message: user.presenceStatusMsg,
});
},
onStatusChange: function(newStatus) {
Presence.stopMaintainingStatus();
if (newStatus === "online") {
Presence.setState(newStatus);
} else Presence.setState(newStatus, null, true);
},
onClick: function(e) {
const PresenceContextMenu = sdk.getComponent('context_menus.PresenceContextMenu');
const elementRect = e.target.getBoundingClientRect();
// The window X and Y offsets are to adjust position when zoomed in to page
const x = (elementRect.left + window.pageXOffset) - (elementRect.width / 2) + 3;
const chevronOffset = 12;
let y = elementRect.top + (elementRect.height / 2) + window.pageYOffset;
y = y - (chevronOffset + 4); // where 4 is 1/4 the height of the chevron
ContextualMenu.createMenu(PresenceContextMenu, {
chevronOffset: chevronOffset,
chevronFace: 'bottom',
left: x,
top: y,
menuWidth: 125,
currentStatus: this.state.status,
onChange: this.onStatusChange,
});
e.stopPropagation();
// const presenceState = this.props.member.user.presence;
// const presenceLastActiveAgo = this.props.member.user.lastActiveAgo;
// const presenceLastTs = this.props.member.user.lastPresenceTs;
// const presenceCurrentlyActive = this.props.member.user.currentlyActive;
// const presenceMessage = this.props.member.user.presenceStatusMsg;
},
render: function() {
const MemberAvatar = sdk.getComponent("avatars.MemberAvatar");
let onClickFn = null;
if (this.props.member.userId === MatrixClientPeg.get().getUserId()) {
onClickFn = this.onClick;
}
const avatarNode = (
<MemberAvatar member={this.props.member} width={this.props.width} height={this.props.height}
resizeMethod={this.props.resizeMethod} />
);
let statusNode = (
<span className={"mx_MemberPresenceAvatar_status mx_MemberPresenceAvatar_status_" + this.state.status} />
);
// LABS: Disable presence management functions for now
if (!SettingsStore.isFeatureEnabled("feature_presence_management")) {
statusNode = null;
onClickFn = null;
}
let avatar = (
<div className="mx_MemberPresenceAvatar">
{ avatarNode }
{ statusNode }
</div>
);
if (onClickFn) {
avatar = (
<AccessibleButton onClick={onClickFn} className="mx_MemberPresenceAvatar" element="div">
{ avatarNode }
{ statusNode }
</AccessibleButton>
);
}
return avatar;
},
});

View file

@ -21,6 +21,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import GeminiScrollbar from 'react-gemini-scrollbar'; import GeminiScrollbar from 'react-gemini-scrollbar';
import Resend from '../../../Resend'; import Resend from '../../../Resend';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
function DeviceListEntry(props) { function DeviceListEntry(props) {
const {userId, device} = props; const {userId, device} = props;
@ -114,12 +115,13 @@ export default React.createClass({
}, },
render: function() { render: function() {
const client = MatrixClientPeg.get(); if (this.state.devices === null) {
const blacklistUnverified = client.getGlobalBlacklistUnverifiedDevices() || const Spinner = sdk.getComponent("elements.Spinner");
this.props.room.getBlacklistUnverifiedDevices(); return <Spinner />;
}
let warning; let warning;
if (blacklistUnverified) { if (SettingsStore.getValue("blacklistUnverifiedDevices", this.props.room.roomId)) {
warning = ( warning = (
<h4> <h4>
{ _t("You are currently blacklisting unverified devices; to send " + { _t("You are currently blacklisting unverified devices; to send " +

View file

@ -381,12 +381,12 @@ export default React.createClass({
return ( return (
<div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}> <div className={this.props.fullWidth ? "mx_AppTileFullWidth" : "mx_AppTile"} id={this.props.id}>
<div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}> <div ref="menu_bar" className="mx_AppTileMenuBar" onClick={this.onClickMenuBar}>
{ this.formatAppTileName() } <b>{ this.formatAppTileName() }</b>
<span className="mx_AppTileMenuBarWidgets"> <span className="mx_AppTileMenuBarWidgets">
{ /* Edit widget */ } { /* Edit widget */ }
{ showEditButton && <img { showEditButton && <img
src="img/edit.svg" src="img/edit_green.svg"
className="mx_filterFlipColor mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding" className="mx_AppTileMenuBarWidget mx_AppTileMenuBarWidgetPadding"
width="8" height="8" width="8" height="8"
alt={_t('Edit')} alt={_t('Edit')}
title={_t('Edit')} title={_t('Edit')}

View file

@ -18,8 +18,8 @@ limitations under the License.
import React from 'react'; import React from 'react';
import sdk from '../../../index'; import sdk from '../../../index';
import UserSettingsStore from '../../../UserSettingsStore';
import * as languageHandler from '../../../languageHandler'; import * as languageHandler from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
function languageMatchesSearchQuery(query, language) { function languageMatchesSearchQuery(query, language) {
if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true; if (language.label.toUpperCase().indexOf(query.toUpperCase()) == 0) return true;
@ -54,9 +54,9 @@ export default class LanguageDropdown extends React.Component {
// If no value is given, we start with the first // If no value is given, we start with the first
// country selected, but our parent component // country selected, but our parent component
// doesn't know this, therefore we do this. // doesn't know this, therefore we do this.
const _localSettings = UserSettingsStore.getLocalSettings(); const language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
if (_localSettings.hasOwnProperty('language')) { if (language) {
this.props.onOptionChange(_localSettings.language); this.props.onOptionChange(language);
}else { }else {
const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser()); const language = languageHandler.normalizeLanguageKey(languageHandler.getLanguageFromBrowser());
this.props.onOptionChange(language); this.props.onOptionChange(language);
@ -95,12 +95,12 @@ export default class LanguageDropdown extends React.Component {
// default value here too, otherwise we need to handle null / undefined // default value here too, otherwise we need to handle null / undefined
// values between mounting and the initial value propgating // values between mounting and the initial value propgating
let language = SettingsStore.getValue("language", null, /*excludeDefault:*/true);
let value = null; let value = null;
const _localSettings = UserSettingsStore.getLocalSettings(); if (language) {
if (_localSettings.hasOwnProperty('language')) { value = this.props.value || language;
value = this.props.value || _localSettings.language;
} else { } else {
const language = navigator.language || navigator.userLanguage; language = navigator.language || navigator.userLanguage;
value = this.props.value || language; value = this.props.value || language;
} }

View file

@ -20,14 +20,16 @@ import React from 'react';
import * as Roles from '../../../Roles'; import * as Roles from '../../../Roles';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
let LEVEL_ROLE_MAP = {};
const reverseRoles = {};
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'PowerSelector', displayName: 'PowerSelector',
propTypes: { propTypes: {
value: React.PropTypes.number.isRequired, value: React.PropTypes.number.isRequired,
// The maximum value that can be set with the power selector
maxValue: React.PropTypes.number.isRequired,
// Default user power level for the room
usersDefault: React.PropTypes.number.isRequired,
// if true, the <select/> should be a 'controlled' form element and updated by React // if true, the <select/> should be a 'controlled' form element and updated by React
// to reflect the current value, rather than left freeform. // to reflect the current value, rather than left freeform.
@ -43,78 +45,98 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
return { return {
custom: (LEVEL_ROLE_MAP[this.props.value] === undefined), levelRoleMap: {},
// List of power levels to show in the drop-down
options: [],
};
},
getDefaultProps: function() {
return {
maxValue: Infinity,
usersDefault: 0,
}; };
}, },
componentWillMount: function() { componentWillMount: function() {
LEVEL_ROLE_MAP = Roles.levelRoleMap(); this._initStateFromProps(this.props);
Object.keys(LEVEL_ROLE_MAP).forEach(function(key) { },
reverseRoles[LEVEL_ROLE_MAP[key]] = key;
componentWillReceiveProps: function(newProps) {
this._initStateFromProps(newProps);
},
_initStateFromProps: function(newProps) {
// This needs to be done now because levelRoleMap has translated strings
const levelRoleMap = Roles.levelRoleMap(newProps.usersDefault);
const options = Object.keys(levelRoleMap).filter((l) => {
return l === undefined || l <= newProps.maxValue;
});
this.setState({
levelRoleMap,
options,
custom: levelRoleMap[newProps.value] === undefined,
}); });
}, },
onSelectChange: function(event) { onSelectChange: function(event) {
this.setState({ custom: event.target.value === "Custom" }); this.setState({ custom: event.target.value === "SELECT_VALUE_CUSTOM" });
if (event.target.value !== "Custom") { if (event.target.value !== "SELECT_VALUE_CUSTOM") {
this.props.onChange(this.getValue()); this.props.onChange(event.target.value);
} }
}, },
onCustomBlur: function(event) { onCustomBlur: function(event) {
this.props.onChange(this.getValue()); this.props.onChange(parseInt(this.refs.custom.value));
}, },
onCustomKeyDown: function(event) { onCustomKeyDown: function(event) {
if (event.key == "Enter") { if (event.key == "Enter") {
this.props.onChange(this.getValue()); this.props.onChange(parseInt(this.refs.custom.value));
} }
}, },
getValue: function() {
let value;
if (this.refs.select) {
value = reverseRoles[this.refs.select.value];
if (this.refs.custom) {
if (value === undefined) value = parseInt( this.refs.custom.value );
}
}
return value;
},
render: function() { render: function() {
let customPicker; let customPicker;
if (this.state.custom) { if (this.state.custom) {
let input;
if (this.props.disabled) { if (this.props.disabled) {
input = <span>{ this.props.value }</span>; customPicker = <span>{ _t(
"Custom of %(powerLevel)s",
{ powerLevel: this.props.value },
) }</span>;
} else { } else {
input = <input ref="custom" type="text" size="3" defaultValue={this.props.value} onBlur={this.onCustomBlur} onKeyDown={this.onCustomKeyDown} />; customPicker = <span> = <input
ref="custom"
type="text"
size="3"
defaultValue={this.props.value}
onBlur={this.onCustomBlur}
onKeyDown={this.onCustomKeyDown}
/>
</span>;
} }
customPicker = <span> of { input }</span>;
} }
let selectValue; let selectValue;
if (this.state.custom) { if (this.state.custom) {
selectValue = "Custom"; selectValue = "SELECT_VALUE_CUSTOM";
} else { } else {
selectValue = LEVEL_ROLE_MAP[this.props.value] || "Custom"; selectValue = this.state.levelRoleMap[this.props.value] ?
this.props.value : "SELECT_VALUE_CUSTOM";
} }
let select; let select;
if (this.props.disabled) { if (this.props.disabled) {
select = <span>{ selectValue }</span>; select = <span>{ this.state.levelRoleMap[selectValue] }</span>;
} else { } else {
// Each level must have a definition in LEVEL_ROLE_MAP // Each level must have a definition in this.state.levelRoleMap
const levels = [0, 50, 100]; let options = this.state.options.map((level) => {
let options = levels.map((level) => {
return { return {
value: LEVEL_ROLE_MAP[level], value: level,
// Give a userDefault (users_default in the power event) of 0 but text: Roles.textualPowerLevel(level, this.props.usersDefault),
// because level !== undefined, this should never be used.
text: Roles.textualPowerLevel(level, 0),
}; };
}); });
options.push({ value: "Custom", text: _t("Custom level") }); options.push({ value: "SELECT_VALUE_CUSTOM", text: _t("Custom level") });
options = options.map((op) => { options = options.map((op) => {
return <option value={op.value} key={op.value}>{ op.text }</option>; return <option value={op.value} key={op.value}>{ op.text }</option>;
}); });

View file

@ -0,0 +1,110 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import SettingsStore from "../../../settings/SettingsStore";
import { _t } from '../../../languageHandler';
module.exports = React.createClass({
displayName: 'SettingsFlag',
propTypes: {
name: React.PropTypes.string.isRequired,
level: React.PropTypes.string.isRequired,
roomId: React.PropTypes.string, // for per-room settings
label: React.PropTypes.string, // untranslated
onChange: React.PropTypes.func,
isExplicit: React.PropTypes.bool,
manualSave: React.PropTypes.bool,
// If group is supplied, then this will create a radio button instead.
group: React.PropTypes.string,
value: React.PropTypes.any, // the value for the radio button
},
getInitialState: function() {
return {
value: SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId,
this.props.isExplicit,
),
};
},
onChange: function(e) {
if (this.props.group && !e.target.checked) return;
const newState = this.props.group ? this.props.value : e.target.checked;
if (!this.props.manualSave) this.save(newState);
else this.setState({ value: newState });
if (this.props.onChange) this.props.onChange(newState);
},
save: function(val = undefined) {
return SettingsStore.setValue(
this.props.name,
this.props.roomId,
this.props.level,
val !== undefined ? val : this.state.value,
);
},
render: function() {
const value = this.props.manualSave ? this.state.value : SettingsStore.getValueAt(
this.props.level,
this.props.name,
this.props.roomId,
this.props.isExplicit,
);
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
let label = this.props.label;
if (!label) label = SettingsStore.getDisplayName(this.props.name, this.props.level);
else label = _t(label);
// We generate a relatively complex ID to avoid conflicts
const id = this.props.name + "_" + this.props.group + "_" + this.props.value + "_" + this.props.level;
let checkbox = (
<input id={id}
type="checkbox"
defaultChecked={value}
onChange={this.onChange}
disabled={!canChange}
/>
);
if (this.props.group) {
checkbox = (
<input id={id}
type="radio"
name={this.props.group}
value={this.props.value}
checked={value === this.props.value}
onChange={this.onChange}
disabled={!canChange}
/>
);
}
return (
<label>
{ checkbox }
{ label }
</label>
);
},
});

View file

@ -0,0 +1,59 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
'use strict';
import SettingsStore from "../../../settings/SettingsStore";
const React = require('react');
module.exports = React.createClass({
displayName: 'LoginPage',
render: function() {
// FIXME: this should be turned into a proper skin with a StatusLoginPage component
if (SettingsStore.getValue("theme") === 'status') {
return (
<div className="mx_StatusLogin">
<div className="mx_StatusLogin_brand">
<img src="themes/status/img/logo.svg" alt="Status" width="221" height="53" />
</div>
<div className="mx_StatusLogin_content">
<div className="mx_StatusLogin_header">
<h1>Status Community Chat</h1>
<div className="mx_StatusLogin_subtitle">
A safer, decentralised communication
platform <a href="https://riot.im">powered by Riot</a>
</div>
</div>
{ this.props.children }
<div className="mx_StatusLogin_footer">
<p>This channel is for our development community.</p>
<p>Interested in SNT and discussions on the cryptocurrency market?</p>
<p><a href="https://t.me/StatusNetworkChat" target="_blank" className="mx_StatusLogin_footer_cta">Join Telegram Chat</a></p>
</div>
</div>
</div>
);
} else {
return (
<div className="mx_Login">
{ this.props.children }
</div>
);
}
},
});

View file

@ -20,7 +20,7 @@ import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {field_input_incorrect} from '../../../UiEffects'; import {field_input_incorrect} from '../../../UiEffects';
import SdkConfig from '../../../SdkConfig';
/** /**
* A pure UI component which displays a username/password form. * A pure UI component which displays a username/password form.
@ -144,7 +144,10 @@ class PasswordLogin extends React.Component {
type="text" type="text"
name="username" // make it a little easier for browser's remember-password name="username" // make it a little easier for browser's remember-password
onChange={this.onUsernameChanged} onChange={this.onUsernameChanged}
placeholder={_t('User name')} placeholder={ SdkConfig.get().disable_custom_urls ?
_t("Username on %(hs)s", {
hs: this.props.hsUrl.replace(/^https?:\/\//, '')
}) : _t("User name")}
value={this.state.username} value={this.state.username}
autoFocus autoFocus
disabled={disabled} disabled={disabled}
@ -210,9 +213,9 @@ class PasswordLogin extends React.Component {
const loginField = this.renderLoginField(this.state.loginType, matrixIdText === ''); const loginField = this.renderLoginField(this.state.loginType, matrixIdText === '');
return ( let loginType;
<div> if (!SdkConfig.get().disable_3pid_login) {
<form onSubmit={this.onSubmitForm}> loginType = (
<div className="mx_Login_type_container"> <div className="mx_Login_type_container">
<label className="mx_Login_type_label">{ _t('Sign in with') }</label> <label className="mx_Login_type_label">{ _t('Sign in with') }</label>
<Dropdown <Dropdown
@ -225,6 +228,13 @@ class PasswordLogin extends React.Component {
<span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span> <span key={PasswordLogin.LOGIN_FIELD_PHONE}>{ _t('Phone') }</span>
</Dropdown> </Dropdown>
</div> </div>
);
}
return (
<div>
<form onSubmit={this.onSubmitForm}>
{ loginType }
{ loginField } { loginField }
<input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password" <input className={pwFieldClass} ref={(e) => {this._passwordField = e;}} type="password"
name="password" name="password"

View file

@ -22,6 +22,8 @@ import Email from '../../../email';
import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import { looksValid as phoneNumberLooksValid } from '../../../phonenumber';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import SettingsStore from "../../../settings/SettingsStore";
const FIELD_EMAIL = 'field_email'; const FIELD_EMAIL = 'field_email';
const FIELD_PHONE_COUNTRY = 'field_phone_country'; const FIELD_PHONE_COUNTRY = 'field_phone_country';
@ -122,7 +124,7 @@ module.exports = React.createClass({
password: this.refs.password.value.trim(), password: this.refs.password.value.trim(),
email: email, email: email,
phoneCountry: this.state.phoneCountry, phoneCountry: this.state.phoneCountry,
phoneNumber: this.refs.phoneNumber.value.trim(), phoneNumber: this.refs.phoneNumber ? this.refs.phoneNumber.value.trim() : '',
}); });
if (promise) { if (promise) {
@ -180,7 +182,7 @@ module.exports = React.createClass({
this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID"); this.markFieldValid(field_id, emailValid, "RegistrationForm.ERR_EMAIL_INVALID");
break; break;
case FIELD_PHONE_NUMBER: case FIELD_PHONE_NUMBER:
const phoneNumber = this.refs.phoneNumber.value; const phoneNumber = this.refs.phoneNumber ? this.refs.phoneNumber.value : '';
const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber); const phoneNumberValid = phoneNumber === '' || phoneNumberLooksValid(phoneNumber);
this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID");
break; break;
@ -273,10 +275,14 @@ module.exports = React.createClass({
render: function() { render: function() {
const self = this; const self = this;
const theme = SettingsStore.getValue("theme");
// FIXME: remove hardcoded Status team tweaks at some point
const emailPlaceholder = theme === 'status' ? _t("Email address") : _t("Email address (optional)");
const emailSection = ( const emailSection = (
<div> <div>
<input type="text" ref="email" <input type="text" ref="email"
autoFocus={true} placeholder={_t("Email address (optional)")} autoFocus={true} placeholder={ emailPlaceholder }
defaultValue={this.props.defaultEmail} defaultValue={this.props.defaultEmail}
className={this._classForField(FIELD_EMAIL, 'mx_Login_field')} className={this._classForField(FIELD_EMAIL, 'mx_Login_field')}
onBlur={function() {self.validateField(FIELD_EMAIL);}} onBlur={function() {self.validateField(FIELD_EMAIL);}}
@ -306,7 +312,9 @@ module.exports = React.createClass({
} }
const CountryDropdown = sdk.getComponent('views.login.CountryDropdown'); const CountryDropdown = sdk.getComponent('views.login.CountryDropdown');
const phoneSection = ( let phoneSection;
if (!SdkConfig.get().disable_3pid_login) {
phoneSection = (
<div className="mx_Login_phoneSection"> <div className="mx_Login_phoneSection">
<CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange} <CountryDropdown ref="phone_country" onOptionChange={this._onPhoneCountryChange}
className="mx_Login_phoneCountry mx_Login_field_prefix" className="mx_Login_phoneCountry mx_Login_field_prefix"
@ -328,6 +336,7 @@ module.exports = React.createClass({
/> />
</div> </div>
); );
}
const registerButton = ( const registerButton = (
<input className="mx_Login_submit" type="submit" value={_t("Register")} /> <input className="mx_Login_submit" type="submit" value={_t("Register")} />

View file

@ -25,8 +25,8 @@ import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile'; import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import Promise from 'bluebird'; import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MImageBody', displayName: 'MImageBody',
@ -81,7 +81,7 @@ module.exports = React.createClass({
}, },
onImageEnter: function(e) { onImageEnter: function(e) {
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) { if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return; return;
} }
const imgElement = e.target; const imgElement = e.target;
@ -89,7 +89,7 @@ module.exports = React.createClass({
}, },
onImageLeave: function(e) { onImageLeave: function(e) {
if (!this._isGif() || UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) { if (!this._isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) {
return; return;
} }
const imgElement = e.target; const imgElement = e.target;
@ -218,7 +218,7 @@ module.exports = React.createClass({
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
let thumbUrl; let thumbUrl;
if (this._isGif() && UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false)) { if (this._isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) {
thumbUrl = contentUrl; thumbUrl = contentUrl;
} else { } else {
thumbUrl = this._getThumbUrl(); thumbUrl = this._getThumbUrl();

View file

@ -21,8 +21,8 @@ import MFileBody from './MFileBody';
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile'; import { decryptFile, readBlobAsDataUri } from '../../../utils/DecryptFile';
import Promise from 'bluebird'; import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'MVideoBody', displayName: 'MVideoBody',
@ -151,7 +151,7 @@ module.exports = React.createClass({
const contentUrl = this._getContentUrl(); const contentUrl = this._getContentUrl();
const thumbUrl = this._getThumbUrl(); const thumbUrl = this._getThumbUrl();
const autoplay = UserSettingsStore.getSyncedSetting("autoplayGifsAndVideos", false); const autoplay = SettingsStore.getValue("autoplayGifsAndVideos");
let height = null; let height = null;
let width = null; let width = null;
let poster = null; let poster = null;

View file

@ -29,11 +29,11 @@ import Modal from '../../../Modal';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import UserSettingsStore from "../../../UserSettingsStore";
import MatrixClientPeg from '../../../MatrixClientPeg'; import MatrixClientPeg from '../../../MatrixClientPeg';
import ContextualMenu from '../../structures/ContextualMenu'; import ContextualMenu from '../../structures/ContextualMenu';
import {RoomMember} from 'matrix-js-sdk'; import {RoomMember} from 'matrix-js-sdk';
import classNames from 'classnames'; import classNames from 'classnames';
import SettingsStore from "../../../settings/SettingsStore";
import PushProcessor from 'matrix-js-sdk/lib/pushprocessor'; import PushProcessor from 'matrix-js-sdk/lib/pushprocessor';
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -104,7 +104,7 @@ module.exports = React.createClass({
setTimeout(() => { setTimeout(() => {
if (this._unmounted) return; if (this._unmounted) return;
for (let i = 0; i < blocks.length; i++) { for (let i = 0; i < blocks.length; i++) {
if (UserSettingsStore.getSyncedSetting("enableSyntaxHighlightLanguageDetection", false)) { if (SettingsStore.getValue("enableSyntaxHighlightLanguageDetection")) {
highlight.highlightBlock(blocks[i]); highlight.highlightBlock(blocks[i]);
} else { } else {
// Only syntax highlight if there's a class starting with language- // Only syntax highlight if there's a class starting with language-
@ -169,7 +169,7 @@ module.exports = React.createClass({
}, },
pillifyLinks: function(nodes) { pillifyLinks: function(nodes) {
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
let node = nodes[0]; let node = nodes[0];
while (node) { while (node) {
let pillified = false; let pillified = false;
@ -419,7 +419,7 @@ module.exports = React.createClass({
const content = mxEvent.getContent(); const content = mxEvent.getContent();
let body = HtmlUtils.bodyToHtml(content, this.props.highlights, { let body = HtmlUtils.bodyToHtml(content, this.props.highlights, {
disableBigEmoji: UserSettingsStore.getSyncedSetting('TextualBody.disableBigEmoji', false), disableBigEmoji: SettingsStore.getValue('TextualBody.disableBigEmoji'),
}); });
if (this.props.highlightLink) { if (this.props.highlightLink) {

View file

@ -22,10 +22,11 @@ const MatrixClientPeg = require("../../../MatrixClientPeg");
const Modal = require("../../../Modal"); const Modal = require("../../../Modal");
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
const ROOM_COLORS = [ const ROOM_COLORS = [
// magic room default values courtesy of Ribot // magic room default values courtesy of Ribot
["#76cfa6", "#eaf5f0"], [Tinter.getKeyRgb()[0], Tinter.getKeyRgb()[1]],
["#81bddb", "#eaf1f4"], ["#81bddb", "#eaf1f4"],
["#bd79cb", "#f3eaf5"], ["#bd79cb", "#f3eaf5"],
["#c65d94", "#f5eaef"], ["#c65d94", "#f5eaef"],
@ -47,17 +48,17 @@ module.exports = React.createClass({
getInitialState: function() { getInitialState: function() {
const data = { const data = {
index: 0, index: 0,
primary_color: ROOM_COLORS[0].primary_color, primary_color: ROOM_COLORS[0][0],
secondary_color: ROOM_COLORS[0].secondary_color, secondary_color: ROOM_COLORS[0][1],
hasChanged: false, hasChanged: false,
}; };
const event = this.props.room.getAccountData("org.matrix.room.color_scheme"); const scheme = SettingsStore.getValueAt(SettingLevel.ROOM_ACCOUNT, "roomColor", this.props.room.roomId);
if (!event) {
return data; if (scheme.primary_color && scheme.secondary_color) {
} // We only use the user's scheme if the scheme is valid.
const scheme = event.getContent();
data.primary_color = scheme.primary_color; data.primary_color = scheme.primary_color;
data.secondary_color = scheme.secondary_color; data.secondary_color = scheme.secondary_color;
}
data.index = this._getColorIndex(data); data.index = this._getColorIndex(data);
if (data.index === -1) { if (data.index === -1) {
@ -81,13 +82,13 @@ module.exports = React.createClass({
// We would like guests to be able to set room colour but currently // We would like guests to be able to set room colour but currently
// they can't, so we still send the request but display a sensible // they can't, so we still send the request but display a sensible
// error if it fails. // error if it fails.
return MatrixClientPeg.get().setRoomAccountData( // TODO: Support guests for room color. Technically this is possible via granular settings
this.props.room.roomId, "org.matrix.room.color_scheme", { // Granular settings would mean the guest is forced to use the DEVICE level though.
SettingsStore.setValue("roomColor", this.props.room.roomId, SettingLevel.ROOM_ACCOUNT, {
primary_color: this.state.primary_color, primary_color: this.state.primary_color,
secondary_color: this.state.secondary_color, secondary_color: this.state.secondary_color,
}, }).catch(function(err) {
).catch(function(err) { if (err.errcode === 'M_GUEST_ACCESS_FORBIDDEN') {
if (err.errcode == 'M_GUEST_ACCESS_FORBIDDEN') {
dis.dispatch({action: 'view_set_mxid'}); dis.dispatch({action: 'view_set_mxid'});
} }
}); });

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2016 OpenMarket Ltd Copyright 2016 OpenMarket Ltd
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,13 +15,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import Promise from 'bluebird';
const React = require('react'); const React = require('react');
const MatrixClientPeg = require('../../../MatrixClientPeg');
const sdk = require("../../../index"); const sdk = require("../../../index");
const Modal = require("../../../Modal");
const UserSettingsStore = require('../../../UserSettingsStore');
import { _t, _tJsx } from '../../../languageHandler'; import { _t, _tJsx } from '../../../languageHandler';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
module.exports = React.createClass({ module.exports = React.createClass({
@ -30,137 +28,64 @@ module.exports = React.createClass({
room: React.PropTypes.object, room: React.PropTypes.object,
}, },
getInitialState: function() {
const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState;
const roomPreviewUrls = this.props.room.currentState.getStateEvents('org.matrix.room.preview_urls', '');
const userPreviewUrls = this.props.room.getAccountData("org.matrix.room.preview_urls");
return {
globalDisableUrlPreview: (roomPreviewUrls && roomPreviewUrls.getContent().disable) || false,
userDisableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === true)) || false,
userEnableUrlPreview: (userPreviewUrls && (userPreviewUrls.getContent().disable === false)) || false,
};
},
componentDidMount: function() {
this.originalState = Object.assign({}, this.state);
},
saveSettings: function() { saveSettings: function() {
const promises = []; const promises = [];
if (this.refs.urlPreviewsRoom) promises.push(this.refs.urlPreviewsRoom.save());
if (this.state.globalDisableUrlPreview !== this.originalState.globalDisableUrlPreview) { if (this.refs.urlPrviewsSelf) promises.push(this.refs.urlPreviewsSelf.save());
console.log("UrlPreviewSettings: Updating room's preview_urls state event");
promises.push(
MatrixClientPeg.get().sendStateEvent(
this.props.room.roomId, "org.matrix.room.preview_urls", {
disable: this.state.globalDisableUrlPreview,
}, "",
),
);
}
let content = undefined;
if (this.state.userDisableUrlPreview !== this.originalState.userDisableUrlPreview) {
console.log("UrlPreviewSettings: Disabling user's per-room preview_urls");
content = this.state.userDisableUrlPreview ? { disable: true } : {};
}
if (this.state.userEnableUrlPreview !== this.originalState.userEnableUrlPreview) {
console.log("UrlPreviewSettings: Enabling user's per-room preview_urls");
if (!content || content.disable === undefined) {
content = this.state.userEnableUrlPreview ? { disable: false } : {};
}
}
if (content) {
promises.push(
MatrixClientPeg.get().setRoomAccountData(
this.props.room.roomId, "org.matrix.room.preview_urls", content,
),
);
}
console.log("UrlPreviewSettings: saveSettings: " + JSON.stringify(promises));
return promises; return promises;
}, },
onGlobalDisableUrlPreviewChange: function() {
this.setState({
globalDisableUrlPreview: this.refs.globalDisableUrlPreview.checked ? true : false,
});
},
onUserEnableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: false,
userEnableUrlPreview: this.refs.userEnableUrlPreview.checked ? true : false,
});
},
onUserDisableUrlPreviewChange: function() {
this.setState({
userDisableUrlPreview: this.refs.userDisableUrlPreview.checked ? true : false,
userEnableUrlPreview: false,
});
},
render: function() { render: function() {
const self = this; const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const roomState = this.props.room.currentState; const roomId = this.props.room.roomId;
const cli = MatrixClientPeg.get();
const maySetRoomPreviewUrls = roomState.mayClientSendStateEvent('org.matrix.room.preview_urls', cli); let previewsForAccount = null;
let disableRoomPreviewUrls; if (SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled")) {
if (maySetRoomPreviewUrls) { previewsForAccount = (
disableRoomPreviewUrls =
<label>
<input type="checkbox" ref="globalDisableUrlPreview"
onChange={this.onGlobalDisableUrlPreviewChange}
checked={this.state.globalDisableUrlPreview} />
{ _t("Disable URL previews by default for participants in this room") }
</label>;
} else {
disableRoomPreviewUrls =
<label>
{ _t("URL previews are %(globalDisableUrlPreview)s by default for participants in this room.", {globalDisableUrlPreview: this.state.globalDisableUrlPreview ? _t("disabled") : _t("enabled")}) }
</label>;
}
let urlPreviewText = null;
if (UserSettingsStore.getUrlPreviewsDisabled()) {
urlPreviewText = (
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
);
} else {
urlPreviewText = (
_tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>) _tJsx("You have <a>enabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
); );
} else {
previewsForAccount = (
_tJsx("You have <a>disabled</a> URL previews by default.", /<a>(.*?)<\/a>/, (sub)=><a href="#/settings">{ sub }</a>)
);
} }
let previewsForRoom = null;
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, "room")) {
previewsForRoom = (
<label>
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM}
roomId={this.props.room.roomId}
isExplicit={true}
manualSave={true}
ref="urlPreviewsRoom" />
</label>
);
} else {
let str = "URL previews are enabled by default for participants in this room.";
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled")) {
str = "URL previews are disabled by default for participants in this room.";
}
previewsForRoom = (<label>{ _t(str) }</label>);
}
let previewsForRoomAccount = (
<SettingsFlag name="urlPreviewsEnabled"
level={SettingLevel.ROOM_ACCOUNT}
roomId={this.props.room.roomId}
manualSave={true}
ref="urlPreviewsSelf"
/>
);
return ( return (
<div className="mx_RoomSettings_toggles"> <div className="mx_RoomSettings_toggles">
<h3>{ _t("URL Previews") }</h3> <h3>{ _t("URL Previews") }</h3>
<label> <label>{ previewsForAccount }</label>
{ urlPreviewText } { previewsForRoom }
</label> <label>{ previewsForRoomAccount }</label>
{ disableRoomPreviewUrls }
<label>
<input type="checkbox" ref="userEnableUrlPreview"
onChange={this.onUserEnableUrlPreviewChange}
checked={this.state.userEnableUrlPreview} />
{ _t("Enable URL previews for this room (affects only you)") }
</label>
<label>
<input type="checkbox" ref="userDisableUrlPreview"
onChange={this.onUserDisableUrlPreviewChange}
checked={this.state.userDisableUrlPreview} />
{ _t("Disable URL previews for this room (affects only you)") }
</label>
</div> </div>
); );
}, },

View file

@ -24,9 +24,10 @@ import isEqual from 'lodash/isEqual';
import sdk from '../../../index'; import sdk from '../../../index';
import type {Completion} from '../../../autocomplete/Autocompleter'; import type {Completion} from '../../../autocomplete/Autocompleter';
import Promise from 'bluebird'; import Promise from 'bluebird';
import UserSettingsStore from '../../../UserSettingsStore';
import { Room } from 'matrix-js-sdk'; import { Room } from 'matrix-js-sdk';
import {getCompletions} from '../../../autocomplete/Autocompleter';
import SettingsStore from "../../../settings/SettingsStore";
import Autocompleter from '../../../autocomplete/Autocompleter'; import Autocompleter from '../../../autocomplete/Autocompleter';
const COMPOSER_SELECTED = 0; const COMPOSER_SELECTED = 0;
@ -95,7 +96,7 @@ export default class Autocomplete extends React.Component {
}); });
return Promise.resolve(null); return Promise.resolve(null);
} }
let autocompleteDelay = UserSettingsStore.getLocalSetting('autocompleteDelay', 200); let autocompleteDelay = SettingsStore.getValue("autocompleteDelay");
// Don't debounce if we are already showing completions // Don't debounce if we are already showing completions
if (this.state.completions.length > 0 || this.state.forceComplete) { if (this.state.completions.length > 0 || this.state.forceComplete) {

View file

@ -22,7 +22,6 @@ import dis from "../../../dispatcher";
import ObjectUtils from '../../../ObjectUtils'; import ObjectUtils from '../../../ObjectUtils';
import AppsDrawer from './AppsDrawer'; import AppsDrawer from './AppsDrawer';
import { _t, _tJsx} from '../../../languageHandler'; import { _t, _tJsx} from '../../../languageHandler';
import UserSettingsStore from '../../../UserSettingsStore';
module.exports = React.createClass({ module.exports = React.createClass({

View file

@ -494,7 +494,6 @@ module.exports = withMatrixClient(React.createClass({
const defaultPerms = { const defaultPerms = {
can: {}, can: {},
muted: false, muted: false,
modifyLevel: false,
}; };
const room = this.props.matrixClient.getRoom(member.roomId); const room = this.props.matrixClient.getRoom(member.roomId);
if (!room) return defaultPerms; if (!room) return defaultPerms;
@ -516,13 +515,15 @@ module.exports = withMatrixClient(React.createClass({
}, },
_calculateCanPermissions: function(me, them, powerLevels) { _calculateCanPermissions: function(me, them, powerLevels) {
const isMe = me.userId === them.userId;
const can = { const can = {
kick: false, kick: false,
ban: false, ban: false,
mute: false, mute: false,
modifyLevel: false, modifyLevel: false,
modifyLevelMax: 0,
}; };
const canAffectUser = them.powerLevel < me.powerLevel; const canAffectUser = them.powerLevel < me.powerLevel || isMe;
if (!canAffectUser) { if (!canAffectUser) {
//console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel); //console.log("Cannot affect user: %s >= %s", them.powerLevel, me.powerLevel);
return can; return can;
@ -531,16 +532,13 @@ module.exports = withMatrixClient(React.createClass({
(powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) || (powerLevels.events ? powerLevels.events["m.room.power_levels"] : null) ||
powerLevels.state_default powerLevels.state_default
); );
const levelToSend = (
(powerLevels.events ? powerLevels.events["m.room.message"] : null) ||
powerLevels.events_default
);
can.kick = me.powerLevel >= powerLevels.kick; can.kick = me.powerLevel >= powerLevels.kick;
can.ban = me.powerLevel >= powerLevels.ban; can.ban = me.powerLevel >= powerLevels.ban;
can.mute = me.powerLevel >= editPowerLevel; can.mute = me.powerLevel >= editPowerLevel;
can.toggleMod = me.powerLevel > them.powerLevel && them.powerLevel >= levelToSend; can.modifyLevel = me.powerLevel >= editPowerLevel && (isMe || me.powerLevel > them.powerLevel);
can.modifyLevel = me.powerLevel > them.powerLevel && me.powerLevel >= editPowerLevel; can.modifyLevelMax = me.powerLevel;
return can; return can;
}, },
@ -832,8 +830,11 @@ module.exports = withMatrixClient(React.createClass({
presenceCurrentlyActive = this.props.member.user.currentlyActive; presenceCurrentlyActive = this.props.member.user.currentlyActive;
} }
let roomMemberDetails = null; const room = this.props.matrixClient.getRoom(this.props.member.roomId);
const poweLevelEvent = room ? room.currentState.getStateEvents("m.room.power_levels", "") : null;
const powerLevelUsersDefault = poweLevelEvent.getContent().users_default;
let roomMemberDetails = null;
if (this.props.member.roomId) { // is in room if (this.props.member.roomId) { // is in room
const PowerSelector = sdk.getComponent('elements.PowerSelector'); const PowerSelector = sdk.getComponent('elements.PowerSelector');
const PresenceLabel = sdk.getComponent('rooms.PresenceLabel'); const PresenceLabel = sdk.getComponent('rooms.PresenceLabel');
@ -842,7 +843,9 @@ module.exports = withMatrixClient(React.createClass({
{ _t("Level:") } <b> { _t("Level:") } <b>
<PowerSelector controlled={true} <PowerSelector controlled={true}
value={parseInt(this.props.member.powerLevel)} value={parseInt(this.props.member.powerLevel)}
maxValue={this.state.can.modifyLevelMax}
disabled={!this.state.can.modifyLevel} disabled={!this.state.can.modifyLevel}
usersDefault={powerLevelUsersDefault}
onChange={this.onPowerChange} /> onChange={this.onPowerChange} />
</b> </b>
</div> </div>

View file

@ -22,7 +22,7 @@ import Modal from '../../../Modal';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import Autocomplete from './Autocomplete'; import Autocomplete from './Autocomplete';
import UserSettingsStore from '../../../UserSettingsStore'; import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class MessageComposer extends React.Component { export default class MessageComposer extends React.Component {
@ -49,10 +49,10 @@ export default class MessageComposer extends React.Component {
inputState: { inputState: {
style: [], style: [],
blockType: null, blockType: null,
isRichtextEnabled: UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false), isRichtextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
wordCount: 0, wordCount: 0,
}, },
showFormatting: UserSettingsStore.getSyncedSetting('MessageComposer.showFormatting', false), showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
}; };
} }
@ -226,7 +226,7 @@ export default class MessageComposer extends React.Component {
} }
onToggleFormattingClicked() { onToggleFormattingClicked() {
UserSettingsStore.setSyncedSetting('MessageComposer.showFormatting', !this.state.showFormatting); SettingsStore.setValue("MessageComposer.showFormatting", null, SettingLevel.DEVICE, !this.state.showFormatting);
this.setState({showFormatting: !this.state.showFormatting}); this.setState({showFormatting: !this.state.showFormatting});
} }
@ -238,7 +238,7 @@ export default class MessageComposer extends React.Component {
render() { render() {
const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId); const me = this.props.room.getMember(MatrixClientPeg.get().credentials.userId);
const uploadInputStyle = {display: 'none'}; const uploadInputStyle = {display: 'none'};
const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); const MemberPresenceAvatar = sdk.getComponent('avatars.MemberPresenceAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg"); const TintableSvg = sdk.getComponent("elements.TintableSvg");
const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput"); const MessageComposerInput = sdk.getComponent("rooms.MessageComposerInput");
@ -246,7 +246,7 @@ export default class MessageComposer extends React.Component {
controls.push( controls.push(
<div key="controls_avatar" className="mx_MessageComposer_avatar"> <div key="controls_avatar" className="mx_MessageComposer_avatar">
<MemberAvatar member={me} width={24} height={24} /> <MemberPresenceAvatar member={me} width={24} height={24} />
</div>, </div>,
); );

View file

@ -35,7 +35,6 @@ import { _t, _td } from '../../../languageHandler';
import Analytics from '../../../Analytics'; import Analytics from '../../../Analytics';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';
import * as RichText from '../../../RichText'; import * as RichText from '../../../RichText';
import * as HtmlUtils from '../../../HtmlUtils'; import * as HtmlUtils from '../../../HtmlUtils';
@ -50,6 +49,7 @@ const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN);
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g'); const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione'; import {asciiRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort} from 'emojione';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
const EMOJI_SHORTNAMES = Object.keys(emojioneList); const EMOJI_SHORTNAMES = Object.keys(emojioneList);
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort(); const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$'); const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
@ -158,7 +158,7 @@ export default class MessageComposerInput extends React.Component {
this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this); this.onMarkdownToggleClicked = this.onMarkdownToggleClicked.bind(this);
this.onTextPasted = this.onTextPasted.bind(this); this.onTextPasted = this.onTextPasted.bind(this);
const isRichtextEnabled = UserSettingsStore.getSyncedSetting('MessageComposerInput.isRichTextEnabled', false); const isRichtextEnabled = SettingsStore.getValue('MessageComposerInput.isRichTextEnabled');
Analytics.setRichtextMode(isRichtextEnabled); Analytics.setRichtextMode(isRichtextEnabled);
@ -209,7 +209,7 @@ export default class MessageComposerInput extends React.Component {
createEditorState(richText: boolean, contentState: ?ContentState): EditorState { createEditorState(richText: boolean, contentState: ?ContentState): EditorState {
const decorators = richText ? RichText.getScopedRTDecorators(this.props) : const decorators = richText ? RichText.getScopedRTDecorators(this.props) :
RichText.getScopedMDDecorators(this.props); RichText.getScopedMDDecorators(this.props);
const shouldShowPillAvatar = !UserSettingsStore.getSyncedSetting("Pill.shouldHidePillAvatar", false); const shouldShowPillAvatar = !SettingsStore.getValue("Pill.shouldHidePillAvatar");
decorators.push({ decorators.push({
strategy: this.findPillEntities.bind(this), strategy: this.findPillEntities.bind(this),
component: (entityProps) => { component: (entityProps) => {
@ -377,7 +377,7 @@ export default class MessageComposerInput extends React.Component {
} }
sendTyping(isTyping) { sendTyping(isTyping) {
if (UserSettingsStore.getSyncedSetting('dontSendTypingNotifications', false)) return; if (SettingsStore.getValue('dontSendTypingNotifications')) return;
MatrixClientPeg.get().sendTyping( MatrixClientPeg.get().sendTyping(
this.props.room.roomId, this.props.room.roomId,
this.isTyping, TYPING_SERVER_TIMEOUT, this.isTyping, TYPING_SERVER_TIMEOUT,
@ -424,7 +424,7 @@ export default class MessageComposerInput extends React.Component {
} }
// Automatic replacement of plaintext emoji to Unicode emoji // Automatic replacement of plaintext emoji to Unicode emoji
if (UserSettingsStore.getSyncedSetting('MessageComposerInput.autoReplaceEmoji', false)) { if (SettingsStore.getValue('MessageComposerInput.autoReplaceEmoji')) {
// The first matched group includes just the matched plaintext emoji // The first matched group includes just the matched plaintext emoji
const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset)); const emojiMatch = REGEX_EMOJI_WHITESPACE.exec(text.slice(0, currentStartOffset));
if(emojiMatch) { if(emojiMatch) {
@ -544,7 +544,7 @@ export default class MessageComposerInput extends React.Component {
editorState: this.createEditorState(enabled, contentState), editorState: this.createEditorState(enabled, contentState),
isRichtextEnabled: enabled, isRichtextEnabled: enabled,
}); });
UserSettingsStore.setSyncedSetting('MessageComposerInput.isRichTextEnabled', enabled); SettingsStore.setValue("MessageComposerInput.isRichTextEnabled", null, SettingLevel.ACCOUNT, enabled);
} }
handleKeyCommand = (command: string): boolean => { handleKeyCommand = (command: string): boolean => {

View file

@ -31,7 +31,7 @@ import linkifyMatrix from '../../../linkify-matrix';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import ManageIntegsButton from '../elements/ManageIntegsButton'; import ManageIntegsButton from '../elements/ManageIntegsButton';
import {CancelButton} from './SimpleRoomHeader'; import {CancelButton} from './SimpleRoomHeader';
import UserSettingsStore from "../../../UserSettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -339,7 +339,7 @@ module.exports = React.createClass({
</AccessibleButton>; </AccessibleButton>;
} }
if (this.props.onPinnedClick && UserSettingsStore.isFeatureEnabled('feature_pinning')) { if (this.props.onPinnedClick && SettingsStore.isFeatureEnabled('feature_pinning')) {
let pinsIndicator = null; let pinsIndicator = null;
if (this._hasUnreadPins()) { if (this._hasUnreadPins()) {
pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />); pinsIndicator = (<div className="mx_RoomHeader_pinsIndicator mx_RoomHeader_pinsIndicatorUnread" />);

View file

@ -23,8 +23,8 @@ import sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import ObjectUtils from '../../../ObjectUtils'; import ObjectUtils from '../../../ObjectUtils';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore';
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
// parse a string as an integer; if the input is undefined, or cannot be parsed // parse a string as an integer; if the input is undefined, or cannot be parsed
@ -309,9 +309,9 @@ module.exports = React.createClass({
} }
// url preview settings // url preview settings
const ps = this.saveUrlPreviewSettings(); let ps = this.saveUrlPreviewSettings();
if (ps.length > 0) { if (ps.length > 0) {
promises.push(ps); ps.map(p => promises.push(p));
} }
// related groups // related groups
@ -363,26 +363,16 @@ module.exports = React.createClass({
}, },
saveBlacklistUnverifiedDevicesPerRoom: function() { saveBlacklistUnverifiedDevicesPerRoom: function() {
if (!this.refs.blacklistUnverified) return; if (!this.refs.blacklistUnverifiedDevices) return;
if (this._isRoomBlacklistUnverified() !== this.refs.blacklistUnverified.checked) { this.refs.blacklistUnverifiedDevices.save().then(() => {
this._setRoomBlacklistUnverified(this.refs.blacklistUnverified.checked); const value = SettingsStore.getValueAt(
} SettingLevel.ROOM_DEVICE,
}, "blacklistUnverifiedDevices",
this.props.room.roomId,
_isRoomBlacklistUnverified: function() { /*explicit=*/true,
const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom; );
if (blacklistUnverifiedDevicesPerRoom) {
return blacklistUnverifiedDevicesPerRoom[this.props.room.roomId];
}
return false;
},
_setRoomBlacklistUnverified: function(value) {
const blacklistUnverifiedDevicesPerRoom = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevicesPerRoom || {};
blacklistUnverifiedDevicesPerRoom[this.props.room.roomId] = value;
UserSettingsStore.setLocalSetting('blacklistUnverifiedDevicesPerRoom', blacklistUnverifiedDevicesPerRoom);
this.props.room.setBlacklistUnverifiedDevices(value); this.props.room.setBlacklistUnverifiedDevices(value);
});
}, },
_hasDiff: function(strA, strB) { _hasDiff: function(strA, strB) {
@ -588,19 +578,20 @@ module.exports = React.createClass({
}, },
_renderEncryptionSection: function() { _renderEncryptionSection: function() {
const SettingsFlag = sdk.getComponent("elements.SettingsFlag");
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const roomState = this.props.room.currentState; const roomState = this.props.room.currentState;
const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId); const isEncrypted = cli.isRoomEncrypted(this.props.room.roomId);
const isGlobalBlacklistUnverified = UserSettingsStore.getLocalSettings().blacklistUnverifiedDevices;
const isRoomBlacklistUnverified = this._isRoomBlacklistUnverified();
const settings = let settings = (
<label> <SettingsFlag name="blacklistUnverifiedDevices"
<input type="checkbox" ref="blacklistUnverified" level={SettingLevel.ROOM_DEVICE}
defaultChecked={isGlobalBlacklistUnverified || isRoomBlacklistUnverified} roomId={this.props.room.roomId}
disabled={isGlobalBlacklistUnverified || (this.refs.encrypt && !this.refs.encrypt.checked)} /> manualSave={true}
{ _t('Never send encrypted messages to unverified devices in this room from this device') }. ref="blacklistUnverifiedDevices"
</label>; />
);
if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) { if (!isEncrypted && roomState.mayClientSendStateEvent("m.room.encryption", cli)) {
return ( return (
@ -910,31 +901,31 @@ module.exports = React.createClass({
<div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings"> <div className="mx_RoomSettings_powerLevels mx_RoomSettings_settings">
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('The default role for new room members is') } </span>
<PowerSelector ref="users_default" value={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="users_default" value={default_user_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < default_user_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To send messages, you must be a') } </span>
<PowerSelector ref="events_default" value={send_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="events_default" value={send_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < send_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To invite users into the room, you must be a') } </span>
<PowerSelector ref="invite" value={invite_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="invite" value={invite_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < invite_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To configure the room, you must be a') } </span>
<PowerSelector ref="state_default" value={state_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="state_default" value={state_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < state_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To kick users, you must be a') } </span>
<PowerSelector ref="kick" value={kick_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="kick" value={kick_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < kick_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To ban users, you must be a') } </span>
<PowerSelector ref="ban" value={ban_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="ban" value={ban_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < ban_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
<div className="mx_RoomSettings_powerLevel"> <div className="mx_RoomSettings_powerLevel">
<span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span> <span className="mx_RoomSettings_powerLevelKey">{ _t('To remove other users\' messages, you must be a') } </span>
<PowerSelector ref="redact" value={redact_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} /> <PowerSelector ref="redact" value={redact_level} usersDefault={default_user_level} controlled={false} disabled={!can_change_levels || current_user_level < redact_level} onChange={this.onPowerLevelsChanged} />
</div> </div>
{ Object.keys(events_levels).map(function(event_type, i) { { Object.keys(events_levels).map(function(event_type, i) {
@ -944,7 +935,7 @@ module.exports = React.createClass({
return ( return (
<div className="mx_RoomSettings_powerLevel" key={event_type}> <div className="mx_RoomSettings_powerLevel" key={event_type}>
<span className="mx_RoomSettings_powerLevelKey">{ label } </span> <span className="mx_RoomSettings_powerLevelKey">{ label } </span>
<PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} onChange={self.onPowerLevelsChanged} <PowerSelector ref={"event_levels_"+event_type} value={events_levels[event_type]} usersDefault={default_user_level} onChange={self.onPowerLevelsChanged}
controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} /> controlled={false} disabled={!can_change_levels || current_user_level < events_levels[event_type]} />
</div> </div>
); );

View file

@ -27,7 +27,6 @@ const ContextualMenu = require('../../structures/ContextualMenu');
const RoomNotifs = require('../../../RoomNotifs'); const RoomNotifs = require('../../../RoomNotifs');
const FormattingUtils = require('../../../utils/FormattingUtils'); const FormattingUtils = require('../../../utils/FormattingUtils');
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
const UserSettingsStore = require('../../../UserSettingsStore');
import ActiveRoomObserver from '../../../ActiveRoomObserver'; import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';

View file

@ -23,7 +23,7 @@ import classNames from 'classnames';
import sdk from '../../../index'; import sdk from '../../../index';
import dis from '../../../dispatcher'; import dis from '../../../dispatcher';
import UserSettingsStore from '../../../UserSettingsStore'; import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({ module.exports = React.createClass({
displayName: 'VideoView', displayName: 'VideoView',
@ -113,7 +113,7 @@ module.exports = React.createClass({
const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight; const maxVideoHeight = fullscreenElement ? null : this.props.maxHeight;
const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed", const localVideoFeedClasses = classNames("mx_VideoView_localVideoFeed",
{ "mx_VideoView_localVideoFeed_flipped": { "mx_VideoView_localVideoFeed_flipped":
UserSettingsStore.getSyncedSetting('VideoView.flipVideoHorizontally', false), SettingsStore.getValue('VideoView.flipVideoHorizontally'),
}, },
); );
return ( return (

View file

@ -66,7 +66,7 @@
"This email address was not found": "This email address was not found", "This email address was not found": "This email address was not found",
"Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.", "Your email address does not appear to be associated with a Matrix ID on this Homeserver.": "Your email address does not appear to be associated with a Matrix ID on this Homeserver.",
"Default": "Default", "Default": "Default",
"User": "User", "Restricted": "Restricted",
"Moderator": "Moderator", "Moderator": "Moderator",
"Admin": "Admin", "Admin": "Admin",
"Start a chat": "Start a chat", "Start a chat": "Start a chat",
@ -154,6 +154,7 @@
"%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s", "%(widgetName)s widget added by %(senderName)s": "%(widgetName)s widget added by %(senderName)s",
"%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s", "%(widgetName)s widget removed by %(senderName)s": "%(widgetName)s widget removed by %(senderName)s",
"Message Pinning": "Message Pinning", "Message Pinning": "Message Pinning",
"Presence Management": "Presence Management",
"%(displayName)s is typing": "%(displayName)s is typing", "%(displayName)s is typing": "%(displayName)s is typing",
"%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing", "%(names)s and %(count)s others are typing|other": "%(names)s and %(count)s others are typing",
"%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing", "%(names)s and %(count)s others are typing|one": "%(names)s and one other is typing",
@ -165,6 +166,11 @@
"Not a valid Riot keyfile": "Not a valid Riot keyfile", "Not a valid Riot keyfile": "Not a valid Riot keyfile",
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?", "Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
"Failed to join room": "Failed to join room", "Failed to join room": "Failed to join room",
"Hide avatar changes": "Hide avatar changes",
"Hide display name changes": "Hide display name changes",
"Enable inline URL previews by default": "Enable inline URL previews by default",
"Enable URL previews for this room (only affects you)": "Enable URL previews for this room (only affects you)",
"Enable URL previews by default for participants in this room": "Enable URL previews by default for participants in this room",
"Active call (%(roomName)s)": "Active call (%(roomName)s)", "Active call (%(roomName)s)": "Active call (%(roomName)s)",
"unknown caller": "unknown caller", "unknown caller": "unknown caller",
"Incoming voice call from %(name)s": "Incoming voice call from %(name)s", "Incoming voice call from %(name)s": "Incoming voice call from %(name)s",
@ -205,6 +211,9 @@
"Delete": "Delete", "Delete": "Delete",
"Disable Notifications": "Disable Notifications", "Disable Notifications": "Disable Notifications",
"Enable Notifications": "Enable Notifications", "Enable Notifications": "Enable Notifications",
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
"URL Previews": "URL Previews",
"Cannot add any more widgets": "Cannot add any more widgets", "Cannot add any more widgets": "Cannot add any more widgets",
"The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.",
"Add a widget": "Add a widget", "Add a widget": "Add a widget",
@ -432,15 +441,6 @@
"Related communities for this room:": "Related communities for this room:", "Related communities for this room:": "Related communities for this room:",
"This room has no related communities": "This room has no related communities", "This room has no related communities": "This room has no related communities",
"New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)", "New community ID (e.g. +foo:%(localDomain)s)": "New community ID (e.g. +foo:%(localDomain)s)",
"Disable URL previews by default for participants in this room": "Disable URL previews by default for participants in this room",
"URL previews are %(globalDisableUrlPreview)s by default for participants in this room.": "URL previews are %(globalDisableUrlPreview)s by default for participants in this room.",
"disabled": "disabled",
"enabled": "enabled",
"You have <a>disabled</a> URL previews by default.": "You have <a>disabled</a> URL previews by default.",
"You have <a>enabled</a> URL previews by default.": "You have <a>enabled</a> URL previews by default.",
"URL Previews": "URL Previews",
"Enable URL previews for this room (affects only you)": "Enable URL previews for this room (affects only you)",
"Disable URL previews for this room (affects only you)": "Disable URL previews for this room (affects only you)",
"Error decrypting audio": "Error decrypting audio", "Error decrypting audio": "Error decrypting audio",
"Error decrypting attachment": "Error decrypting attachment", "Error decrypting attachment": "Error decrypting attachment",
"Decrypt %(text)s": "Decrypt %(text)s", "Decrypt %(text)s": "Decrypt %(text)s",
@ -484,6 +484,7 @@
"Sign in with": "Sign in with", "Sign in with": "Sign in with",
"Email address": "Email address", "Email address": "Email address",
"Sign in": "Sign in", "Sign in": "Sign in",
"Sign in to get started": "Sign in to get started",
"If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?",
"Email address (optional)": "Email address (optional)", "Email address (optional)": "Email address (optional)",
"You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s", "You are registering with %(SelectedTeamName)s": "You are registering with %(SelectedTeamName)s",
@ -583,6 +584,7 @@
"%(items)s and %(count)s others|other": "%(items)s and %(count)s others", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others",
"%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(count)s others|one": "%(items)s and one other",
"%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s",
"Custom of %(powerLevel)s": "Custom of %(powerLevel)s",
"Custom level": "Custom level", "Custom level": "Custom level",
"Room directory": "Room directory", "Room directory": "Room directory",
"Start chat": "Start chat", "Start chat": "Start chat",
@ -769,7 +771,6 @@
"Always show message timestamps": "Always show message timestamps", "Always show message timestamps": "Always show message timestamps",
"Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)",
"Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)", "Hide join/leave messages (invites/kicks/bans unaffected)": "Hide join/leave messages (invites/kicks/bans unaffected)",
"Hide avatar and display name changes": "Hide avatar and display name changes",
"Use compact timeline layout": "Use compact timeline layout", "Use compact timeline layout": "Use compact timeline layout",
"Hide removed messages": "Hide removed messages", "Hide removed messages": "Hide removed messages",
"Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting",
@ -931,5 +932,8 @@
"This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.", "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.": "This process allows you to import encryption keys that you had previously exported from another Matrix client. You will then be able to decrypt any messages that the other client could decrypt.",
"The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.", "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.": "The export file will be protected with a passphrase. You should enter the passphrase here, to decrypt the file.",
"File to import": "File to import", "File to import": "File to import",
"Import": "Import" "Import": "Import",
"Status.im theme": "Status.im theme",
"Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.",
"Username on %(hs)s": "Username on %(hs)s"
} }

View file

@ -19,8 +19,7 @@ import request from 'browser-request';
import counterpart from 'counterpart'; import counterpart from 'counterpart';
import Promise from 'bluebird'; import Promise from 'bluebird';
import React from 'react'; import React from 'react';
import SettingsStore, {SettingLevel} from "./settings/SettingsStore";
import UserSettingsStore from './UserSettingsStore';
const i18nFolder = 'i18n/'; const i18nFolder = 'i18n/';
@ -168,7 +167,7 @@ export function setLanguage(preferredLangs) {
}).then((langData) => { }).then((langData) => {
counterpart.registerTranslations(langToUse, langData); counterpart.registerTranslations(langToUse, langData);
counterpart.setLocale(langToUse); counterpart.setLocale(langToUse);
UserSettingsStore.setLocalSetting('language', langToUse); SettingsStore.setValue("language", null, SettingLevel.DEVICE, langToUse);
console.log("set language to " + langToUse); console.log("set language to " + langToUse);
// Set 'en' as fallback language: // Set 'en' as fallback language:

252
src/settings/Settings.js Normal file
View file

@ -0,0 +1,252 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {_td} from '../languageHandler';
import {
AudioNotificationsEnabledController,
NotificationBodyEnabledController,
NotificationsEnabledController,
} from "./controllers/NotificationControllers";
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config'];
const LEVELS_ROOM_SETTINGS_WITH_ROOM = ['device', 'room-device', 'room-account', 'account', 'config', 'room'];
const LEVELS_ACCOUNT_SETTINGS = ['device', 'account', 'config'];
const LEVELS_FEATURE = ['device', 'config'];
const LEVELS_DEVICE_ONLY_SETTINGS = ['device'];
const LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG = ['device', 'config'];
export const SETTINGS = {
// EXAMPLE SETTING:
// "my-setting": {
// // Must be set to true for features. Default is 'false'.
// isFeature: false,
//
// // Display names are strongly recommended for clarity.
// displayName: _td("Cool Name"),
//
// // Display name can also be an object for different levels.
// //displayName: {
// // "device": _td("Name for when the setting is used at 'device'"),
// // "room": _td("Name for when the setting is used at 'room'"),
// // "default": _td("The name for all other levels"),
// //}
//
// // The supported levels are required. Preferably, use the preset arrays
// // at the top of this file to define this rather than a custom array.
// supportedLevels: [
// // The order does not matter.
//
// "device", // Affects the current device only
// "room-device", // Affects the current room on the current device
// "room-account", // Affects the current room for the current account
// "account", // Affects the current account
// "room", // Affects the current room (controlled by room admins)
// "config", // Affects the current application
//
// // "default" is always supported and does not get listed here.
// ],
//
// // Required. Can be any data type. The value specified here should match
// // the data being stored (ie: if a boolean is used, the setting should
// // represent a boolean).
// default: {
// your: "value",
// },
//
// // Optional settings controller. See SettingsController for more information.
// controller: new MySettingController(),
//
// // Optional flag to make supportedLevels be respected as the order to handle
// // settings. The first element is treated as "most preferred". The "default"
// // level is always appended to the end.
// supportedLevelsAreOrdered: false,
// },
"feature_pinning": {
isFeature: true,
displayName: _td("Message Pinning"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"feature_presence_management": {
isFeature: true,
displayName: _td("Presence Management"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"MessageComposerInput.dontSuggestEmoji": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Disable Emoji suggestions while typing'),
default: false,
},
"useCompactLayout": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Use compact timeline layout'),
default: false,
},
"hideRedactions": {
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
displayName: _td('Hide removed messages'),
default: false,
},
"hideJoinLeaves": {
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
displayName: _td('Hide join/leave messages (invites/kicks/bans unaffected)'),
default: false,
},
"hideAvatarChanges": {
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
displayName: _td('Hide avatar changes'),
default: false,
},
"hideDisplaynameChanges": {
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
displayName: _td('Hide display name changes'),
default: false,
},
"hideReadReceipts": {
supportedLevels: LEVELS_ROOM_SETTINGS,
displayName: _td('Hide read receipts'),
default: false,
},
"showTwelveHourTimestamps": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Show timestamps in 12 hour format (e.g. 2:30pm)'),
default: false,
},
"alwaysShowTimestamps": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Always show message timestamps'),
default: false,
},
"autoplayGifsAndVideos": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Autoplay GIFs and videos'),
default: false,
},
"enableSyntaxHighlightLanguageDetection": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Enable automatic language detection for syntax highlighting'),
default: false,
},
"Pill.shouldHidePillAvatar": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Hide avatars in user and room mentions'),
default: false,
},
"TextualBody.disableBigEmoji": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Disable big emoji in chat'),
default: false,
},
"MessageComposerInput.isRichTextEnabled": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: false,
},
"MessageComposer.showFormatting": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: false,
},
"dontSendTypingNotifications": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td("Don't send typing notifications"),
default: false,
},
"MessageComposerInput.autoReplaceEmoji": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Automatically replace plain text Emoji'),
default: false,
},
"VideoView.flipVideoHorizontally": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Mirror local video feed'),
default: false,
},
"theme": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: "light",
},
"webRtcForceTURN": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td('Disable Peer-to-Peer for 1:1 calls'),
default: false,
},
"webrtc_audioinput": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: null,
},
"webrtc_videoinput": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: null,
},
"language": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: "en",
},
"analyticsOptOut": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
displayName: _td('Opt out of analytics'),
default: false,
},
"autocompleteDelay": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
default: 200,
},
"blacklistUnverifiedDevices": {
// We specifically want to have room-device > device so that users may set a device default
// with a per-room override.
supportedLevels: ['room-device', 'device'],
supportedLevelsAreOrdered: true,
displayName: {
"default": _td('Never send encrypted messages to unverified devices from this device'),
"room-device": _td('Never send encrypted messages to unverified devices in this room from this device'),
},
default: false,
},
"urlPreviewsEnabled": {
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
displayName: {
"default": _td('Enable inline URL previews by default'),
"room-account": _td("Enable URL previews for this room (only affects you)"),
"room": _td("Enable URL previews by default for participants in this room"),
},
default: true,
},
"roomColor": {
supportedLevels: LEVELS_ROOM_SETTINGS_WITH_ROOM,
displayName: _td("Room Colour"),
default: {
primary_color: null, // Hex string, eg: #000000
secondary_color: null, // Hex string, eg: #000000
},
},
"notificationsEnabled": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
controller: new NotificationsEnabledController(),
},
"notificationBodyEnabled": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: true,
controller: new NotificationBodyEnabledController(),
},
"audioNotificationsEnabled": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: true,
controller: new AudioNotificationsEnabledController(),
},
};

View file

@ -0,0 +1,347 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import DeviceSettingsHandler from "./handlers/DeviceSettingsHandler";
import RoomDeviceSettingsHandler from "./handlers/RoomDeviceSettingsHandler";
import DefaultSettingsHandler from "./handlers/DefaultSettingsHandler";
import RoomAccountSettingsHandler from "./handlers/RoomAccountSettingsHandler";
import AccountSettingsHandler from "./handlers/AccountSettingsHandler";
import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
import {_t} from '../languageHandler';
import SdkConfig from "../SdkConfig";
import {SETTINGS} from "./Settings";
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
/**
* Represents the various setting levels supported by the SettingsStore.
*/
export const SettingLevel = {
// Note: This enum is not used in this class or in the Settings file
// This should always be used elsewhere in the project.
DEVICE: "device",
ROOM_DEVICE: "room-device",
ROOM_ACCOUNT: "room-account",
ACCOUNT: "account",
ROOM: "room",
CONFIG: "config",
DEFAULT: "default",
};
// Convert the settings to easier to manage objects for the handlers
const defaultSettings = {};
const featureNames = [];
for (const key of Object.keys(SETTINGS)) {
defaultSettings[key] = SETTINGS[key].default;
if (SETTINGS[key].isFeature) featureNames.push(key);
}
const LEVEL_HANDLERS = {
"device": new DeviceSettingsHandler(featureNames),
"room-device": new RoomDeviceSettingsHandler(),
"room-account": new RoomAccountSettingsHandler(),
"account": new AccountSettingsHandler(),
"room": new RoomSettingsHandler(),
"config": new ConfigSettingsHandler(),
"default": new DefaultSettingsHandler(defaultSettings),
};
// Wrap all the handlers with local echo
for (const key of Object.keys(LEVEL_HANDLERS)) {
LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]);
}
const LEVEL_ORDER = [
'device', 'room-device', 'room-account', 'account', 'room', 'config', 'default',
];
/**
* Controls and manages application settings by providing varying levels at which the
* setting value may be specified. The levels are then used to determine what the setting
* value should be given a set of circumstances. The levels, in priority order, are:
* - "device" - Values are determined by the current device
* - "room-device" - Values are determined by the current device for a particular room
* - "room-account" - Values are determined by the current account for a particular room
* - "account" - Values are determined by the current account
* - "room" - Values are determined by a particular room (by the room admins)
* - "config" - Values are determined by the config.json
* - "default" - Values are determined by the hardcoded defaults
*
* Each level has a different method to storing the setting value. For implementation
* specific details, please see the handlers. The "config" and "default" levels are
* both always supported on all platforms. All other settings should be guarded by
* isLevelSupported() prior to attempting to set the value.
*
* Settings can also represent features. Features are significant portions of the
* application that warrant a dedicated setting to toggle them on or off. Features are
* special-cased to ensure that their values respect the configuration (for example, a
* feature may be reported as disabled even though a user has specifically requested it
* be enabled).
*/
export default class SettingsStore {
/**
* Gets the translated display name for a given setting
* @param {string} settingName The setting to look up.
* @param {"device"|"room-device"|"room-account"|"account"|"room"|"config"|"default"} atLevel
* The level to get the display name for; Defaults to 'default'.
* @return {String} The display name for the setting, or null if not found.
*/
static getDisplayName(settingName, atLevel = "default") {
if (!SETTINGS[settingName] || !SETTINGS[settingName].displayName) return null;
let displayName = SETTINGS[settingName].displayName;
if (displayName instanceof Object) {
if (displayName[atLevel]) displayName = displayName[atLevel];
else displayName = displayName["default"];
}
return _t(displayName);
}
/**
* Returns a list of all available labs feature names
* @returns {string[]} The list of available feature names
*/
static getLabsFeatures() {
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.
* @param {string} settingName The setting to look up.
* @return {boolean} True if the setting is a feature.
*/
static isFeature(settingName) {
if (!SETTINGS[settingName]) return false;
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
*/
static isFeatureEnabled(settingName, roomId = 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.
*/
static setFeatureEnabled(settingName, value) {
// 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, "device", value);
}
/**
* 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.
* @param {string} settingName The name of the setting to read the value of.
* @param {String} roomId The room ID to read the setting value in, may be null.
* @param {boolean} excludeDefault True to disable using the default value.
* @return {*} The value, or null if not found
*/
static getValue(settingName, roomId = null, excludeDefault = false) {
return SettingsStore.getValueAt(LEVEL_ORDER[0], settingName, roomId, false, excludeDefault);
}
/**
* Gets a setting's value at a particular level, ignoring all levels that are more specific.
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to
* look at.
* @param {string} settingName The name of the setting to read.
* @param {String} roomId The room ID to read the setting value in, may be null.
* @param {boolean} explicit If true, this method will not consider other levels, just the one
* provided. Defaults to false.
* @param {boolean} excludeDefault True to disable using the default value.
* @return {*} The value, or null if not found.
*/
static getValueAt(level, settingName, roomId = null, explicit = false, excludeDefault = false) {
// Verify that the setting is actually a setting
if (!SETTINGS[settingName]) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
}
const setting = SETTINGS[settingName];
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
if (!levelOrder.includes("default")) levelOrder.push("default"); // always include default
const minIndex = levelOrder.indexOf(level);
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);
if (explicit) {
const handler = handlers[level];
if (!handler) return SettingsStore._tryControllerOverride(settingName, level, roomId, null);
const value = handler.getValue(settingName, roomId);
return SettingsStore._tryControllerOverride(settingName, level, roomId, value);
}
for (let i = minIndex; i < levelOrder.length; i++) {
const handler = handlers[levelOrder[i]];
if (!handler) continue;
if (excludeDefault && levelOrder[i] === "default") continue;
const value = handler.getValue(settingName, roomId);
if (value === null || value === undefined) continue;
return SettingsStore._tryControllerOverride(settingName, level, roomId, value);
}
return SettingsStore._tryControllerOverride(settingName, level, roomId, null);
}
static _tryControllerOverride(settingName, level, roomId, calculatedValue) {
const controller = SETTINGS[settingName].controller;
if (!controller) return calculatedValue;
const actualValue = controller.getValueOverride(level, roomId, calculatedValue);
if (actualValue !== undefined && actualValue !== null) return actualValue;
return calculatedValue;
}
/**
* Sets the value for a setting. The room ID is optional if the setting is not being
* set for a particular room, otherwise it should be supplied. The value may be null
* to indicate that the level should no longer have an override.
* @param {string} settingName The name of the setting to change.
* @param {String} roomId The room ID to change the value in, may be null.
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level
* to change the value at.
* @param {*} value The new value of the setting, may be null.
* @return {Promise} Resolves when the setting has been changed.
*/
static setValue(settingName, roomId, level, value) {
// Verify that the setting is actually a setting
if (!SETTINGS[settingName]) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
}
const handler = SettingsStore._getHandler(settingName, level);
if (!handler) {
throw new Error("Setting " + settingName + " does not have a handler for " + level);
}
if (!handler.canSetValue(settingName, roomId)) {
throw new Error("User cannot set " + settingName + " at " + level + " in " + roomId);
}
return handler.setValue(settingName, roomId, value).then(() => {
const controller = SETTINGS[settingName].controller;
if (!controller) return;
controller.onChange(level, roomId, value);
});
}
/**
* Determines if the current user is permitted to set the given setting at the given
* level for a particular room. The room ID is optional if the setting is not being
* set for a particular room, otherwise it should be supplied.
* @param {string} settingName The name of the setting to check.
* @param {String} roomId The room ID to check in, may be null.
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level to
* check at.
* @return {boolean} True if the user may set the setting, false otherwise.
*/
static canSetValue(settingName, roomId, level) {
// Verify that the setting is actually a setting
if (!SETTINGS[settingName]) {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
}
const handler = SettingsStore._getHandler(settingName, level);
if (!handler) return false;
return handler.canSetValue(settingName, roomId);
}
/**
* Determines if the given level is supported on this device.
* @param {"device"|"room-device"|"room-account"|"account"|"room"} level The level
* to check the feasibility of.
* @return {boolean} True if the level is supported, false otherwise.
*/
static isLevelSupported(level) {
if (!LEVEL_HANDLERS[level]) return false;
return LEVEL_HANDLERS[level].isSupported();
}
static _getHandler(settingName, level) {
const handlers = SettingsStore._getHandlers(settingName);
if (!handlers[level]) return null;
return handlers[level];
}
static _getHandlers(settingName) {
if (!SETTINGS[settingName]) return {};
const handlers = {};
for (const level of SETTINGS[settingName].supportedLevels) {
if (!LEVEL_HANDLERS[level]) throw new Error("Unexpected level " + level);
handlers[level] = LEVEL_HANDLERS[level];
}
// Always support 'default'
if (!handlers['default']) handlers['default'] = LEVEL_HANDLERS['default'];
return handlers;
}
static _getFeatureState(settingName) {
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;
}
}

View file

@ -0,0 +1,49 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingController from "./SettingController";
export class NotificationsEnabledController extends SettingController {
getValueOverride(level, roomId, calculatedValue) {
const Notifier = require('../../Notifier'); // avoids cyclical references
return calculatedValue && Notifier.isPossible();
}
onChange(level, roomId, newValue) {
const Notifier = require('../../Notifier'); // avoids cyclical references
if (Notifier.supportsDesktopNotifications()) {
Notifier.setEnabled(newValue);
}
}
}
export class NotificationBodyEnabledController extends SettingController {
getValueOverride(level, roomId, calculatedValue) {
const Notifier = require('../../Notifier'); // avoids cyclical references
return calculatedValue && Notifier.isEnabled();
}
}
export class AudioNotificationsEnabledController extends SettingController {
getValueOverride(level, roomId, calculatedValue) {
const Notifier = require('../../Notifier'); // avoids cyclical references
return calculatedValue && Notifier.isEnabled();
}
}

View file

@ -0,0 +1,49 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
/**
* Represents a controller for individual settings to alter the reading behaviour
* based upon environmental conditions, or to react to changes and therefore update
* the working environment.
*
* This is not intended to replace the functionality of a SettingsHandler, it is only
* intended to handle environmental factors for specific settings.
*/
export default class SettingController {
/**
* Gets the overridden value for the setting, if any. This must return null if the
* value is not to be overridden, otherwise it must return the new value.
* @param {string} level The level at which the value was requested at.
* @param {String} roomId The room ID, may be null.
* @param {*} calculatedValue The value that the handlers think the setting should be,
* may be null.
* @return {*} The value that should be used, or null if no override is applicable.
*/
getValueOverride(level, roomId, calculatedValue) {
return null; // no override
}
/**
* Called when the setting value has been changed.
* @param {string} level The level at which the setting has been modified.
* @param {String} roomId The room ID, may be null.
* @param {*} newValue The new value for the setting, may be null.
*/
onChange(level, roomId, newValue) {
// do nothing by default
}
}

View file

@ -0,0 +1,73 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg';
/**
* Gets and sets settings at the "account" level for the current user.
* This handler does not make use of the roomId parameter.
*/
export default class AccountSettingHandler extends SettingsHandler {
getValue(settingName, roomId) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings("org.matrix.preview_urls");
return !content['disable'];
}
let preferredValue = this._getSettings()[settingName];
if (preferredValue === null || preferredValue === undefined) {
// Honour the old setting on read only
if (settingName === "hideAvatarChanges" || settingName === "hideDisplaynameChanges") {
preferredValue = this._getSettings()["hideAvatarDisplaynameChanges"];
}
}
return preferredValue;
}
setValue(settingName, roomId, newValue) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings("org.matrix.preview_urls");
content['disable'] = !newValue;
return MatrixClientPeg.get().setAccountData("org.matrix.preview_urls", content);
}
const content = this._getSettings();
content[settingName] = newValue;
return MatrixClientPeg.get().setAccountData("im.vector.web.settings", content);
}
canSetValue(settingName, roomId) {
return true; // It's their account, so they should be able to
}
isSupported() {
const cli = MatrixClientPeg.get();
return cli !== undefined && cli !== null;
}
_getSettings(eventType = "im.vector.web.settings") {
const cli = MatrixClientPeg.get();
if (!cli) return {};
const event = cli.getAccountData(eventType);
if (!event || !event.getContent()) return {};
return event.getContent();
}
}

View file

@ -0,0 +1,49 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsHandler from "./SettingsHandler";
import SdkConfig from "../../SdkConfig";
/**
* Gets and sets settings at the "config" level. This handler does not make use of the
* roomId parameter.
*/
export default class ConfigSettingsHandler extends SettingsHandler {
getValue(settingName, roomId) {
const config = SdkConfig.get() || {};
// Special case themes
if (settingName === "theme") {
return config["default_theme"];
}
const settingsConfig = config["settingDefaults"];
if (!settingsConfig || !settingsConfig[settingName]) return null;
return settingsConfig[settingName];
}
setValue(settingName, roomId, newValue) {
throw new Error("Cannot change settings at the config level");
}
canSetValue(settingName, roomId) {
return false;
}
isSupported() {
return true; // SdkConfig is always there
}
}

View file

@ -0,0 +1,48 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsHandler from "./SettingsHandler";
/**
* Gets settings at the "default" level. This handler does not support setting values.
* This handler does not make use of the roomId parameter.
*/
export default class DefaultSettingsHandler extends SettingsHandler {
/**
* Creates a new default settings handler with the given defaults
* @param {object} defaults The default setting values, keyed by setting name.
*/
constructor(defaults) {
super();
this._defaults = defaults;
}
getValue(settingName, roomId) {
return this._defaults[settingName];
}
setValue(settingName, roomId, newValue) {
throw new Error("Cannot set values on the default level handler");
}
canSetValue(settingName, roomId) {
return false;
}
isSupported() {
return true;
}
}

View file

@ -0,0 +1,108 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from "../../MatrixClientPeg";
/**
* Gets and sets settings at the "device" level for the current device.
* This handler does not make use of the roomId parameter. This handler
* will special-case features to support legacy settings.
*/
export default class DeviceSettingsHandler extends SettingsHandler {
/**
* Creates a new device settings handler
* @param {string[]} featureNames The names of known features.
*/
constructor(featureNames) {
super();
this._featureNames = featureNames;
}
getValue(settingName, roomId) {
if (this._featureNames.includes(settingName)) {
return this._readFeature(settingName);
}
// Special case notifications
if (settingName === "notificationsEnabled") {
return localStorage.getItem("notifications_enabled") === "true";
} else if (settingName === "notificationBodyEnabled") {
return localStorage.getItem("notifications_body_enabled") === "true";
} else if (settingName === "audioNotificationsEnabled") {
return localStorage.getItem("audio_notifications_enabled") === "true";
}
return this._getSettings()[settingName];
}
setValue(settingName, roomId, newValue) {
if (this._featureNames.includes(settingName)) {
this._writeFeature(settingName, newValue);
return Promise.resolve();
}
// Special case notifications
if (settingName === "notificationsEnabled") {
localStorage.setItem("notifications_enabled", newValue);
return Promise.resolve();
} else if (settingName === "notificationBodyEnabled") {
localStorage.setItem("notifications_body_enabled", newValue);
return Promise.resolve();
} else if (settingName === "audioNotificationsEnabled") {
localStorage.setItem("audio_notifications_enabled", newValue);
return Promise.resolve();
}
const settings = this._getSettings();
settings[settingName] = newValue;
localStorage.setItem("mx_local_settings", JSON.stringify(settings));
return Promise.resolve();
}
canSetValue(settingName, roomId) {
return true; // It's their device, so they should be able to
}
isSupported() {
return localStorage !== undefined && localStorage !== null;
}
_getSettings() {
const value = localStorage.getItem("mx_local_settings");
if (!value) return {};
return JSON.parse(value);
}
// Note: features intentionally don't use the same key as settings to avoid conflicts
// and to be backwards compatible.
_readFeature(featureName) {
if (MatrixClientPeg.get() && MatrixClientPeg.get().isGuest()) {
// Guests should not have any labs features enabled.
return {enabled: false};
}
const value = localStorage.getItem("mx_labs_feature_" + featureName);
return value === "true";
}
_writeFeature(featureName, enabled) {
localStorage.setItem("mx_labs_feature_" + featureName, enabled);
}
}

View file

@ -0,0 +1,69 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from "bluebird";
import SettingsHandler from "./SettingsHandler";
/**
* A wrapper for a SettingsHandler that performs local echo on
* changes to settings. This wrapper will use the underlying
* handler as much as possible to ensure values are not stale.
*/
export default class LocalEchoWrapper extends SettingsHandler {
/**
* Creates a new local echo wrapper
* @param {SettingsHandler} handler The handler to wrap
*/
constructor(handler) {
super();
this._handler = handler;
this._cache = {
// settingName: { roomId: value }
};
}
getValue(settingName, roomId) {
const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
const bySetting = this._cache[settingName];
if (bySetting && bySetting.hasOwnProperty(cacheRoomId)) {
return bySetting[roomId];
}
return this._handler.getValue(settingName, roomId);
}
setValue(settingName, roomId, newValue) {
if (!this._cache[settingName]) this._cache[settingName] = {};
const bySetting = this._cache[settingName];
const cacheRoomId = roomId ? roomId : "UNDEFINED"; // avoid weird keys
bySetting[cacheRoomId] = newValue;
const handlerPromise = this._handler.setValue(settingName, roomId, newValue);
return Promise.resolve(handlerPromise).finally(() => {
delete bySetting[cacheRoomId];
});
}
canSetValue(settingName, roomId) {
return this._handler.canSetValue(settingName, roomId);
}
isSupported() {
return this._handler.isSupported();
}
}

View file

@ -0,0 +1,80 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg';
/**
* Gets and sets settings at the "room-account" level for the current user.
*/
export default class RoomAccountSettingsHandler extends SettingsHandler {
getValue(settingName, roomId) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
return !content['disable'];
}
// Special case room color
if (settingName === "roomColor") {
// The event content should already be in an appropriate format, we just need
// to get the right value.
return this._getSettings(roomId, "org.matrix.room.color_scheme");
}
return this._getSettings(roomId)[settingName];
}
setValue(settingName, roomId, newValue) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
content['disable'] = !newValue;
return MatrixClientPeg.get().setRoomAccountData(roomId, "org.matrix.room.preview_urls", content);
}
// Special case room color
if (settingName === "roomColor") {
// The new value should match our requirements, we just need to store it in the right place.
return MatrixClientPeg.get().setRoomAccountData(roomId, "org.matrix.room.color_scheme", newValue);
}
const content = this._getSettings(roomId);
content[settingName] = newValue;
return MatrixClientPeg.get().setRoomAccountData(roomId, "im.vector.web.settings", content);
}
canSetValue(settingName, roomId) {
const room = MatrixClientPeg.get().getRoom(roomId);
// If they have the room, they can set their own account data
return room !== undefined && room !== null;
}
isSupported() {
const cli = MatrixClientPeg.get();
return cli !== undefined && cli !== null;
}
_getSettings(roomId, eventType = "im.vector.settings") {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return {};
const event = room.getAccountData(eventType);
if (!event || !event.getContent()) return {};
return event.getContent();
}
}

View file

@ -0,0 +1,77 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
/**
* Gets and sets settings at the "room-device" level for the current device in a particular
* room.
*/
export default class RoomDeviceSettingsHandler extends SettingsHandler {
getValue(settingName, roomId) {
// Special case blacklist setting to use legacy values
if (settingName === "blacklistUnverifiedDevices") {
const value = this._read("mx_local_settings");
if (value && value['blacklistUnverifiedDevicesPerRoom']) {
return value['blacklistUnverifiedDevicesPerRoom'][roomId];
}
}
const value = this._read(this._getKey(settingName, roomId));
if (value) return value.value;
return null;
}
setValue(settingName, roomId, newValue) {
// Special case blacklist setting for legacy structure
if (settingName === "blacklistUnverifiedDevices") {
let value = this._read("mx_local_settings");
if (!value) value = {};
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue;
localStorage.setItem("mx_local_settings", JSON.stringify(value));
return Promise.resolve();
}
if (newValue === null) {
localStorage.removeItem(this._getKey(settingName, roomId));
} else {
newValue = JSON.stringify({value: newValue});
localStorage.setItem(this._getKey(settingName, roomId), newValue);
}
return Promise.resolve();
}
canSetValue(settingName, roomId) {
return true; // It's their device, so they should be able to
}
isSupported() {
return localStorage !== undefined && localStorage !== null;
}
_read(key) {
const rawValue = localStorage.getItem(key);
if (!rawValue) return null;
return JSON.parse(rawValue);
}
_getKey(settingName, roomId) {
return "mx_setting_" + settingName + "_" + roomId;
}
}

View file

@ -0,0 +1,70 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg';
/**
* Gets and sets settings at the "room" level.
*/
export default class RoomSettingsHandler extends SettingsHandler {
getValue(settingName, roomId) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
return !content['disable'];
}
return this._getSettings(roomId)[settingName];
}
setValue(settingName, roomId, newValue) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
const content = this._getSettings(roomId, "org.matrix.room.preview_urls");
content['disable'] = !newValue;
return MatrixClientPeg.get().sendStateEvent(roomId, "org.matrix.room.preview_urls", content);
}
const content = this._getSettings(roomId);
content[settingName] = newValue;
return MatrixClientPeg.get().sendStateEvent(roomId, "im.vector.web.settings", content, "");
}
canSetValue(settingName, roomId) {
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId);
let eventType = "im.vector.web.settings";
if (settingName === "urlPreviewsEnabled") eventType = "org.matrix.room.preview_urls";
if (!room) return false;
return room.currentState.maySendStateEvent(eventType, cli.getUserId());
}
isSupported() {
const cli = MatrixClientPeg.get();
return cli !== undefined && cli !== null;
}
_getSettings(roomId, eventType = "im.vector.web.settings") {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return {};
const event = room.currentState.getStateEvents(eventType, "");
if (!event || !event.getContent()) return {};
return event.getContent();
}
}

View file

@ -0,0 +1,71 @@
/*
Copyright 2017 Travis Ralston
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import Promise from "bluebird";
/**
* Represents the base class for all level handlers. This class performs no logic
* and should be overridden.
*/
export default class SettingsHandler {
/**
* Gets the value for a particular setting at this level for a particular room.
* If no room is applicable, the roomId may be null. The roomId may not be
* applicable to this level and may be ignored by the handler.
* @param {string} settingName The name of the setting.
* @param {String} roomId The room ID to read from, may be null.
* @returns {*} The setting value, or null if not found.
*/
getValue(settingName, roomId) {
console.error("Invalid operation: getValue was not overridden");
return null;
}
/**
* Sets the value for a particular setting at this level for a particular room.
* If no room is applicable, the roomId may be null. The roomId may not be
* applicable to this level and may be ignored by the handler. Setting a value
* to null will cause the level to remove the value. The current user should be
* able to set the value prior to calling this.
* @param {string} settingName The name of the setting to change.
* @param {String} roomId The room ID to set the value in, may be null.
* @param {*} newValue The new value for the setting, may be null.
* @returns {Promise} Resolves when the setting has been saved.
*/
setValue(settingName, roomId, newValue) {
console.error("Invalid operation: setValue was not overridden");
return Promise.reject();
}
/**
* Determines if the current user is able to set the value of the given setting
* in the given room at this level.
* @param {string} settingName The name of the setting to check.
* @param {String} roomId The room ID to check in, may be null
* @returns {boolean} True if the setting can be set by the user, false otherwise.
*/
canSetValue(settingName, roomId) {
return false;
}
/**
* Determines if this level is supported on this device.
* @returns {boolean} True if this level is supported on the current device.
*/
isSupported() {
return false;
}
}

View file

@ -14,6 +14,8 @@
limitations under the License. limitations under the License.
*/ */
import SettingsStore from "./settings/SettingsStore";
function memberEventDiff(ev) { function memberEventDiff(ev) {
const diff = { const diff = {
isMemberEvent: ev.getType() === 'm.room.member', isMemberEvent: ev.getType() === 'm.room.member',
@ -34,16 +36,19 @@ function memberEventDiff(ev) {
return diff; return diff;
} }
export default function shouldHideEvent(ev, syncedSettings) { export default function shouldHideEvent(ev) {
// Wrap getValue() for readability
const isEnabled = (name) => SettingsStore.getValue(name, ev.getRoomId());
// Hide redacted events // Hide redacted events
if (syncedSettings['hideRedactions'] && ev.isRedacted()) return true; if (isEnabled('hideRedactions') && ev.isRedacted()) return true;
const eventDiff = memberEventDiff(ev); const eventDiff = memberEventDiff(ev);
if (eventDiff.isMemberEvent) { if (eventDiff.isMemberEvent) {
if (syncedSettings['hideJoinLeaves'] && (eventDiff.isJoin || eventDiff.isPart)) return true; if (isEnabled('hideJoinLeaves') && (eventDiff.isJoin || eventDiff.isPart)) return true;
const isMemberAvatarDisplaynameChange = eventDiff.isAvatarChange || eventDiff.isDisplaynameChange; if (isEnabled('hideAvatarChanges') && eventDiff.isAvatarChange) return true;
if (syncedSettings['hideAvatarDisplaynameChanges'] && isMemberAvatarDisplaynameChange) return true; if (isEnabled('hideDisplaynameChanges') && eventDiff.isDisplaynameChange) return true;
} }
return false; return false;

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import SettingsStore from "../../../src/settings/SettingsStore";
const React = require('react'); const React = require('react');
const ReactDOM = require("react-dom"); const ReactDOM = require("react-dom");
const TestUtils = require('react-addons-test-utils'); const TestUtils = require('react-addons-test-utils');
@ -23,7 +25,6 @@ import sinon from 'sinon';
const sdk = require('matrix-react-sdk'); const sdk = require('matrix-react-sdk');
const MessagePanel = sdk.getComponent('structures.MessagePanel'); const MessagePanel = sdk.getComponent('structures.MessagePanel');
import UserSettingsStore from '../../../src/UserSettingsStore';
import MatrixClientPeg from '../../../src/MatrixClientPeg'; import MatrixClientPeg from '../../../src/MatrixClientPeg';
const test_utils = require('test-utils'); const test_utils = require('test-utils');
@ -59,7 +60,9 @@ describe('MessagePanel', function() {
sandbox = test_utils.stubClient(); sandbox = test_utils.stubClient();
client = MatrixClientPeg.get(); client = MatrixClientPeg.get();
client.credentials = {userId: '@me:here'}; client.credentials = {userId: '@me:here'};
UserSettingsStore.getSyncedSettings = sinon.stub().returns({});
// HACK: We assume all settings want to be disabled
SettingsStore.getValue = sinon.stub().returns(false);
}); });
afterEach(function() { afterEach(function() {

View file

@ -6,7 +6,6 @@ import sinon from 'sinon';
import Promise from 'bluebird'; import Promise from 'bluebird';
import * as testUtils from '../../../test-utils'; import * as testUtils from '../../../test-utils';
import sdk from 'matrix-react-sdk'; import sdk from 'matrix-react-sdk';
import UserSettingsStore from '../../../../src/UserSettingsStore';
const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput'); const MessageComposerInput = sdk.getComponent('views.rooms.MessageComposerInput');
import MatrixClientPeg from '../../../../src/MatrixClientPeg'; import MatrixClientPeg from '../../../../src/MatrixClientPeg';
import RoomMember from 'matrix-js-sdk'; import RoomMember from 'matrix-js-sdk';