diff --git a/docs/settings.md b/docs/settings.md
index cdba01e04a..9762e7a73e 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -131,6 +131,32 @@ SettingsStore.getValue(...); // this will return the value set in `setValue` abo
```
+## Watching for changes
+
+Most use cases do not need to set up a watcher because they are able to react to changes as they are made, or the changes which are made are not significant enough for it to matter. Watchers are intended to be used in scenarios where it is important to react to changes made by other logged in devices. Typically, this would be done within the component itself, however the component should not be aware of the intricacies of setting inversion or remapping to particular data structures. Instead, a generic watcher interface is provided on `SettingsStore` to watch (and subsequently unwatch) for changes in a setting.
+
+An example of a watcher in action would be:
+
+```javascript
+class MyComponent extends React.Component {
+
+ settingWatcherRef = null;
+
+ componentWillMount() {
+ this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", (settingName, roomId, level, newVal) => {
+ // Always re-read the setting value from the store to avoid reacting to changes which do not have a consequence. For example, the
+ // room color could have been changed at the device level, but an account override prevents that change from making a difference.
+ const actualVal = SettingsStore.getValue(settingName, "!example:matrix.org");
+ if (actualVal !== this.state.color) this.setState({color: actualVal});
+ });
+ }
+
+ componentWillUnmount() {
+ SettingsStore.unwatchSetting(this.settingWatcherRef);
+ }
+}
+```
+
# Maintainers Reference
@@ -159,3 +185,10 @@ Features automatically get considered as `disabled` if they are not listed in th
```
If `enableLabs` is true in the configuration, the default for features becomes `"labs"`.
+
+### Watchers
+
+Watchers can appear complicated under the hood: the request to watch a setting is actually forked off to individual handlers for watching. This means that the handlers need to track their changes and listen for remote changes where possible, but also makes it much easier for the `SettingsStore` to react to changes. The handler is going to know the best things to listen for (specific events, account data, etc) and thus it is left as a responsibility for the handler to track changes.
+
+In practice, handlers which rely on remote changes (account data, room events, etc) will always attach a listener to the `MatrixClient`. They then watch for changes to events they care about and send off appropriate updates to the generalized `WatchManager` - a class specifically designed to deduplicate the logic of managing watchers. The handlers which are localized to the local client (device) generally just trigger the `WatchManager` when they manipulate the setting themselves as there's nothing to really 'watch'.
+
\ No newline at end of file
diff --git a/package.json b/package.json
index fba17bb9c9..84d5632023 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279",
"gfm.css": "^1.1.1",
"glob": "^5.0.14",
- "highlight.js": "^9.13.0",
+ "highlight.js": "9.14.2",
"is-ip": "^2.0.0",
"isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.6",
diff --git a/res/css/_common.scss b/res/css/_common.scss
index fd93c8c967..4e327ab28d 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -249,12 +249,6 @@ textarea {
box-shadow: none;
}
-/* View Source Dialog overide */
-.mx_Dialog_wrapper.mx_Dialog_viewsource .mx_Dialog {
- padding-left: 10px;
- padding-right: 10px;
-}
-
.mx_Dialog {
background-color: $primary-bg-color;
color: $light-fg-color;
diff --git a/res/css/_components.scss b/res/css/_components.scss
index 6aed78a627..f3b07255ae 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -150,16 +150,16 @@
@import "./views/settings/_Notifications.scss";
@import "./views/settings/_PhoneNumbers.scss";
@import "./views/settings/_ProfileSettings.scss";
-@import "./views/settings/tabs/_GeneralRoomSettingsTab.scss";
-@import "./views/settings/tabs/_GeneralUserSettingsTab.scss";
-@import "./views/settings/tabs/_HelpSettingsTab.scss";
-@import "./views/settings/tabs/_NotificationSettingsTab.scss";
-@import "./views/settings/tabs/_PreferencesSettingsTab.scss";
-@import "./views/settings/tabs/_RolesRoomSettingsTab.scss";
-@import "./views/settings/tabs/_SecurityRoomSettingsTab.scss";
-@import "./views/settings/tabs/_SecuritySettingsTab.scss";
@import "./views/settings/tabs/_SettingsTab.scss";
-@import "./views/settings/tabs/_VoiceSettingsTab.scss";
+@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
+@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
+@import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss";
+@import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss";
+@import "./views/settings/tabs/user/_HelpUserSettingsTab.scss";
+@import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss";
+@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";
+@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
+@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
@import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallView.scss";
@import "./views/voip/_IncomingCallbox.scss";
diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss
index a4c7dcf58a..b908861c6f 100644
--- a/res/css/structures/_ViewSource.scss
+++ b/res/css/structures/_ViewSource.scss
@@ -14,6 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+.mx_ViewSource_label_left {
+ float: left;
+}
+
+.mx_ViewSource_label_right {
+ float: right;
+}
+
+.mx_ViewSource_label_bottom {
+ clear: both;
+ border-bottom: 1px solid #e5e5e5;
+}
+
.mx_ViewSource pre {
text-align: left;
font-size: 12px;
diff --git a/res/css/views/rooms/_WhoIsTypingTile.scss b/res/css/views/rooms/_WhoIsTypingTile.scss
index eb51595858..ef20c24c84 100644
--- a/res/css/views/rooms/_WhoIsTypingTile.scss
+++ b/res/css/views/rooms/_WhoIsTypingTile.scss
@@ -40,6 +40,7 @@ limitations under the License.
}
.mx_WhoIsTypingTile_remainingAvatarPlaceholder {
+ position: relative;
display: inline-block;
color: #acacac;
background-color: #ddd;
diff --git a/res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss
similarity index 100%
rename from res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss
rename to res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss
diff --git a/res/css/views/settings/tabs/_RolesRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_RolesRoomSettingsTab.scss
similarity index 100%
rename from res/css/views/settings/tabs/_RolesRoomSettingsTab.scss
rename to res/css/views/settings/tabs/room/_RolesRoomSettingsTab.scss
diff --git a/res/css/views/settings/tabs/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss
similarity index 100%
rename from res/css/views/settings/tabs/_SecurityRoomSettingsTab.scss
rename to res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss
diff --git a/res/css/views/settings/tabs/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
similarity index 100%
rename from res/css/views/settings/tabs/_GeneralUserSettingsTab.scss
rename to res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
diff --git a/res/css/views/settings/tabs/_HelpSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss
similarity index 87%
rename from res/css/views/settings/tabs/_HelpSettingsTab.scss
rename to res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss
index 249f06ca95..fa0d0edeb7 100644
--- a/res/css/views/settings/tabs/_HelpSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss
@@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_HelpSettingsTab_debugButton {
+.mx_HelpUserSettingsTab_debugButton {
margin-bottom: 5px;
margin-top: 5px;
}
-.mx_HelpSettingsTab span.mx_AccessibleButton {
+.mx_HelpUserSettingsTab span.mx_AccessibleButton {
word-break: break-word;
}
\ No newline at end of file
diff --git a/res/css/views/settings/tabs/_NotificationSettingsTab.scss b/res/css/views/settings/tabs/user/_NotificationUserSettingsTab.scss
similarity index 91%
rename from res/css/views/settings/tabs/_NotificationSettingsTab.scss
rename to res/css/views/settings/tabs/user/_NotificationUserSettingsTab.scss
index 8fdb688496..3cebd2958e 100644
--- a/res/css/views/settings/tabs/_NotificationSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_NotificationUserSettingsTab.scss
@@ -14,6 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_NotificationSettingsTab .mx_SettingsTab_heading {
+.mx_NotificationUserSettingsTab .mx_SettingsTab_heading {
margin-bottom: 10px; // Give some spacing between the title and the first elements
}
\ No newline at end of file
diff --git a/res/css/views/settings/tabs/_PreferencesSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss
similarity index 89%
rename from res/css/views/settings/tabs/_PreferencesSettingsTab.scss
rename to res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss
index b59b69e63b..f447221b7a 100644
--- a/res/css/views/settings/tabs/_PreferencesSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss
@@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_PreferencesSettingsTab .mx_Field {
+.mx_PreferencesUserSettingsTab .mx_Field {
margin-right: 100px; // Align with the rest of the controls
}
-.mx_PreferencesSettingsTab .mx_Field input {
+.mx_PreferencesUserSettingsTab .mx_Field input {
display: block;
// Subtract 10px padding on left and right
diff --git a/res/css/views/settings/tabs/_SecuritySettingsTab.scss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss
similarity index 67%
rename from res/css/views/settings/tabs/_SecuritySettingsTab.scss
rename to res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss
index ba357f16c3..4835640904 100644
--- a/res/css/views/settings/tabs/_SecuritySettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss
@@ -14,40 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_SecuritySettingsTab .mx_DevicesPanel {
+.mx_SecurityUserSettingsTab .mx_DevicesPanel {
// Normally the panel is 880px, however this can easily overflow the container.
// TODO: Fix the table to not be squishy
width: auto;
max-width: 880px;
}
-.mx_SecuritySettingsTab_deviceInfo {
+.mx_SecurityUserSettingsTab_deviceInfo {
display: table;
padding-left: 0;
}
-.mx_SecuritySettingsTab_deviceInfo > li {
+.mx_SecurityUserSettingsTab_deviceInfo > li {
display: table-row;
}
-.mx_SecuritySettingsTab_deviceInfo > li > label,
-.mx_SecuritySettingsTab_deviceInfo > li > span {
+.mx_SecurityUserSettingsTab_deviceInfo > li > label,
+.mx_SecurityUserSettingsTab_deviceInfo > li > span {
display: table-cell;
padding-right: 1em;
}
-.mx_SecuritySettingsTab_importExportButtons .mx_AccessibleButton {
+.mx_SecurityUserSettingsTab_importExportButtons .mx_AccessibleButton {
margin-right: 10px;
}
-.mx_SecuritySettingsTab_importExportButtons {
+.mx_SecurityUserSettingsTab_importExportButtons {
margin-bottom: 15px;
}
-.mx_SecuritySettingsTab_ignoredUser {
+.mx_SecurityUserSettingsTab_ignoredUser {
margin-bottom: 5px;
}
-.mx_SecuritySettingsTab_ignoredUser .mx_AccessibleButton {
+.mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton {
margin-right: 10px;
}
\ No newline at end of file
diff --git a/res/css/views/settings/tabs/_VoiceSettingsTab.scss b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss
similarity index 84%
rename from res/css/views/settings/tabs/_VoiceSettingsTab.scss
rename to res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss
index 5ddd57b0e2..f5dba9831e 100644
--- a/res/css/views/settings/tabs/_VoiceSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss
@@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_VoiceSettingsTab .mx_Field select {
+.mx_VoiceUserSettingsTab .mx_Field select {
width: 100%;
max-width: 100%;
}
-.mx_VoiceSettingsTab .mx_Field {
+.mx_VoiceUserSettingsTab .mx_Field {
margin-right: 100px; // align with the rest of the fields
}
-.mx_VoiceSettingsTab_missingMediaPermissions {
+.mx_VoiceUserSettingsTab_missingMediaPermissions {
margin-bottom: 15px;
}
diff --git a/src/BasePlatform.js b/src/BasePlatform.js
index 79f0d69e2c..54310d1849 100644
--- a/src/BasePlatform.js
+++ b/src/BasePlatform.js
@@ -113,4 +113,29 @@ export default class BasePlatform {
reload() {
throw new Error("reload not implemented!");
}
+
+ supportsAutoLaunch(): boolean {
+ return false;
+ }
+
+ // XXX: Surely this should be a setting like any other?
+ async getAutoLaunchEnabled(): boolean {
+ return false;
+ }
+
+ async setAutoLaunchEnabled(enabled: boolean): void {
+ throw new Error("Unimplemented");
+ }
+
+ supportsMinimizeToTray(): boolean {
+ return false;
+ }
+
+ async getMinimizeToTrayEnabled(): boolean {
+ return false;
+ }
+
+ async setMinimizeToTrayEnabled(enabled: boolean): void {
+ throw new Error("Unimplemented");
+ }
}
diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js
index e36034c69d..1cf29c3e82 100644
--- a/src/MatrixClientPeg.js
+++ b/src/MatrixClientPeg.js
@@ -30,6 +30,7 @@ import MatrixActionCreators from './actions/MatrixActionCreators';
import {phasedRollOutExpiredForUser} from "./PhasedRollOut";
import Modal from './Modal';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
+import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
interface MatrixClientCreds {
homeserverUrl: string,
@@ -137,8 +138,9 @@ class MatrixClientPeg {
opts.pendingEventOrdering = "detached";
opts.lazyLoadMembers = true;
- // Connect the matrix client to the dispatcher
+ // Connect the matrix client to the dispatcher and setting handlers
MatrixActionCreators.start(this.matrixClient);
+ MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient;
console.log(`MatrixClientPeg: really starting MatrixClient`);
await this.get().startClient(opts);
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 89fce9c718..b80f49d051 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -34,6 +34,7 @@ import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
+import {Group} from "matrix-js-sdk";
const LONG_DESC_PLACEHOLDER = _td(
`
HTML for your community's page
@@ -569,7 +570,7 @@ export default React.createClass({
_onShareClick: function() {
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share community dialog', '', ShareDialog, {
- target: this._matrixClient.getGroup(this.props.groupId),
+ target: this._matrixClient.getGroup(this.props.groupId) || new Group(this.props.groupId),
});
},
diff --git a/src/components/structures/LeftPanel.js b/src/components/structures/LeftPanel.js
index 000195d349..7088347ff4 100644
--- a/src/components/structures/LeftPanel.js
+++ b/src/components/structures/LeftPanel.js
@@ -26,6 +26,7 @@ import dis from '../../dispatcher';
import VectorConferenceHandler from '../../VectorConferenceHandler';
import TagPanelButtons from './TagPanelButtons';
import SettingsStore from '../../settings/SettingsStore';
+import {_t} from "../../languageHandler";
const LeftPanel = React.createClass({
@@ -212,6 +213,7 @@ const LeftPanel = React.createClass({
);
const searchBox = ();
diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js
index 8e32802d0a..85f3afdfa5 100644
--- a/src/components/structures/RoomView.js
+++ b/src/components/structures/RoomView.js
@@ -537,12 +537,12 @@ module.exports = React.createClass({
case 'picture_snapshot':
this.uploadFile(payload.file);
break;
- case 'notifier_enabled':
case 'upload_failed':
// 413: File was too big or upset the server in some way.
- if(payload.error.http_status === 413) {
+ if (payload.error && payload.error.http_status === 413) {
this._fetchMediaConfig(true);
}
+ case 'notifier_enabled':
case 'upload_started':
case 'upload_finished':
this.forceUpdate();
@@ -1305,7 +1305,10 @@ module.exports = React.createClass({
},
onSearchClick: function() {
- this.setState({ searching: true, showingPinned: false });
+ this.setState({
+ searching: !this.state.searching,
+ showingPinned: false,
+ });
},
onCancelSearchClick: function() {
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js
index 10628ccd13..3f9d954f9f 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,12 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
-
import React from 'react';
-import { _t } from '../../languageHandler';
+import PropTypes from 'prop-types';
import { KeyCode } from '../../Keyboard';
-import sdk from '../../index';
import dis from '../../dispatcher';
import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
@@ -28,8 +26,10 @@ module.exports = React.createClass({
displayName: 'SearchBox',
propTypes: {
- onSearch: React.PropTypes.func,
- onCleared: React.PropTypes.func,
+ onSearch: PropTypes.func,
+ onCleared: PropTypes.func,
+ className: PropTypes.string,
+ placeholder: PropTypes.string.isRequired,
},
getInitialState: function() {
@@ -102,21 +102,22 @@ module.exports = React.createClass({
const clearButton = this.state.searchTerm.length > 0 ?
( {this._clearSearch("button")} }>
- ) : undefined;
+ onClick={ () => {this._clearSearch("button"); } }>
+ ) : undefined;
+ const className = this.props.className || "";
return (
{ clearButton }
diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js
index 4844149f59..fd35fdbeef 100644
--- a/src/components/structures/ViewSource.js
+++ b/src/components/structures/ViewSource.js
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,11 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-'use strict';
-
import React from 'react';
import PropTypes from 'prop-types';
import SyntaxHighlight from '../views/elements/SyntaxHighlight';
+import {_t} from "../../languageHandler";
+import sdk from "../../index";
module.exports = React.createClass({
@@ -27,31 +28,24 @@ module.exports = React.createClass({
propTypes: {
content: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired,
- },
-
- componentDidMount: function() {
- document.addEventListener("keydown", this.onKeyDown);
- },
-
- componentWillUnmount: function() {
- document.removeEventListener("keydown", this.onKeyDown);
- },
-
- onKeyDown: function(ev) {
- if (ev.keyCode == 27) { // escape
- ev.stopPropagation();
- ev.preventDefault();
- this.props.onFinished();
- }
+ roomId: PropTypes.string.isRequired,
+ eventId: PropTypes.string.isRequired,
},
render: function() {
+ const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
-
);
- if (this.props.mxEvent.getType() !== this.props.mxEvent.getWireType()) {
+ if (mxEvent.getType() !== mxEvent.getWireType()) {
viewClearSourceButton = (
{ _t('View Decrypted Source') }
@@ -303,8 +308,11 @@ module.exports = React.createClass({
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
const permalinkButton = (
);
@@ -318,12 +326,12 @@ module.exports = React.createClass({
// Bridges can provide a 'external_url' to link back to the source.
if (
- typeof(this.props.mxEvent.event.content.external_url) === "string" &&
- isUrlPermitted(this.props.mxEvent.event.content.external_url)
+ typeof(mxEvent.event.content.external_url) === "string" &&
+ isUrlPermitted(mxEvent.event.content.external_url)
) {
externalURLButton = (
);
},
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.js
index be6fbee4f6..8f3768864b 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.js
@@ -362,34 +362,6 @@ export default class MessageComposer extends React.Component {
const canSendMessages = !this.state.tombstone &&
this.props.room.maySendMessage();
- // TODO: Remove temporary logging for riot-web#7838
- // Note: we rip apart the power level event ourselves because we don't want to
- // log too much data about it - just the bits we care about. Many of the variables
- // logged here are to help figure out where in the stack the 'cannot post in room'
- // warning is coming from. This means logging various numbers from the PL event to
- // verify RoomState._maySendEventOfType is doing the right thing.
- const room = this.props.room;
- const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
- let plEventString = "";
- if (plEvent) {
- const content = plEvent.getContent();
- if (!content) {
- plEventString = "";
- } else {
- const stringifyFalsey = (v) => v === null ? '' : (v === undefined ? '' : v);
- const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : "");
- const usersPl = stringifyFalsey(content.users_default);
- const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : "");
- const eventPl = stringifyFalsey(content.events_default);
- plEventString = `actualUserPl=${actualUserPl} defaultUserPl=${usersPl} actualEventPl=${actualEventPl} defaultEventPl=${eventPl}`;
- }
- }
- console.log(
- `[riot-web#7838] renderComposer() hasTombstone=${!!this.state.tombstone} maySendMessage=${room.maySendMessage()}` +
- ` myMembership=${room.getMyMembership()} maySendEvent=${room.currentState.maySendEvent('m.room.message', room.myUserId)}` +
- ` myUserId=${room.myUserId} roomId=${room.roomId} hasPlEvent=${!!plEvent} powerLevels='${plEventString}'`
- );
-
if (canSendMessages) {
// This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly
@@ -469,8 +441,6 @@ export default class MessageComposer extends React.Component {
);
} else {
- // TODO: Remove temporary logging for riot-web#7838
- console.log("[riot-web#7838] Falling back to showing cannot post in room error");
controls.push(
{ _t('You do not have permission to post to this room') }
diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js
index dba40f033a..9dd690f6e5 100644
--- a/src/components/views/rooms/WhoIsTypingTile.js
+++ b/src/components/views/rooms/WhoIsTypingTile.js
@@ -170,6 +170,7 @@ module.exports = React.createClass({
width={24}
height={24}
resizeMethod="crop"
+ viewUserOnClick={true}
/>
);
});
diff --git a/src/components/views/settings/tabs/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
similarity index 93%
rename from src/components/views/settings/tabs/AdvancedRoomSettingsTab.js
rename to src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
index 9b99622516..3c6a7addc3 100644
--- a/src/components/views/settings/tabs/AdvancedRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js
@@ -16,11 +16,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import {_t} from "../../../../languageHandler";
-import MatrixClientPeg from "../../../../MatrixClientPeg";
-import sdk from "../../../../index";
-import AccessibleButton from "../../elements/AccessibleButton";
-import Modal from "../../../../Modal";
+import {_t} from "../../../../../languageHandler";
+import MatrixClientPeg from "../../../../../MatrixClientPeg";
+import sdk from "../../../../..";
+import AccessibleButton from "../../../elements/AccessibleButton";
+import Modal from "../../../../../Modal";
export default class AdvancedRoomSettingsTab extends React.Component {
static propTypes = {
diff --git a/src/components/views/settings/tabs/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
similarity index 92%
rename from src/components/views/settings/tabs/GeneralRoomSettingsTab.js
rename to src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
index f43fc8a682..5d707fcf16 100644
--- a/src/components/views/settings/tabs/GeneralRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js
@@ -16,14 +16,14 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import {_t} from "../../../../languageHandler";
-import RoomProfileSettings from "../../room_settings/RoomProfileSettings";
-import MatrixClientPeg from "../../../../MatrixClientPeg";
-import sdk from "../../../../index";
-import AccessibleButton from "../../elements/AccessibleButton";
+import {_t} from "../../../../../languageHandler";
+import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
+import MatrixClientPeg from "../../../../../MatrixClientPeg";
+import sdk from "../../../../..";
+import AccessibleButton from "../../../elements/AccessibleButton";
import {MatrixClient} from "matrix-js-sdk";
-import dis from "../../../../dispatcher";
-import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
+import dis from "../../../../../dispatcher";
+import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
export default class GeneralRoomSettingsTab extends React.Component {
static childContextTypes = {
diff --git a/src/components/views/settings/tabs/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
similarity index 98%
rename from src/components/views/settings/tabs/RolesRoomSettingsTab.js
rename to src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
index d223e8f2e9..a6dac5a147 100644
--- a/src/components/views/settings/tabs/RolesRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
@@ -16,11 +16,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import {_t, _td} from "../../../../languageHandler";
-import MatrixClientPeg from "../../../../MatrixClientPeg";
-import sdk from "../../../../index";
-import AccessibleButton from "../../elements/AccessibleButton";
-import Modal from "../../../../Modal";
+import {_t, _td} from "../../../../../languageHandler";
+import MatrixClientPeg from "../../../../../MatrixClientPeg";
+import sdk from "../../../../..";
+import AccessibleButton from "../../../elements/AccessibleButton";
+import Modal from "../../../../../Modal";
const plEventsToLabels = {
// These will be translated for us later.
diff --git a/src/components/views/settings/tabs/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
similarity index 96%
rename from src/components/views/settings/tabs/SecurityRoomSettingsTab.js
rename to src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
index 698f67dd18..a6eca3bf19 100644
--- a/src/components/views/settings/tabs/SecurityRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
@@ -16,11 +16,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import {_t} from "../../../../languageHandler";
-import MatrixClientPeg from "../../../../MatrixClientPeg";
-import sdk from "../../../../index";
-import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
-import {SettingLevel} from "../../../../settings/SettingsStore";
+import {_t} from "../../../../../languageHandler";
+import MatrixClientPeg from "../../../../../MatrixClientPeg";
+import sdk from "../../../../..";
+import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
+import {SettingLevel} from "../../../../../settings/SettingsStore";
export default class SecurityRoomSettingsTab extends React.Component {
static propTypes = {
@@ -188,7 +188,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
if (joinRule !== 'public' && guestAccess === 'forbidden') {
guestWarning = (
-
+
{_t("Guests cannot join this room even if explicitly invited.")}
{_t("Click here to fix")}
@@ -201,7 +201,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
if (joinRule === 'public' && !hasAliases) {
aliasWarning = (
-
+
{_t("To link to this room, please add an alias.")}
diff --git a/src/components/views/settings/tabs/FlairSettingsTab.js b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
similarity index 74%
rename from src/components/views/settings/tabs/FlairSettingsTab.js
rename to src/components/views/settings/tabs/user/FlairUserSettingsTab.js
index db513a161a..0063a9a981 100644
--- a/src/components/views/settings/tabs/FlairSettingsTab.js
+++ b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js
@@ -15,14 +15,13 @@ limitations under the License.
*/
import React from 'react';
-import {_t} from "../../../../languageHandler";
-import {DragDropContext} from "react-beautiful-dnd";
-import GroupUserSettings from "../../groups/GroupUserSettings";
-import MatrixClientPeg from "../../../../MatrixClientPeg";
+import {_t} from "../../../../../languageHandler";
+import GroupUserSettings from "../../../groups/GroupUserSettings";
+import MatrixClientPeg from "../../../../../MatrixClientPeg";
import PropTypes from "prop-types";
import {MatrixClient} from "matrix-js-sdk";
-export default class FlairSettingsTab extends React.Component {
+export default class FlairUserSettingsTab extends React.Component {
static childContextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient),
};
@@ -42,9 +41,7 @@ export default class FlairSettingsTab extends React.Component {
{_t("Flair")}
-
-
-
+
);
diff --git a/src/components/views/settings/tabs/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
similarity index 88%
rename from src/components/views/settings/tabs/GeneralUserSettingsTab.js
rename to src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
index fd3274c9e0..093160e330 100644
--- a/src/components/views/settings/tabs/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js
@@ -15,21 +15,21 @@ limitations under the License.
*/
import React from 'react';
-import {_t} from "../../../../languageHandler";
-import ProfileSettings from "../ProfileSettings";
-import EmailAddresses from "../EmailAddresses";
-import PhoneNumbers from "../PhoneNumbers";
-import Field from "../../elements/Field";
-import * as languageHandler from "../../../../languageHandler";
-import {SettingLevel} from "../../../../settings/SettingsStore";
-import SettingsStore from "../../../../settings/SettingsStore";
-import LanguageDropdown from "../../elements/LanguageDropdown";
-import AccessibleButton from "../../elements/AccessibleButton";
-import DeactivateAccountDialog from "../../dialogs/DeactivateAccountDialog";
-const PlatformPeg = require("../../../../PlatformPeg");
-const sdk = require('../../../../index');
-const Modal = require("../../../../Modal");
-const dis = require("../../../../dispatcher");
+import {_t} from "../../../../../languageHandler";
+import ProfileSettings from "../../ProfileSettings";
+import EmailAddresses from "../../EmailAddresses";
+import PhoneNumbers from "../../PhoneNumbers";
+import Field from "../../../elements/Field";
+import * as languageHandler from "../../../../../languageHandler";
+import {SettingLevel} from "../../../../../settings/SettingsStore";
+import SettingsStore from "../../../../../settings/SettingsStore";
+import LanguageDropdown from "../../../elements/LanguageDropdown";
+import AccessibleButton from "../../../elements/AccessibleButton";
+import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
+const PlatformPeg = require("../../../../../PlatformPeg");
+const sdk = require('../../../../..');
+const Modal = require("../../../../../Modal");
+const dis = require("../../../../../dispatcher");
export default class GeneralUserSettingsTab extends React.Component {
constructor() {
diff --git a/src/components/views/settings/tabs/HelpSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
similarity index 90%
rename from src/components/views/settings/tabs/HelpSettingsTab.js
rename to src/components/views/settings/tabs/user/HelpUserSettingsTab.js
index 4ad62451cb..d001a3f2e6 100644
--- a/src/components/views/settings/tabs/HelpSettingsTab.js
+++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js
@@ -16,15 +16,15 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-import {_t, getCurrentLanguage} from "../../../../languageHandler";
-import MatrixClientPeg from "../../../../MatrixClientPeg";
-import AccessibleButton from "../../elements/AccessibleButton";
-import SdkConfig from "../../../../SdkConfig";
-import createRoom from "../../../../createRoom";
-const packageJson = require('../../../../../package.json');
-const Modal = require("../../../../Modal");
-const sdk = require("../../../../index");
-const PlatformPeg = require("../../../../PlatformPeg");
+import {_t, getCurrentLanguage} from "../../../../../languageHandler";
+import MatrixClientPeg from "../../../../../MatrixClientPeg";
+import AccessibleButton from "../../../elements/AccessibleButton";
+import SdkConfig from "../../../../../SdkConfig";
+import createRoom from "../../../../../createRoom";
+const packageJson = require('../../../../../../package.json');
+const Modal = require("../../../../../Modal");
+const sdk = require("../../../../..");
+const PlatformPeg = require("../../../../../PlatformPeg");
// if this looks like a release, use the 'version' from package.json; else use
// the git sha. Prepend version with v, to look like riot-web version
@@ -45,7 +45,7 @@ const ghVersionLabel = function(repo, token='') {
return { token };
};
-export default class HelpSettingsTab extends React.Component {
+export default class HelpUserSettingsTab extends React.Component {
static propTypes = {
closeSettingsFn: PropTypes.func.isRequired,
};
@@ -117,7 +117,7 @@ export default class HelpSettingsTab extends React.Component {
}
return (
-
{_t("Devices")}
diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
similarity index 90%
rename from src/components/views/settings/tabs/VoiceSettingsTab.js
rename to src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
index aefb114dd3..31791708e0 100644
--- a/src/components/views/settings/tabs/VoiceSettingsTab.js
+++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js
@@ -15,16 +15,16 @@ limitations under the License.
*/
import React from 'react';
-import {_t} from "../../../../languageHandler";
-import CallMediaHandler from "../../../../CallMediaHandler";
-import Field from "../../elements/Field";
-import AccessibleButton from "../../elements/AccessibleButton";
-import {SettingLevel} from "../../../../settings/SettingsStore";
-const Modal = require("../../../../Modal");
-const sdk = require("../../../../index");
-const MatrixClientPeg = require("../../../../MatrixClientPeg");
+import {_t} from "../../../../../languageHandler";
+import CallMediaHandler from "../../../../../CallMediaHandler";
+import Field from "../../../elements/Field";
+import AccessibleButton from "../../../elements/AccessibleButton";
+import {SettingLevel} from "../../../../../settings/SettingsStore";
+const Modal = require("../../../../../Modal");
+const sdk = require("../../../../..");
+const MatrixClientPeg = require("../../../../../MatrixClientPeg");
-export default class VoiceSettingsTab extends React.Component {
+export default class VoiceUserSettingsTab extends React.Component {
constructor() {
super();
@@ -103,7 +103,7 @@ export default class VoiceSettingsTab extends React.Component {
let webcamDropdown = null;
if (this.state.mediaDevices === false) {
requestButton = (
-
+
{_t("Missing media permissions, click the button below to request.")}
{_t("Request media permissions")}
@@ -166,7 +166,7 @@ export default class VoiceSettingsTab extends React.Component {
}
return (
-
+
{_t("Voice & Video")}
{requestButton}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 84c9dacd07..979bbd0553 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -306,6 +306,7 @@
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
"Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs",
"Show developer tools": "Show developer tools",
+ "Order rooms in the room list by most important first instead of most recent": "Order rooms in the room list by most important first instead of most recent",
"Collecting app version information": "Collecting app version information",
"Collecting logs": "Collecting logs",
"Uploading report": "Uploading report",
@@ -500,19 +501,7 @@
"Upload profile picture": "Upload profile picture",
"Display Name": "Display Name",
"Save": "Save",
- "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
- "Upgrade room to version %(ver)s": "Upgrade room to version %(ver)s",
- "Room information": "Room information",
- "Internal room ID:": "Internal room ID:",
- "Room version": "Room version",
- "Room version:": "Room version:",
- "Developer options": "Developer options",
- "Open Devtools": "Open Devtools",
"Flair": "Flair",
- "General": "General",
- "Room Addresses": "Room Addresses",
- "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?",
- "URL Previews": "URL Previews",
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success",
"Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them",
@@ -528,6 +517,7 @@
"Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Deactivate Account": "Deactivate Account",
+ "General": "General",
"Legal": "Legal",
"Credits": "Credits",
"For help with using Riot, click here.": "For help with using Riot, click here.",
@@ -551,10 +541,50 @@
"Labs": "Labs",
"Notifications": "Notifications",
"Start automatically after system login": "Start automatically after system login",
+ "Close button should minimize window to tray": "Close button should minimize window to tray",
"Preferences": "Preferences",
"Composer": "Composer",
"Timeline": "Timeline",
+ "Room list": "Room list",
"Autocomplete delay (ms)": "Autocomplete delay (ms)",
+ "Unignore": "Unignore",
+ "": "",
+ "Import E2E room keys": "Import E2E room keys",
+ "Cryptography": "Cryptography",
+ "Device ID:": "Device ID:",
+ "Device key:": "Device key:",
+ "Ignored users": "Ignored users",
+ "Bulk options": "Bulk options",
+ "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
+ "Key backup": "Key backup",
+ "Security & Privacy": "Security & Privacy",
+ "Devices": "Devices",
+ "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
+ "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
+ "Learn more about how we use analytics.": "Learn more about how we use analytics.",
+ "No media permissions": "No media permissions",
+ "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam",
+ "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
+ "Request media permissions": "Request media permissions",
+ "No Audio Outputs detected": "No Audio Outputs detected",
+ "No Microphones detected": "No Microphones detected",
+ "No Webcams detected": "No Webcams detected",
+ "Default Device": "Default Device",
+ "Audio Output": "Audio Output",
+ "Microphone": "Microphone",
+ "Camera": "Camera",
+ "Voice & Video": "Voice & Video",
+ "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers",
+ "Upgrade room to version %(ver)s": "Upgrade room to version %(ver)s",
+ "Room information": "Room information",
+ "Internal room ID:": "Internal room ID:",
+ "Room version": "Room version",
+ "Room version:": "Room version:",
+ "Developer options": "Developer options",
+ "Open Devtools": "Open Devtools",
+ "Room Addresses": "Room Addresses",
+ "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?",
+ "URL Previews": "URL Previews",
"To change the room's avatar, you must be a": "To change the room's avatar, you must be a",
"To change the room's name, you must be a": "To change the room's name, you must be a",
"To change the room's main address, you must be a": "To change the room's main address, you must be a",
@@ -592,38 +622,11 @@
"Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)",
"Members only (since they were invited)": "Members only (since they were invited)",
"Members only (since they joined)": "Members only (since they joined)",
- "Security & Privacy": "Security & Privacy",
"Encryption": "Encryption",
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
"Encrypted": "Encrypted",
"Who can access this room?": "Who can access this room?",
"Who can read history?": "Who can read history?",
- "Unignore": "Unignore",
- "": "",
- "Import E2E room keys": "Import E2E room keys",
- "Cryptography": "Cryptography",
- "Device ID:": "Device ID:",
- "Device key:": "Device key:",
- "Ignored users": "Ignored users",
- "Bulk options": "Bulk options",
- "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites",
- "Key backup": "Key backup",
- "Devices": "Devices",
- "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.",
- "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.",
- "Learn more about how we use analytics.": "Learn more about how we use analytics.",
- "No media permissions": "No media permissions",
- "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam",
- "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.",
- "Request media permissions": "Request media permissions",
- "No Audio Outputs detected": "No Audio Outputs detected",
- "No Microphones detected": "No Microphones detected",
- "No Webcams detected": "No Webcams detected",
- "Default Device": "Default Device",
- "Audio Output": "Audio Output",
- "Microphone": "Microphone",
- "Camera": "Camera",
- "Voice & Video": "Voice & Video",
"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.",
"Add a widget": "Add a widget",
@@ -1202,6 +1205,7 @@
"View Decrypted Source": "View Decrypted Source",
"Unhide Preview": "Unhide Preview",
"Share Message": "Share Message",
+ "Share Permalink": "Share Permalink",
"Quote": "Quote",
"Source URL": "Source URL",
"Collapse Reply Thread": "Collapse Reply Thread",
diff --git a/src/settings/Settings.js b/src/settings/Settings.js
index cf68fed8ba..e4db12f5ba 100644
--- a/src/settings/Settings.js
+++ b/src/settings/Settings.js
@@ -1,6 +1,6 @@
/*
Copyright 2017 Travis Ralston
-Copyright 2018 New Vector Ltd
+Copyright 2018, 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -340,4 +340,9 @@ export const SETTINGS = {
displayName: _td('Show developer tools'),
default: false,
},
+ "RoomList.orderByImportance": {
+ supportedLevels: LEVELS_ACCOUNT_SETTINGS,
+ displayName: _td('Order rooms in the room list by most important first instead of most recent'),
+ default: true,
+ },
};
diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js
index 1bdd72dc5f..1704ad9db2 100644
--- a/src/settings/SettingsStore.js
+++ b/src/settings/SettingsStore.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,6 +24,7 @@ import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
import {_t} from '../languageHandler';
import SdkConfig from "../SdkConfig";
+import dis from '../dispatcher';
import {SETTINGS} from "./Settings";
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
@@ -98,6 +100,121 @@ const LEVEL_ORDER = [
* be enabled).
*/
export default class SettingsStore {
+ // We support watching settings for changes, and do so only at the levels which are
+ // relevant to the setting. We pass the watcher on to the handlers and aggregate it
+ // before sending it off to the caller. We need to track which callback functions we
+ // provide to the handlers though so we can unwatch it on demand. In practice, we
+ // return a "callback reference" to the caller which correlates to an entry in this
+ // dictionary for each handler's callback function.
+ //
+ // We also maintain a list of monitors which are special watchers: they cause dispatches
+ // when the setting changes. We track which rooms we're monitoring though to ensure we
+ // don't duplicate updates on the bus.
+ static _watchers = {}; // { callbackRef => { level => callbackFn } }
+ static _monitors = {}; // { settingName => { roomId => callbackRef } }
+
+ /**
+ * Watches for changes in a particular setting. This is done without any local echo
+ * wrapping and fires whenever a change is detected in a setting's value. Watching
+ * is intended to be used in scenarios where the app needs to react to changes made
+ * by other devices. It is otherwise expected that callers will be able to use the
+ * Controller system or track their own changes to settings. Callers should retain
+ * @param {string} settingName The setting name to watch
+ * @param {String} roomId The room ID to watch for changes in. May be null for 'all'.
+ * @param {function} callbackFn A function to be called when a setting change is
+ * detected. Four arguments can be expected: the setting name, the room ID (may be null),
+ * the level the change happened at, and finally the new value for those arguments. The
+ * callback may need to do a call to #getValue() to see if a consequential change has
+ * occurred.
+ * @returns {string} A reference to the watcher that was employed.
+ */
+ static watchSetting(settingName, roomId, callbackFn) {
+ const setting = SETTINGS[settingName];
+ const originalSettingName = settingName;
+ if (!setting) throw new Error(`${settingName} is not a setting`);
+
+ if (setting.invertedSettingName) {
+ settingName = setting.invertedSettingName;
+ }
+
+ const watcherId = `${new Date().getTime()}_${settingName}_${roomId}`;
+ SettingsStore._watchers[watcherId] = {};
+
+ const levels = Object.keys(LEVEL_HANDLERS);
+ for (const level of levels) {
+ const handler = SettingsStore._getHandler(originalSettingName, level);
+ if (!handler) continue;
+
+ const localizedCallback = (changedInRoomId, newVal) => {
+ callbackFn(originalSettingName, changedInRoomId, level, newVal);
+ };
+
+ console.log(`Starting watcher for ${settingName}@${roomId || ''} at level ${level}`);
+ SettingsStore._watchers[watcherId][level] = localizedCallback;
+ handler.watchSetting(settingName, roomId, localizedCallback);
+ }
+
+ return watcherId;
+ }
+
+ /**
+ * Stops the SettingsStore from watching a setting. This is a no-op if the watcher
+ * provided is not found.
+ * @param {string} watcherReference The watcher reference (received from #watchSetting)
+ * to cancel.
+ */
+ static unwatchSetting(watcherReference) {
+ if (!SettingsStore._watchers[watcherReference]) return;
+
+ for (const handlerName of Object.keys(SettingsStore._watchers[watcherReference])) {
+ const handler = LEVEL_HANDLERS[handlerName];
+ if (!handler) continue;
+ handler.unwatchSetting(SettingsStore._watchers[watcherReference][handlerName]);
+ }
+
+ delete SettingsStore._watchers[watcherReference];
+ }
+
+ /**
+ * Sets up a monitor for a setting. This behaves similar to #watchSetting except instead
+ * of making a call to a callback, it forwards all changes to the dispatcher. Callers can
+ * expect to listen for the 'setting_updated' action with an object containing settingName,
+ * roomId, level, and newValue.
+ * @param {string} settingName The setting name to monitor.
+ * @param {String} roomId The room ID to monitor for changes in. Use null for all rooms.
+ */
+ static monitorSetting(settingName, roomId) {
+ if (!this._monitors[settingName]) this._monitors[settingName] = {};
+
+ const registerWatcher = () => {
+ this._monitors[settingName][roomId] = SettingsStore.watchSetting(
+ settingName, roomId, (settingName, inRoomId, level, newValue) => {
+ dis.dispatch({
+ action: 'setting_updated',
+ settingName,
+ roomId: inRoomId,
+ level,
+ newValue,
+ });
+ },
+ );
+ };
+
+ const hasRoom = Object.keys(this._monitors[settingName]).find((r) => r === roomId || r === null);
+ if (!hasRoom) {
+ registerWatcher();
+ } else {
+ if (roomId === null) {
+ // Unregister all existing watchers and register the new one
+ for (const roomId of Object.keys(this._monitors[settingName])) {
+ SettingsStore.unwatchSetting(this._monitors[settingName][roomId]);
+ }
+ this._monitors[settingName] = {};
+ registerWatcher();
+ } // else a watcher is already registered for the room, so don't bother registering it again
+ }
+ }
+
/**
* Gets the translated display name for a given setting
* @param {string} settingName The setting to look up.
diff --git a/src/settings/WatchManager.js b/src/settings/WatchManager.js
new file mode 100644
index 0000000000..0561529392
--- /dev/null
+++ b/src/settings/WatchManager.js
@@ -0,0 +1,57 @@
+/*
+Copyright 2019 New Vector 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.
+*/
+
+/**
+ * Generalized management class for dealing with watchers on a per-handler (per-level)
+ * basis without duplicating code. Handlers are expected to push updates through this
+ * class, which are then proxied outwards to any applicable watchers.
+ */
+export class WatchManager {
+ _watchers = {}; // { settingName: { roomId: callbackFns[] } }
+
+ // Proxy for handlers to delegate changes to this manager
+ watchSetting(settingName, roomId, cb) {
+ if (!this._watchers[settingName]) this._watchers[settingName] = {};
+ if (!this._watchers[settingName][roomId]) this._watchers[settingName][roomId] = [];
+ this._watchers[settingName][roomId].push(cb);
+ }
+
+ // Proxy for handlers to delegate changes to this manager
+ unwatchSetting(cb) {
+ for (const settingName of Object.keys(this._watchers)) {
+ for (const roomId of Object.keys(this._watchers[settingName])) {
+ let idx;
+ while ((idx = this._watchers[settingName][roomId].indexOf(cb)) !== -1) {
+ this._watchers[settingName][roomId].splice(idx, 1);
+ }
+ }
+ }
+ }
+
+ notifyUpdate(settingName, inRoomId, newValue) {
+ if (!this._watchers[settingName]) return;
+
+ const roomWatchers = this._watchers[settingName];
+ const callbacks = [];
+
+ if (inRoomId !== null && roomWatchers[inRoomId]) callbacks.push(...roomWatchers[inRoomId]);
+ if (roomWatchers[null]) callbacks.push(...roomWatchers[null]);
+
+ for (const callback of callbacks) {
+ callback(inRoomId, newValue);
+ }
+ }
+}
diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js
index b822709573..4bef585e6b 100644
--- a/src/settings/handlers/AccountSettingsHandler.js
+++ b/src/settings/handlers/AccountSettingsHandler.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,14 +15,49 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg';
+import {WatchManager} from "../WatchManager";
+import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
/**
* 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 {
+export default class AccountSettingsHandler extends MatrixClientBackedSettingsHandler {
+ constructor() {
+ super();
+
+ this._watchers = new WatchManager();
+ this._onAccountData = this._onAccountData.bind(this);
+ }
+
+ initMatrixClient(oldClient, newClient) {
+ if (oldClient) {
+ oldClient.removeListener("accountData", this._onAccountData);
+ }
+
+ newClient.on("accountData", this._onAccountData);
+ }
+
+ _onAccountData(event) {
+ if (event.getType() === "org.matrix.preview_urls") {
+ let val = event.getContent()['disable'];
+ if (typeof(val) !== "boolean") {
+ val = null;
+ } else {
+ val = !val;
+ }
+
+ this._watchers.notifyUpdate("urlPreviewsEnabled", null, val);
+ } else if (event.getType() === "im.vector.web.settings") {
+ // We can't really discern what changed, so trigger updates for everything
+ for (const settingName of Object.keys(event.getContent())) {
+ console.log(settingName);
+ this._watchers.notifyUpdate(settingName, null, event.getContent()[settingName]);
+ }
+ }
+ }
+
getValue(settingName, roomId) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
@@ -67,6 +103,14 @@ export default class AccountSettingHandler extends SettingsHandler {
return cli !== undefined && cli !== null;
}
+ watchSetting(settingName, roomId, cb) {
+ this._watchers.watchSetting(settingName, roomId, cb);
+ }
+
+ unwatchSetting(cb) {
+ this._watchers.unwatchSetting(cb);
+ }
+
_getSettings(eventType = "im.vector.web.settings") {
const cli = MatrixClientPeg.get();
if (!cli) return null;
diff --git a/src/settings/handlers/ConfigSettingsHandler.js b/src/settings/handlers/ConfigSettingsHandler.js
index a54ad1cef6..095347a542 100644
--- a/src/settings/handlers/ConfigSettingsHandler.js
+++ b/src/settings/handlers/ConfigSettingsHandler.js
@@ -47,4 +47,12 @@ export default class ConfigSettingsHandler extends SettingsHandler {
isSupported() {
return true; // SdkConfig is always there
}
+
+ watchSetting(settingName, roomId, cb) {
+ // no-op: no changes possible
+ }
+
+ unwatchSetting(cb) {
+ // no-op: no changes possible
+ }
}
diff --git a/src/settings/handlers/DefaultSettingsHandler.js b/src/settings/handlers/DefaultSettingsHandler.js
index 11e8b729bc..83824850b3 100644
--- a/src/settings/handlers/DefaultSettingsHandler.js
+++ b/src/settings/handlers/DefaultSettingsHandler.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -51,4 +52,12 @@ export default class DefaultSettingsHandler extends SettingsHandler {
isSupported() {
return true;
}
+
+ watchSetting(settingName, roomId, cb) {
+ // no-op: no changes possible
+ }
+
+ unwatchSetting(cb) {
+ // no-op: no changes possible
+ }
}
diff --git a/src/settings/handlers/DeviceSettingsHandler.js b/src/settings/handlers/DeviceSettingsHandler.js
index b2a225e190..457fb888e9 100644
--- a/src/settings/handlers/DeviceSettingsHandler.js
+++ b/src/settings/handlers/DeviceSettingsHandler.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,6 +18,7 @@ limitations under the License.
import Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from "../../MatrixClientPeg";
+import {WatchManager} from "../WatchManager";
/**
* Gets and sets settings at the "device" level for the current device.
@@ -31,6 +33,7 @@ export default class DeviceSettingsHandler extends SettingsHandler {
constructor(featureNames) {
super();
this._featureNames = featureNames;
+ this._watchers = new WatchManager();
}
getValue(settingName, roomId) {
@@ -66,18 +69,22 @@ export default class DeviceSettingsHandler extends SettingsHandler {
// Special case notifications
if (settingName === "notificationsEnabled") {
localStorage.setItem("notifications_enabled", newValue);
+ this._watchers.notifyUpdate(settingName, null, newValue);
return Promise.resolve();
} else if (settingName === "notificationBodyEnabled") {
localStorage.setItem("notifications_body_enabled", newValue);
+ this._watchers.notifyUpdate(settingName, null, newValue);
return Promise.resolve();
} else if (settingName === "audioNotificationsEnabled") {
localStorage.setItem("audio_notifications_enabled", newValue);
+ this._watchers.notifyUpdate(settingName, null, newValue);
return Promise.resolve();
}
const settings = this._getSettings() || {};
settings[settingName] = newValue;
localStorage.setItem("mx_local_settings", JSON.stringify(settings));
+ this._watchers.notifyUpdate(settingName, null, newValue);
return Promise.resolve();
}
@@ -90,6 +97,14 @@ export default class DeviceSettingsHandler extends SettingsHandler {
return localStorage !== undefined && localStorage !== null;
}
+ watchSetting(settingName, roomId, cb) {
+ this._watchers.watchSetting(settingName, roomId, cb);
+ }
+
+ unwatchSetting(cb) {
+ this._watchers.unwatchSetting(cb);
+ }
+
_getSettings() {
const value = localStorage.getItem("mx_local_settings");
if (!value) return null;
@@ -111,5 +126,6 @@ export default class DeviceSettingsHandler extends SettingsHandler {
_writeFeature(featureName, enabled) {
localStorage.setItem("mx_labs_feature_" + featureName, enabled);
+ this._watchers.notifyUpdate(featureName, null, enabled);
}
}
diff --git a/src/settings/handlers/LocalEchoWrapper.js b/src/settings/handlers/LocalEchoWrapper.js
index d616edd9fb..3b1200f0b7 100644
--- a/src/settings/handlers/LocalEchoWrapper.js
+++ b/src/settings/handlers/LocalEchoWrapper.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -66,4 +67,12 @@ export default class LocalEchoWrapper extends SettingsHandler {
isSupported() {
return this._handler.isSupported();
}
+
+ watchSetting(settingName, roomId, cb) {
+ this._handler.watchSetting(settingName, roomId, cb);
+ }
+
+ unwatchSetting(cb) {
+ this._handler.unwatchSetting(cb);
+ }
}
diff --git a/src/settings/handlers/MatrixClientBackedSettingsHandler.js b/src/settings/handlers/MatrixClientBackedSettingsHandler.js
new file mode 100644
index 0000000000..effe7ae9a7
--- /dev/null
+++ b/src/settings/handlers/MatrixClientBackedSettingsHandler.js
@@ -0,0 +1,48 @@
+/*
+Copyright 2019 New Vector 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.
+*/
+
+import SettingsHandler from "./SettingsHandler";
+
+// Dev note: This whole class exists in the event someone logs out and back in - we want
+// to make sure the right MatrixClient is listening for changes.
+
+/**
+ * Represents the base class for settings handlers which need access to a MatrixClient.
+ * This class performs no logic and should be overridden.
+ */
+export default class MatrixClientBackedSettingsHandler extends SettingsHandler {
+ static _matrixClient;
+ static _instances = [];
+
+ static set matrixClient(client) {
+ const oldClient = MatrixClientBackedSettingsHandler._matrixClient;
+ MatrixClientBackedSettingsHandler._matrixClient = client;
+
+ for (const instance of MatrixClientBackedSettingsHandler._instances) {
+ instance.initMatrixClient(oldClient, client);
+ }
+ }
+
+ constructor() {
+ super();
+
+ MatrixClientBackedSettingsHandler._instances.push(this);
+ }
+
+ initMatrixClient() {
+ console.warn("initMatrixClient not overridden");
+ }
+}
diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js
index d0dadc2de7..448b42f61e 100644
--- a/src/settings/handlers/RoomAccountSettingsHandler.js
+++ b/src/settings/handlers/RoomAccountSettingsHandler.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,13 +15,51 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg';
+import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
+import {WatchManager} from "../WatchManager";
/**
* Gets and sets settings at the "room-account" level for the current user.
*/
-export default class RoomAccountSettingsHandler extends SettingsHandler {
+export default class RoomAccountSettingsHandler extends MatrixClientBackedSettingsHandler {
+ constructor() {
+ super();
+
+ this._watchers = new WatchManager();
+ this._onAccountData = this._onAccountData.bind(this);
+ }
+
+ initMatrixClient(oldClient, newClient) {
+ if (oldClient) {
+ oldClient.removeListener("Room.accountData", this._onAccountData);
+ }
+
+ newClient.on("Room.accountData", this._onAccountData);
+ }
+
+ _onAccountData(event, room) {
+ const roomId = room.roomId;
+
+ if (event.getType() === "org.matrix.room.preview_urls") {
+ let val = event.getContent()['disable'];
+ if (typeof (val) !== "boolean") {
+ val = null;
+ } else {
+ val = !val;
+ }
+
+ this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, val);
+ } else if (event.getType() === "org.matrix.room.color_scheme") {
+ this._watchers.notifyUpdate("roomColor", roomId, event.getContent());
+ } else if (event.getType() === "im.vector.web.settings") {
+ // We can't really discern what changed, so trigger updates for everything
+ for (const settingName of Object.keys(event.getContent())) {
+ this._watchers.notifyUpdate(settingName, roomId, event.getContent()[settingName]);
+ }
+ }
+ }
+
getValue(settingName, roomId) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
@@ -74,6 +113,14 @@ export default class RoomAccountSettingsHandler extends SettingsHandler {
return cli !== undefined && cli !== null;
}
+ watchSetting(settingName, roomId, cb) {
+ this._watchers.watchSetting(settingName, roomId, cb);
+ }
+
+ unwatchSetting(cb) {
+ this._watchers.unwatchSetting(cb);
+ }
+
_getSettings(roomId, eventType = "im.vector.web.settings") {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return null;
diff --git a/src/settings/handlers/RoomDeviceSettingsHandler.js b/src/settings/handlers/RoomDeviceSettingsHandler.js
index 186be3041f..710c5e6255 100644
--- a/src/settings/handlers/RoomDeviceSettingsHandler.js
+++ b/src/settings/handlers/RoomDeviceSettingsHandler.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -16,12 +17,19 @@ limitations under the License.
import Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler";
+import {WatchManager} from "../WatchManager";
/**
* Gets and sets settings at the "room-device" level for the current device in a particular
* room.
*/
export default class RoomDeviceSettingsHandler extends SettingsHandler {
+ constructor() {
+ super();
+
+ this._watchers = new WatchManager();
+ }
+
getValue(settingName, roomId) {
// Special case blacklist setting to use legacy values
if (settingName === "blacklistUnverifiedDevices") {
@@ -44,6 +52,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue;
localStorage.setItem("mx_local_settings", JSON.stringify(value));
+ this._watchers.notifyUpdate(settingName, roomId, newValue);
return Promise.resolve();
}
@@ -54,6 +63,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
localStorage.setItem(this._getKey(settingName, roomId), newValue);
}
+ this._watchers.notifyUpdate(settingName, roomId, newValue);
return Promise.resolve();
}
@@ -65,6 +75,14 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
return localStorage !== undefined && localStorage !== null;
}
+ watchSetting(settingName, roomId, cb) {
+ this._watchers.watchSetting(settingName, roomId, cb);
+ }
+
+ unwatchSetting(cb) {
+ this._watchers.unwatchSetting(cb);
+ }
+
_read(key) {
const rawValue = localStorage.getItem(key);
if (!rawValue) return null;
diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js
index 71abff94f6..1622b44dd0 100644
--- a/src/settings/handlers/RoomSettingsHandler.js
+++ b/src/settings/handlers/RoomSettingsHandler.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,13 +15,49 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg';
+import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
+import {WatchManager} from "../WatchManager";
/**
* Gets and sets settings at the "room" level.
*/
-export default class RoomSettingsHandler extends SettingsHandler {
+export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandler {
+ constructor() {
+ super();
+
+ this._watchers = new WatchManager();
+ this._onEvent = this._onEvent.bind(this);
+ }
+
+ initMatrixClient(oldClient, newClient) {
+ if (oldClient) {
+ oldClient.removeListener("RoomState.events", this._onEvent);
+ }
+
+ newClient.on("RoomState.events", this._onEvent);
+ }
+
+ _onEvent(event) {
+ const roomId = event.getRoomId();
+
+ if (event.getType() === "org.matrix.room.preview_urls") {
+ let val = event.getContent()['disable'];
+ if (typeof (val) !== "boolean") {
+ val = null;
+ } else {
+ val = !val;
+ }
+
+ this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, val);
+ } else if (event.getType() === "im.vector.web.settings") {
+ // We can't really discern what changed, so trigger updates for everything
+ for (const settingName of Object.keys(event.getContent())) {
+ this._watchers.notifyUpdate(settingName, roomId, event.getContent()[settingName]);
+ }
+ }
+ }
+
getValue(settingName, roomId) {
// Special case URL previews
if (settingName === "urlPreviewsEnabled") {
@@ -64,6 +101,14 @@ export default class RoomSettingsHandler extends SettingsHandler {
return cli !== undefined && cli !== null;
}
+ watchSetting(settingName, roomId, cb) {
+ this._watchers.watchSetting(settingName, roomId, cb);
+ }
+
+ unwatchSetting(cb) {
+ this._watchers.unwatchSetting(cb);
+ }
+
_getSettings(roomId, eventType = "im.vector.web.settings") {
const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return null;
diff --git a/src/settings/handlers/SettingsHandler.js b/src/settings/handlers/SettingsHandler.js
index 69f633c650..0a704d5be7 100644
--- a/src/settings/handlers/SettingsHandler.js
+++ b/src/settings/handlers/SettingsHandler.js
@@ -1,5 +1,6 @@
/*
Copyright 2017 Travis Ralston
+Copyright 2019 New Vector Ltd.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -68,4 +69,27 @@ export default class SettingsHandler {
isSupported() {
return false;
}
+
+ /**
+ * Watches for a setting change within this handler. The caller should preserve
+ * a reference to the callback so that it may be unwatched. The caller should
+ * additionally provide a unique callback for multiple watchers on the same setting.
+ * @param {string} settingName The setting name to watch for changes in.
+ * @param {String} roomId The room ID to watch for changes in.
+ * @param {function} cb A function taking two arguments: the room ID the setting changed
+ * in and the new value for the setting at this level in the given room.
+ */
+ watchSetting(settingName, roomId, cb) {
+ throw new Error("Invalid operation: watchSetting was not overridden");
+ }
+
+ /**
+ * Unwatches a previously watched setting. If the callback is not associated with
+ * a watcher, this is a no-op.
+ * @param {function} cb A callback function previously supplied to watchSetting
+ * which should no longer be used.
+ */
+ unwatchSetting(cb) {
+ throw new Error("Invalid operation: unwatchSetting was not overridden");
+ }
}
diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js
index 0a11c2774a..aec57dedeb 100644
--- a/src/stores/RoomListStore.js
+++ b/src/stores/RoomListStore.js
@@ -59,6 +59,22 @@ class RoomListStore extends Store {
this._recentsComparator = this._recentsComparator.bind(this);
}
+ /**
+ * Alerts the RoomListStore to a potential change in how room list sorting should
+ * behave.
+ * @param {boolean} forceRegeneration True to force a change in the algorithm
+ */
+ updateSortingAlgorithm(forceRegeneration=false) {
+ const byImportance = SettingsStore.getValue("RoomList.orderByImportance");
+ if (byImportance !== this._state.orderRoomsByImportance || forceRegeneration) {
+ console.log("Updating room sorting algorithm: sortByImportance=" + byImportance);
+ this._setState({orderRoomsByImportance: byImportance});
+
+ // Trigger a resort of the entire list to reflect the change in algorithm
+ this._generateInitialRoomLists();
+ }
+ }
+
_init() {
// Initialise state
const defaultLists = {
@@ -77,7 +93,10 @@ class RoomListStore extends Store {
presentationLists: defaultLists, // like `lists`, but with arrays of rooms instead
ready: false,
stickyRoomId: null,
+ orderRoomsByImportance: true,
};
+
+ SettingsStore.monitorSetting('RoomList.orderByImportance', null);
}
_setState(newState) {
@@ -99,6 +118,11 @@ class RoomListStore extends Store {
__onDispatch(payload) {
const logicallyReady = this._matrixClient && this._state.ready;
switch (payload.action) {
+ case 'setting_updated': {
+ if (payload.settingName !== 'RoomList.orderByImportance') break;
+ this.updateSortingAlgorithm();
+ }
+ break;
// Initialise state after initial sync
case 'MatrixActions.sync': {
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
@@ -106,7 +130,7 @@ class RoomListStore extends Store {
}
this._matrixClient = payload.matrixClient;
- this._generateInitialRoomLists();
+ this.updateSortingAlgorithm(/*force=*/true);
}
break;
case 'MatrixActions.Room.receipt': {
@@ -517,6 +541,14 @@ class RoomListStore extends Store {
}
_calculateCategory(room) {
+ if (!this._state.orderRoomsByImportance) {
+ // Effectively disable the categorization of rooms if we're supposed to
+ // be sorting by more recent messages first. This triggers the timestamp
+ // comparison bit of _setRoomCategory and _recentsComparator instead of
+ // the category ordering.
+ return CATEGORY_IDLE;
+ }
+
const mentions = room.getUnreadNotificationCount("highlight") > 0;
if (mentions) return CATEGORY_RED;