Merge branches 'develop' and 't3chguy/pretty_Devtools' of github.com:matrix-org/matrix-react-sdk into t3chguy/pretty_Devtools

This commit is contained in:
Michael Telatynski 2019-02-25 22:39:37 +00:00
commit dd9077f1e4
59 changed files with 911 additions and 346 deletions

View file

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

View file

@ -73,7 +73,7 @@
"gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279", "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#b302279",
"gfm.css": "^1.1.1", "gfm.css": "^1.1.1",
"glob": "^5.0.14", "glob": "^5.0.14",
"highlight.js": "^9.13.0", "highlight.js": "9.14.2",
"is-ip": "^2.0.0", "is-ip": "^2.0.0",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"linkifyjs": "^2.1.6", "linkifyjs": "^2.1.6",

View file

@ -249,12 +249,6 @@ textarea {
box-shadow: none; box-shadow: none;
} }
/* View Source Dialog overide */
.mx_Dialog_wrapper.mx_Dialog_viewsource .mx_Dialog {
padding-left: 10px;
padding-right: 10px;
}
.mx_Dialog { .mx_Dialog {
background-color: $primary-bg-color; background-color: $primary-bg-color;
color: $light-fg-color; color: $light-fg-color;

View file

@ -150,16 +150,16 @@
@import "./views/settings/_Notifications.scss"; @import "./views/settings/_Notifications.scss";
@import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_PhoneNumbers.scss";
@import "./views/settings/_ProfileSettings.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/_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/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallView.scss"; @import "./views/voip/_CallView.scss";
@import "./views/voip/_IncomingCallbox.scss"; @import "./views/voip/_IncomingCallbox.scss";

View file

@ -14,6 +14,19 @@ See the License for the specific language governing permissions and
limitations under the License. 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 { .mx_ViewSource pre {
text-align: left; text-align: left;
font-size: 12px; font-size: 12px;

View file

@ -40,6 +40,7 @@ limitations under the License.
} }
.mx_WhoIsTypingTile_remainingAvatarPlaceholder { .mx_WhoIsTypingTile_remainingAvatarPlaceholder {
position: relative;
display: inline-block; display: inline-block;
color: #acacac; color: #acacac;
background-color: #ddd; background-color: #ddd;

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_HelpSettingsTab_debugButton { .mx_HelpUserSettingsTab_debugButton {
margin-bottom: 5px; margin-bottom: 5px;
margin-top: 5px; margin-top: 5px;
} }
.mx_HelpSettingsTab span.mx_AccessibleButton { .mx_HelpUserSettingsTab span.mx_AccessibleButton {
word-break: break-word; word-break: break-word;
} }

View file

@ -14,6 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. 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 margin-bottom: 10px; // Give some spacing between the title and the first elements
} }

View file

@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_PreferencesSettingsTab .mx_Field { .mx_PreferencesUserSettingsTab .mx_Field {
margin-right: 100px; // Align with the rest of the controls margin-right: 100px; // Align with the rest of the controls
} }
.mx_PreferencesSettingsTab .mx_Field input { .mx_PreferencesUserSettingsTab .mx_Field input {
display: block; display: block;
// Subtract 10px padding on left and right // Subtract 10px padding on left and right

View file

@ -14,40 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_SecuritySettingsTab .mx_DevicesPanel { .mx_SecurityUserSettingsTab .mx_DevicesPanel {
// Normally the panel is 880px, however this can easily overflow the container. // Normally the panel is 880px, however this can easily overflow the container.
// TODO: Fix the table to not be squishy // TODO: Fix the table to not be squishy
width: auto; width: auto;
max-width: 880px; max-width: 880px;
} }
.mx_SecuritySettingsTab_deviceInfo { .mx_SecurityUserSettingsTab_deviceInfo {
display: table; display: table;
padding-left: 0; padding-left: 0;
} }
.mx_SecuritySettingsTab_deviceInfo > li { .mx_SecurityUserSettingsTab_deviceInfo > li {
display: table-row; display: table-row;
} }
.mx_SecuritySettingsTab_deviceInfo > li > label, .mx_SecurityUserSettingsTab_deviceInfo > li > label,
.mx_SecuritySettingsTab_deviceInfo > li > span { .mx_SecurityUserSettingsTab_deviceInfo > li > span {
display: table-cell; display: table-cell;
padding-right: 1em; padding-right: 1em;
} }
.mx_SecuritySettingsTab_importExportButtons .mx_AccessibleButton { .mx_SecurityUserSettingsTab_importExportButtons .mx_AccessibleButton {
margin-right: 10px; margin-right: 10px;
} }
.mx_SecuritySettingsTab_importExportButtons { .mx_SecurityUserSettingsTab_importExportButtons {
margin-bottom: 15px; margin-bottom: 15px;
} }
.mx_SecuritySettingsTab_ignoredUser { .mx_SecurityUserSettingsTab_ignoredUser {
margin-bottom: 5px; margin-bottom: 5px;
} }
.mx_SecuritySettingsTab_ignoredUser .mx_AccessibleButton { .mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton {
margin-right: 10px; margin-right: 10px;
} }

View file

@ -14,15 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_VoiceSettingsTab .mx_Field select { .mx_VoiceUserSettingsTab .mx_Field select {
width: 100%; width: 100%;
max-width: 100%; max-width: 100%;
} }
.mx_VoiceSettingsTab .mx_Field { .mx_VoiceUserSettingsTab .mx_Field {
margin-right: 100px; // align with the rest of the fields margin-right: 100px; // align with the rest of the fields
} }
.mx_VoiceSettingsTab_missingMediaPermissions { .mx_VoiceUserSettingsTab_missingMediaPermissions {
margin-bottom: 15px; margin-bottom: 15px;
} }

View file

@ -113,4 +113,29 @@ export default class BasePlatform {
reload() { reload() {
throw new Error("reload not implemented!"); 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");
}
} }

View file

@ -30,6 +30,7 @@ import MatrixActionCreators from './actions/MatrixActionCreators';
import {phasedRollOutExpiredForUser} from "./PhasedRollOut"; import {phasedRollOutExpiredForUser} from "./PhasedRollOut";
import Modal from './Modal'; import Modal from './Modal';
import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; import {verificationMethods} from 'matrix-js-sdk/lib/crypto';
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
interface MatrixClientCreds { interface MatrixClientCreds {
homeserverUrl: string, homeserverUrl: string,
@ -137,8 +138,9 @@ class MatrixClientPeg {
opts.pendingEventOrdering = "detached"; opts.pendingEventOrdering = "detached";
opts.lazyLoadMembers = true; opts.lazyLoadMembers = true;
// Connect the matrix client to the dispatcher // Connect the matrix client to the dispatcher and setting handlers
MatrixActionCreators.start(this.matrixClient); MatrixActionCreators.start(this.matrixClient);
MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient;
console.log(`MatrixClientPeg: really starting MatrixClient`); console.log(`MatrixClientPeg: really starting MatrixClient`);
await this.get().startClient(opts); await this.get().startClient(opts);

View file

@ -34,6 +34,7 @@ import GroupStore from '../../stores/GroupStore';
import FlairStore from '../../stores/FlairStore'; import FlairStore from '../../stores/FlairStore';
import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker';
import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to"; import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to";
import {Group} from "matrix-js-sdk";
const LONG_DESC_PLACEHOLDER = _td( const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1> `<h1>HTML for your community's page</h1>
@ -569,7 +570,7 @@ export default React.createClass({
_onShareClick: function() { _onShareClick: function() {
const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share community dialog', '', 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),
}); });
}, },

View file

@ -26,6 +26,7 @@ import dis from '../../dispatcher';
import VectorConferenceHandler from '../../VectorConferenceHandler'; import VectorConferenceHandler from '../../VectorConferenceHandler';
import TagPanelButtons from './TagPanelButtons'; import TagPanelButtons from './TagPanelButtons';
import SettingsStore from '../../settings/SettingsStore'; import SettingsStore from '../../settings/SettingsStore';
import {_t} from "../../languageHandler";
const LeftPanel = React.createClass({ const LeftPanel = React.createClass({
@ -212,6 +213,7 @@ const LeftPanel = React.createClass({
); );
const searchBox = (<SearchBox const searchBox = (<SearchBox
placeholder={ _t('Filter room names') }
onSearch={ this.onSearch } onSearch={ this.onSearch }
onCleared={ this.onSearchCleared } onCleared={ this.onSearchCleared }
collapsed={this.props.collapsed} />); collapsed={this.props.collapsed} />);

View file

@ -537,12 +537,12 @@ module.exports = React.createClass({
case 'picture_snapshot': case 'picture_snapshot':
this.uploadFile(payload.file); this.uploadFile(payload.file);
break; break;
case 'notifier_enabled':
case 'upload_failed': case 'upload_failed':
// 413: File was too big or upset the server in some way. // 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); this._fetchMediaConfig(true);
} }
case 'notifier_enabled':
case 'upload_started': case 'upload_started':
case 'upload_finished': case 'upload_finished':
this.forceUpdate(); this.forceUpdate();
@ -1305,7 +1305,10 @@ module.exports = React.createClass({
}, },
onSearchClick: function() { onSearchClick: function() {
this.setState({ searching: true, showingPinned: false }); this.setState({
searching: !this.state.searching,
showingPinned: false,
});
}, },
onCancelSearchClick: function() { onCancelSearchClick: function() {

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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,12 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import { _t } from '../../languageHandler'; import PropTypes from 'prop-types';
import { KeyCode } from '../../Keyboard'; import { KeyCode } from '../../Keyboard';
import sdk from '../../index';
import dis from '../../dispatcher'; import dis from '../../dispatcher';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton'; import AccessibleButton from '../../components/views/elements/AccessibleButton';
@ -28,8 +26,10 @@ module.exports = React.createClass({
displayName: 'SearchBox', displayName: 'SearchBox',
propTypes: { propTypes: {
onSearch: React.PropTypes.func, onSearch: PropTypes.func,
onCleared: React.PropTypes.func, onCleared: PropTypes.func,
className: PropTypes.string,
placeholder: PropTypes.string.isRequired,
}, },
getInitialState: function() { getInitialState: function() {
@ -102,21 +102,22 @@ module.exports = React.createClass({
const clearButton = this.state.searchTerm.length > 0 ? const clearButton = this.state.searchTerm.length > 0 ?
(<AccessibleButton key="button" (<AccessibleButton key="button"
className="mx_SearchBox_closeButton" className="mx_SearchBox_closeButton"
onClick={ () => {this._clearSearch("button")} }> onClick={ () => {this._clearSearch("button"); } }>
</AccessibleButton>) : undefined; </AccessibleButton>) : undefined;
const className = this.props.className || "";
return ( return (
<div className="mx_SearchBox mx_textinput"> <div className="mx_SearchBox mx_textinput">
<input <input
key="searchfield" key="searchfield"
type="text" type="text"
ref="search" ref="search"
className="mx_textinput_icon mx_textinput_search" className={"mx_textinput_icon mx_textinput_search " + className}
value={ this.state.searchTerm } value={ this.state.searchTerm }
onFocus={ this._onFocus } onFocus={ this._onFocus }
onChange={ this.onChange } onChange={ this.onChange }
onKeyDown={ this._onKeyDown } onKeyDown={ this._onKeyDown }
placeholder={ _t('Filter room names') } placeholder={ this.props.placeholder }
/> />
{ clearButton } { clearButton }
</div> </div>

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
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,11 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import SyntaxHighlight from '../views/elements/SyntaxHighlight'; import SyntaxHighlight from '../views/elements/SyntaxHighlight';
import {_t} from "../../languageHandler";
import sdk from "../../index";
module.exports = React.createClass({ module.exports = React.createClass({
@ -27,31 +28,24 @@ module.exports = React.createClass({
propTypes: { propTypes: {
content: PropTypes.object.isRequired, content: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}, roomId: PropTypes.string.isRequired,
eventId: PropTypes.string.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();
}
}, },
render: function() { render: function() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<div className="mx_ViewSource"> <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
<div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
<div className="mx_ViewSource_label_right">Event ID: { this.props.eventId }</div>
<div className="mx_ViewSource_label_bottom" />
<div className="mx_Dialog_content">
<SyntaxHighlight className="json"> <SyntaxHighlight className="json">
{ JSON.stringify(this.props.content, null, 2) } { JSON.stringify(this.props.content, null, 2) }
</SyntaxHighlight> </SyntaxHighlight>
</div> </div>
</BaseDialog>
); );
}, },
}); });

View file

@ -66,7 +66,7 @@ module.exports = React.createClass({
} }
const scriptTag = document.createElement('script'); const scriptTag = document.createElement('script');
scriptTag.setAttribute( scriptTag.setAttribute(
'src', `${protocol}//www.google.com/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`, 'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
); );
this.refs.recaptchaContainer.appendChild(scriptTag); this.refs.recaptchaContainer.appendChild(scriptTag);
} }

View file

@ -98,6 +98,8 @@ module.exports = React.createClass({
onViewSourceClick: function() { onViewSourceClick: function() {
const ViewSource = sdk.getComponent('structures.ViewSource'); const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Event Source', '', ViewSource, { Modal.createTrackedDialog('View Event Source', '', ViewSource, {
roomId: this.props.mxEvent.getRoomId(),
eventId: this.props.mxEvent.getId(),
content: this.props.mxEvent.event, content: this.props.mxEvent.event,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
this.closeMenu(); this.closeMenu();
@ -106,6 +108,8 @@ module.exports = React.createClass({
onViewClearSourceClick: function() { onViewClearSourceClick: function() {
const ViewSource = sdk.getComponent('structures.ViewSource'); const ViewSource = sdk.getComponent('structures.ViewSource');
Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, { Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, {
roomId: this.props.mxEvent.getRoomId(),
eventId: this.props.mxEvent.getId(),
// FIXME: _clearEvent is private // FIXME: _clearEvent is private
content: this.props.mxEvent._clearEvent, content: this.props.mxEvent._clearEvent,
}, 'mx_Dialog_viewsource'); }, 'mx_Dialog_viewsource');
@ -211,7 +215,8 @@ module.exports = React.createClass({
}, },
render: function() { render: function() {
const eventStatus = this.props.mxEvent.status; const mxEvent = this.props.mxEvent;
const eventStatus = mxEvent.status;
let resendButton; let resendButton;
let redactButton; let redactButton;
let cancelButton; let cancelButton;
@ -251,8 +256,8 @@ module.exports = React.createClass({
); );
} }
if (isSent && this.props.mxEvent.getType() === 'm.room.message') { if (isSent && mxEvent.getType() === 'm.room.message') {
const content = this.props.mxEvent.getContent(); const content = mxEvent.getContent();
if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) { if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) {
forwardButton = ( forwardButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}> <div className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
@ -282,7 +287,7 @@ module.exports = React.createClass({
</div> </div>
); );
if (this.props.mxEvent.getType() !== this.props.mxEvent.getWireType()) { if (mxEvent.getType() !== mxEvent.getWireType()) {
viewClearSourceButton = ( viewClearSourceButton = (
<div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}> <div className="mx_MessageContextMenu_field" onClick={this.onViewClearSourceClick}>
{ _t('View Decrypted Source') } { _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) // 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 = ( const permalinkButton = (
<div className="mx_MessageContextMenu_field"> <div className="mx_MessageContextMenu_field">
<a href={makeEventPermalink(this.props.mxEvent.getRoomId(), this.props.mxEvent.getId())} <a href={makeEventPermalink(mxEvent.getRoomId(), mxEvent.getId())}
target="_blank" rel="noopener" onClick={this.onPermalinkClick}>{ _t('Share Message') }</a> target="_blank" rel="noopener" onClick={this.onPermalinkClick}>
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
? _t('Share Permalink') : _t('Share Message') }
</a>
</div> </div>
); );
@ -318,12 +326,12 @@ module.exports = React.createClass({
// Bridges can provide a 'external_url' to link back to the source. // Bridges can provide a 'external_url' to link back to the source.
if ( if (
typeof(this.props.mxEvent.event.content.external_url) === "string" && typeof(mxEvent.event.content.external_url) === "string" &&
isUrlPermitted(this.props.mxEvent.event.content.external_url) isUrlPermitted(mxEvent.event.content.external_url)
) { ) {
externalURLButton = ( externalURLButton = (
<div className="mx_MessageContextMenu_field"> <div className="mx_MessageContextMenu_field">
<a href={this.props.mxEvent.event.content.external_url} <a href={mxEvent.event.content.external_url}
rel="noopener" target="_blank" onClick={this.closeMenu}>{ _t('Source URL') }</a> rel="noopener" target="_blank" onClick={this.closeMenu}>{ _t('Source URL') }</a>
</div> </div>
); );

View file

@ -18,10 +18,10 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Tab, TabbedView} from "../../structures/TabbedView"; import {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler"; import {_t, _td} from "../../../languageHandler";
import AdvancedRoomSettingsTab from "../settings/tabs/AdvancedRoomSettingsTab"; import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
import RolesRoomSettingsTab from "../settings/tabs/RolesRoomSettingsTab"; import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab";
import GeneralRoomSettingsTab from "../settings/tabs/GeneralRoomSettingsTab"; import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab";
import SecurityRoomSettingsTab from "../settings/tabs/SecurityRoomSettingsTab"; import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab";
import sdk from "../../../index"; import sdk from "../../../index";
export default class RoomSettingsDialog extends React.Component { export default class RoomSettingsDialog extends React.Component {

View file

@ -18,15 +18,15 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {Tab, TabbedView} from "../../structures/TabbedView"; import {Tab, TabbedView} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler"; import {_t, _td} from "../../../languageHandler";
import GeneralUserSettingsTab from "../settings/tabs/GeneralUserSettingsTab"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import LabsSettingsTab from "../settings/tabs/LabsSettingsTab"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
import SecuritySettingsTab from "../settings/tabs/SecuritySettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab"; import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab";
import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab"; import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab";
import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab"; import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab";
import HelpSettingsTab from "../settings/tabs/HelpSettingsTab"; import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab";
import FlairSettingsTab from "../settings/tabs/FlairSettingsTab"; import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab";
import sdk from "../../../index"; import sdk from "../../../index";
export default class UserSettingsDialog extends React.Component { export default class UserSettingsDialog extends React.Component {
@ -45,39 +45,39 @@ export default class UserSettingsDialog extends React.Component {
tabs.push(new Tab( tabs.push(new Tab(
_td("Flair"), _td("Flair"),
"mx_UserSettingsDialog_flairIcon", "mx_UserSettingsDialog_flairIcon",
<FlairSettingsTab />, <FlairUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
_td("Notifications"), _td("Notifications"),
"mx_UserSettingsDialog_bellIcon", "mx_UserSettingsDialog_bellIcon",
<NotificationSettingsTab />, <NotificationUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
_td("Preferences"), _td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon", "mx_UserSettingsDialog_preferencesIcon",
<PreferencesSettingsTab />, <PreferencesUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
_td("Voice & Video"), _td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon", "mx_UserSettingsDialog_voiceIcon",
<VoiceSettingsTab />, <VoiceUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
_td("Security & Privacy"), _td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon", "mx_UserSettingsDialog_securityIcon",
<SecuritySettingsTab />, <SecurityUserSettingsTab />,
)); ));
if (SettingsStore.getLabsFeatures().length > 0) { if (SettingsStore.getLabsFeatures().length > 0) {
tabs.push(new Tab( tabs.push(new Tab(
_td("Labs"), _td("Labs"),
"mx_UserSettingsDialog_labsIcon", "mx_UserSettingsDialog_labsIcon",
<LabsSettingsTab />, <LabsUserSettingsTab />,
)); ));
} }
tabs.push(new Tab( tabs.push(new Tab(
_td("Help & About"), _td("Help & About"),
"mx_UserSettingsDialog_helpIcon", "mx_UserSettingsDialog_helpIcon",
<HelpSettingsTab closeSettingsFn={this.props.onFinished} />, <HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
)); ));
return tabs; return tabs;

View file

@ -68,7 +68,9 @@ export default React.createClass({
render() { render() {
const GroupTile = sdk.getComponent('groups.GroupTile'); const GroupTile = sdk.getComponent('groups.GroupTile');
return <div className="mx_GroupPublicity_toggle"> return <div className="mx_GroupPublicity_toggle">
<GroupTile groupId={this.props.groupId} showDescription={false} avatarHeight={40} /> <GroupTile groupId={this.props.groupId} showDescription={false}
avatarHeight={40} draggable={false}
/>
<ToggleSwitch checked={this.state.isGroupPublicised} <ToggleSwitch checked={this.state.isGroupPublicised}
disabled={!this.state.ready || this.state.busy} disabled={!this.state.ready || this.state.busy}
onChange={this._onPublicityToggle} /> onChange={this._onPublicityToggle} />

View file

@ -33,6 +33,7 @@ const GroupTile = React.createClass({
showDescription: PropTypes.bool, showDescription: PropTypes.bool,
// Height of the group avatar in pixels // Height of the group avatar in pixels
avatarHeight: PropTypes.number, avatarHeight: PropTypes.number,
draggable: PropTypes.bool,
}, },
contextTypes: { contextTypes: {
@ -49,6 +50,7 @@ const GroupTile = React.createClass({
return { return {
showDescription: true, showDescription: true,
avatarHeight: 50, avatarHeight: 50,
draggable: true,
}; };
}, },
@ -78,11 +80,21 @@ const GroupTile = React.createClass({
<div className="mx_GroupTile_desc">{ profile.shortDescription }</div> : <div className="mx_GroupTile_desc">{ profile.shortDescription }</div> :
<div />; <div />;
const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp(
profile.avatarUrl, avatarHeight, avatarHeight, "crop", profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null;
) : null;
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273 let avatarElement = (
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156 <div className="mx_GroupTile_avatar">
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}> <BaseAvatar
name={name}
idName={this.props.groupId}
url={httpUrl}
width={avatarHeight}
height={avatarHeight} />
</div>
);
if (this.props.draggable) {
const avatarClone = avatarElement;
avatarElement = (
<Droppable droppableId="my-groups-droppable" type="draggable-TagTile"> <Droppable droppableId="my-groups-droppable" type="draggable-TagTile">
{ (droppableProvided, droppableSnapshot) => ( { (droppableProvided, droppableSnapshot) => (
<div ref={droppableProvided.innerRef}> <div ref={droppableProvided.innerRef}>
@ -99,33 +111,23 @@ const GroupTile = React.createClass({
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
> >
<div className="mx_GroupTile_avatar"> {avatarClone}
<BaseAvatar
name={name}
idName={this.props.groupId}
url={httpUrl}
width={avatarHeight}
height={avatarHeight} />
</div>
</div> </div>
{ /* Instead of a blank placeholder, use a copy of the avatar itself. */ } { /* Instead of a blank placeholder, use a copy of the avatar itself. */ }
{ provided.placeholder ? { provided.placeholder ? avatarClone : <div /> }
<div className="mx_GroupTile_avatar">
<BaseAvatar
name={name}
idName={this.props.groupId}
url={httpUrl}
width={avatarHeight}
height={avatarHeight} />
</div> :
<div />
}
</div> </div>
) } ) }
</Draggable> </Draggable>
</div> </div>
) } ) }
</Droppable> </Droppable>
);
}
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
// instead of onClick. Otherwise we experience https://github.com/vector-im/riot-web/issues/6156
return <AccessibleButton className="mx_GroupTile" onMouseDown={this.onMouseDown} onClick={nop}>
{ avatarElement }
<div className="mx_GroupTile_profile"> <div className="mx_GroupTile_profile">
<div className="mx_GroupTile_name">{ name }</div> <div className="mx_GroupTile_name">{ name }</div>
{ descElement } { descElement }

View file

@ -321,6 +321,9 @@ module.exports = withMatrixClient(React.createClass({
const {tile, replyThread} = this.refs; const {tile, replyThread} = this.refs;
let e2eInfoCallback = null;
if (this.props.mxEvent.isEncrypted()) e2eInfoCallback = () => this.onCryptoClicked();
ContextualMenu.createMenu(MessageContextMenu, { ContextualMenu.createMenu(MessageContextMenu, {
chevronOffset: 10, chevronOffset: 10,
mxEvent: this.props.mxEvent, mxEvent: this.props.mxEvent,
@ -328,7 +331,7 @@ module.exports = withMatrixClient(React.createClass({
top: y, top: y,
eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined, eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined,
collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined, collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined,
e2eInfoCallback: () => this.onCryptoClicked(), e2eInfoCallback: e2eInfoCallback,
onFinished: function() { onFinished: function() {
self.setState({menu: false}); self.setState({menu: false});
}, },

View file

@ -339,12 +339,11 @@ module.exports = React.createClass({
return nameA.localeCompare(nameB); return nameA.localeCompare(nameB);
}, },
onSearchQueryChanged: function(ev) { onSearchQueryChanged: function(searchQuery) {
const q = ev.target.value;
this.setState({ this.setState({
searchQuery: q, searchQuery,
filteredJoinedMembers: this._filterMembers(this.state.members, 'join', q), filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery),
filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', q), filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery),
}); });
}, },
@ -438,6 +437,7 @@ module.exports = React.createClass({
return <div className="mx_MemberList"><Spinner /></div>; return <div className="mx_MemberList"><Spinner /></div>;
} }
const SearchBox = sdk.getComponent('structures.SearchBox');
const TruncatedList = sdk.getComponent("elements.TruncatedList"); const TruncatedList = sdk.getComponent("elements.TruncatedList");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
@ -445,7 +445,6 @@ module.exports = React.createClass({
const room = cli.getRoom(this.props.roomId); const room = cli.getRoom(this.props.roomId);
let inviteButton; let inviteButton;
if (room && room.getMyMembership() === 'join') { if (room && room.getMyMembership() === 'join') {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
inviteButton = inviteButton =
<AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick}> <AccessibleButton className="mx_MemberList_invite" onClick={this.onInviteButtonClick}>
@ -477,9 +476,10 @@ module.exports = React.createClass({
{ invitedSection } { invitedSection }
</div> </div>
</GeminiScrollbarWrapper> </GeminiScrollbarWrapper>
<input className="mx_MemberList_query mx_textinput_icon mx_textinput_search" id="mx_MemberList_query" type="text"
onChange={this.onSearchQueryChanged} value={this.state.searchQuery} <SearchBox className="mx_MemberList_query mx_textinput_icon mx_textinput_search"
placeholder={_t('Filter room members')} /> placeholder={ _t('Filter room members') }
onSearch={ this.onSearchQueryChanged } />
</div> </div>
); );
}, },

View file

@ -362,34 +362,6 @@ export default class MessageComposer extends React.Component {
const canSendMessages = !this.state.tombstone && const canSendMessages = !this.state.tombstone &&
this.props.room.maySendMessage(); 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 = "<no power level event>";
if (plEvent) {
const content = plEvent.getContent();
if (!content) {
plEventString = "<no event content>";
} else {
const stringifyFalsey = (v) => v === null ? '<null>' : (v === undefined ? '<undefined>' : v);
const actualUserPl = stringifyFalsey(content.users ? content.users[room.myUserId] : "<no users in content>");
const usersPl = stringifyFalsey(content.users_default);
const actualEventPl = stringifyFalsey(content.events ? content.events['m.room.message'] : "<no events in content>");
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) { if (canSendMessages) {
// This also currently includes the call buttons. Really we should // This also currently includes the call buttons. Really we should
// check separately for whether we can call, but this is slightly // check separately for whether we can call, but this is slightly
@ -469,8 +441,6 @@ export default class MessageComposer extends React.Component {
</div> </div>
</div>); </div>);
} else { } 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( controls.push(
<div key="controls_error" className="mx_MessageComposer_noperm_error"> <div key="controls_error" className="mx_MessageComposer_noperm_error">
{ _t('You do not have permission to post to this room') } { _t('You do not have permission to post to this room') }

View file

@ -170,6 +170,7 @@ module.exports = React.createClass({
width={24} width={24}
height={24} height={24}
resizeMethod="crop" resizeMethod="crop"
viewUserOnClick={true}
/> />
); );
}); });

View file

@ -16,11 +16,11 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../index"; import sdk from "../../../../..";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../Modal"; import Modal from "../../../../../Modal";
export default class AdvancedRoomSettingsTab extends React.Component { export default class AdvancedRoomSettingsTab extends React.Component {
static propTypes = { static propTypes = {

View file

@ -16,14 +16,14 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import RoomProfileSettings from "../../room_settings/RoomProfileSettings"; import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
import MatrixClientPeg from "../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../index"; import sdk from "../../../../..";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import {MatrixClient} from "matrix-js-sdk"; import {MatrixClient} from "matrix-js-sdk";
import dis from "../../../../dispatcher"; import dis from "../../../../../dispatcher";
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
export default class GeneralRoomSettingsTab extends React.Component { export default class GeneralRoomSettingsTab extends React.Component {
static childContextTypes = { static childContextTypes = {

View file

@ -16,11 +16,11 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {_t, _td} from "../../../../languageHandler"; import {_t, _td} from "../../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../index"; import sdk from "../../../../..";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../Modal"; import Modal from "../../../../../Modal";
const plEventsToLabels = { const plEventsToLabels = {
// These will be translated for us later. // These will be translated for us later.

View file

@ -16,11 +16,11 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import sdk from "../../../../index"; import sdk from "../../../../..";
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import {SettingLevel} from "../../../../settings/SettingsStore"; import {SettingLevel} from "../../../../../settings/SettingsStore";
export default class SecurityRoomSettingsTab extends React.Component { export default class SecurityRoomSettingsTab extends React.Component {
static propTypes = { static propTypes = {
@ -188,7 +188,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
if (joinRule !== 'public' && guestAccess === 'forbidden') { if (joinRule !== 'public' && guestAccess === 'forbidden') {
guestWarning = ( guestWarning = (
<div className='mx_SecurityRoomSettingsTab_warning'> <div className='mx_SecurityRoomSettingsTab_warning'>
<img src={require("../../../../../res/img/warning.svg")} width={15} height={15} /> <img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
<span> <span>
{_t("Guests cannot join this room even if explicitly invited.")}&nbsp; {_t("Guests cannot join this room even if explicitly invited.")}&nbsp;
<a href="" onClick={this._fixGuestAccess}>{_t("Click here to fix")}</a> <a href="" onClick={this._fixGuestAccess}>{_t("Click here to fix")}</a>
@ -201,7 +201,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
if (joinRule === 'public' && !hasAliases) { if (joinRule === 'public' && !hasAliases) {
aliasWarning = ( aliasWarning = (
<div className='mx_SecurityRoomSettingsTab_warning'> <div className='mx_SecurityRoomSettingsTab_warning'>
<img src={require("../../../../../res/img/warning.svg")} width={15} height={15} /> <img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
<span> <span>
{_t("To link to this room, please add an alias.")} {_t("To link to this room, please add an alias.")}
</span> </span>

View file

@ -15,14 +15,13 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import {DragDropContext} from "react-beautiful-dnd"; import GroupUserSettings from "../../../groups/GroupUserSettings";
import GroupUserSettings from "../../groups/GroupUserSettings"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import MatrixClientPeg from "../../../../MatrixClientPeg";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import {MatrixClient} from "matrix-js-sdk"; import {MatrixClient} from "matrix-js-sdk";
export default class FlairSettingsTab extends React.Component { export default class FlairUserSettingsTab extends React.Component {
static childContextTypes = { static childContextTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient), matrixClient: PropTypes.instanceOf(MatrixClient),
}; };
@ -42,9 +41,7 @@ export default class FlairSettingsTab extends React.Component {
<div className="mx_SettingsTab"> <div className="mx_SettingsTab">
<span className="mx_SettingsTab_heading">{_t("Flair")}</span> <span className="mx_SettingsTab_heading">{_t("Flair")}</span>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<DragDropContext>
<GroupUserSettings /> <GroupUserSettings />
</DragDropContext>
</div> </div>
</div> </div>
); );

View file

@ -15,21 +15,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import ProfileSettings from "../ProfileSettings"; import ProfileSettings from "../../ProfileSettings";
import EmailAddresses from "../EmailAddresses"; import EmailAddresses from "../../EmailAddresses";
import PhoneNumbers from "../PhoneNumbers"; import PhoneNumbers from "../../PhoneNumbers";
import Field from "../../elements/Field"; import Field from "../../../elements/Field";
import * as languageHandler from "../../../../languageHandler"; import * as languageHandler from "../../../../../languageHandler";
import {SettingLevel} from "../../../../settings/SettingsStore"; import {SettingLevel} from "../../../../../settings/SettingsStore";
import SettingsStore from "../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore";
import LanguageDropdown from "../../elements/LanguageDropdown"; import LanguageDropdown from "../../../elements/LanguageDropdown";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import DeactivateAccountDialog from "../../dialogs/DeactivateAccountDialog"; import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog";
const PlatformPeg = require("../../../../PlatformPeg"); const PlatformPeg = require("../../../../../PlatformPeg");
const sdk = require('../../../../index'); const sdk = require('../../../../..');
const Modal = require("../../../../Modal"); const Modal = require("../../../../../Modal");
const dis = require("../../../../dispatcher"); const dis = require("../../../../../dispatcher");
export default class GeneralUserSettingsTab extends React.Component { export default class GeneralUserSettingsTab extends React.Component {
constructor() { constructor() {

View file

@ -16,15 +16,15 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {_t, getCurrentLanguage} from "../../../../languageHandler"; import {_t, getCurrentLanguage} from "../../../../../languageHandler";
import MatrixClientPeg from "../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import SdkConfig from "../../../../SdkConfig"; import SdkConfig from "../../../../../SdkConfig";
import createRoom from "../../../../createRoom"; import createRoom from "../../../../../createRoom";
const packageJson = require('../../../../../package.json'); const packageJson = require('../../../../../../package.json');
const Modal = require("../../../../Modal"); const Modal = require("../../../../../Modal");
const sdk = require("../../../../index"); const sdk = require("../../../../..");
const PlatformPeg = require("../../../../PlatformPeg"); const PlatformPeg = require("../../../../../PlatformPeg");
// if this looks like a release, use the 'version' from package.json; else use // 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 // the git sha. Prepend version with v, to look like riot-web version
@ -45,7 +45,7 @@ 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>;
}; };
export default class HelpSettingsTab extends React.Component { export default class HelpUserSettingsTab extends React.Component {
static propTypes = { static propTypes = {
closeSettingsFn: PropTypes.func.isRequired, closeSettingsFn: PropTypes.func.isRequired,
}; };
@ -117,7 +117,7 @@ export default class HelpSettingsTab extends React.Component {
} }
return ( return (
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'> <div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Legal")}</span> <span className='mx_SettingsTab_subheading'>{_t("Legal")}</span>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{legalLinks} {legalLinks}
@ -190,7 +190,7 @@ export default class HelpSettingsTab extends React.Component {
} }
return ( return (
<div className="mx_SettingsTab mx_HelpSettingsTab"> <div className="mx_SettingsTab mx_HelpUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Help & About")}</div> <div className="mx_SettingsTab_heading">{_t("Help & About")}</div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span> <span className='mx_SettingsTab_subheading'>{_t('Bug reporting')}</span>
@ -203,12 +203,12 @@ export default class HelpSettingsTab extends React.Component {
"other users. They do not contain messages.", "other users. They do not contain messages.",
) )
} }
<div className='mx_HelpSettingsTab_debugButton'> <div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onBugReport} kind='primary'> <AccessibleButton onClick={this._onBugReport} kind='primary'>
{_t("Submit debug logs")} {_t("Submit debug logs")}
</AccessibleButton> </AccessibleButton>
</div> </div>
<div className='mx_HelpSettingsTab_debugButton'> <div className='mx_HelpUserSettingsTab_debugButton'>
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'> <AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
{_t("Clear Cache and Reload")} {_t("Clear Cache and Reload")}
</AccessibleButton> </AccessibleButton>
@ -221,7 +221,7 @@ export default class HelpSettingsTab extends React.Component {
{faqText} {faqText}
</div> </div>
</div> </div>
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'> <div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Versions")}</span> <span className='mx_SettingsTab_subheading'>{_t("Versions")}</span>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{_t("matrix-react-sdk version:")} {reactSdkVersion}<br /> {_t("matrix-react-sdk version:")} {reactSdkVersion}<br />
@ -232,7 +232,7 @@ export default class HelpSettingsTab extends React.Component {
</div> </div>
{this._renderLegal()} {this._renderLegal()}
{this._renderCredits()} {this._renderCredits()}
<div className='mx_SettingsTab_section mx_HelpSettingsTab_versions'> <div className='mx_SettingsTab_section mx_HelpUserSettingsTab_versions'>
<span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span> <span className='mx_SettingsTab_subheading'>{_t("Advanced")}</span>
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}<br /> {_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}<br />

View file

@ -15,11 +15,11 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
const sdk = require("../../../../index"); const sdk = require("../../../../..");
export class LabsSettingToggle extends React.Component { export class LabsSettingToggle extends React.Component {
static propTypes = { static propTypes = {
@ -38,7 +38,7 @@ export class LabsSettingToggle extends React.Component {
} }
} }
export default class LabsSettingsTab extends React.Component { export default class LabsUserSettingsTab extends React.Component {
constructor() { constructor() {
super(); super();
} }

View file

@ -15,10 +15,10 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
const sdk = require("../../../../index"); const sdk = require("../../../../..");
export default class NotificationSettingsTab extends React.Component { export default class NotificationUserSettingsTab extends React.Component {
constructor() { constructor() {
super(); super();
} }
@ -26,7 +26,7 @@ export default class NotificationSettingsTab extends React.Component {
render() { render() {
const Notifications = sdk.getComponent("views.settings.Notifications"); const Notifications = sdk.getComponent("views.settings.Notifications");
return ( return (
<div className="mx_SettingsTab mx_NotificationSettingsTab"> <div className="mx_SettingsTab mx_NotificationUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Notifications")}</div> <div className="mx_SettingsTab_heading">{_t("Notifications")}</div>
<div className="mx_SettingsTab_section mx_SettingsTab_subsectionText"> <div className="mx_SettingsTab_section mx_SettingsTab_subsectionText">
<Notifications /> <Notifications />

View file

@ -15,15 +15,15 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import {SettingLevel} from "../../../../settings/SettingsStore"; import {SettingLevel} from "../../../../../settings/SettingsStore";
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import SettingsStore from "../../../../settings/SettingsStore"; import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../elements/Field"; import Field from "../../../elements/Field";
const sdk = require("../../../../index"); const sdk = require("../../../../..");
const PlatformPeg = require("../../../../PlatformPeg"); const PlatformPeg = require("../../../../../PlatformPeg");
export default class PreferencesSettingsTab extends React.Component { export default class PreferencesUserSettingsTab extends React.Component {
static COMPOSER_SETTINGS = [ static COMPOSER_SETTINGS = [
'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji', 'MessageComposerInput.suggestEmoji',
@ -44,6 +44,10 @@ export default class PreferencesSettingsTab extends React.Component {
'showDisplaynameChanges', 'showDisplaynameChanges',
]; ];
static ROOM_LIST_SETTINGS = [
'RoomList.orderByImportance',
];
static ADVANCED_SETTINGS = [ static ADVANCED_SETTINGS = [
'alwaysShowEncryptionIcons', 'alwaysShowEncryptionIcons',
'Pill.shouldShowPillAvatar', 'Pill.shouldShowPillAvatar',
@ -59,24 +63,39 @@ export default class PreferencesSettingsTab extends React.Component {
this.state = { this.state = {
autoLaunch: false, autoLaunch: false,
autoLaunchSupported: false, autoLaunchSupported: false,
minimizeToTray: true,
minimizeToTraySupported: false,
}; };
} }
async componentWillMount(): void { async componentWillMount(): void {
const autoLaunchSupported = await PlatformPeg.get().supportsAutoLaunch(); const platform = PlatformPeg.get();
const autoLaunchSupported = await platform.supportsAutoLaunch();
let autoLaunch = false; let autoLaunch = false;
if (autoLaunchSupported) { if (autoLaunchSupported) {
autoLaunch = await PlatformPeg.get().getAutoLaunchEnabled(); autoLaunch = await platform.getAutoLaunchEnabled();
} }
this.setState({autoLaunch, autoLaunchSupported}); const minimizeToTraySupported = await platform.supportsMinimizeToTray();
let minimizeToTray = true;
if (minimizeToTraySupported) {
minimizeToTray = await platform.getMinimizeToTrayEnabled();
}
this.setState({autoLaunch, autoLaunchSupported, minimizeToTraySupported, minimizeToTray});
} }
_onAutoLaunchChange = (checked) => { _onAutoLaunchChange = (checked) => {
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked}));
}; };
_onMinimizeToTrayChange = (checked) => {
PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked}));
};
_onAutocompleteDelayChange = (e) => { _onAutocompleteDelayChange = (e) => {
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
}; };
@ -94,18 +113,29 @@ export default class PreferencesSettingsTab extends React.Component {
label={_t('Start automatically after system login')} />; label={_t('Start automatically after system login')} />;
} }
let minimizeToTrayOption = null;
if (this.state.minimizeToTraySupported) {
minimizeToTrayOption = <LabelledToggleSwitch value={this.state.minimizeToTray}
onChange={this._onMinimizeToTrayChange}
label={_t('Close button should minimize window to tray')} />;
}
return ( return (
<div className="mx_SettingsTab mx_PreferencesSettingsTab"> <div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Preferences")}</div> <div className="mx_SettingsTab_heading">{_t("Preferences")}</div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Composer")}</span> <span className="mx_SettingsTab_subheading">{_t("Composer")}</span>
{this._renderGroup(PreferencesSettingsTab.COMPOSER_SETTINGS)} {this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Timeline")}</span> <span className="mx_SettingsTab_subheading">{_t("Timeline")}</span>
{this._renderGroup(PreferencesSettingsTab.TIMELINE_SETTINGS)} {this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Room list")}</span>
{this._renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
<span className="mx_SettingsTab_subheading">{_t("Advanced")}</span> <span className="mx_SettingsTab_subheading">{_t("Advanced")}</span>
{this._renderGroup(PreferencesSettingsTab.ADVANCED_SETTINGS)} {this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)}
{minimizeToTrayOption}
{autoLaunchOption} {autoLaunchOption}
<Field id={"autocompleteDelay"} label={_t('Autocomplete delay (ms)')} type='number' <Field id={"autocompleteDelay"} label={_t('Autocomplete delay (ms)')} type='number'
value={SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay')} value={SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay')}

View file

@ -16,15 +16,15 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import {SettingLevel} from "../../../../settings/SettingsStore"; import {SettingLevel} from "../../../../../settings/SettingsStore";
import MatrixClientPeg from "../../../../MatrixClientPeg"; import MatrixClientPeg from "../../../../../MatrixClientPeg";
import * as FormattingUtils from "../../../../utils/FormattingUtils"; import * as FormattingUtils from "../../../../../utils/FormattingUtils";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import Analytics from "../../../../Analytics"; import Analytics from "../../../../../Analytics";
import Promise from "bluebird"; import Promise from "bluebird";
import Modal from "../../../../Modal"; import Modal from "../../../../../Modal";
import sdk from "../../../../index"; import sdk from "../../../../..";
export class IgnoredUser extends React.Component { export class IgnoredUser extends React.Component {
static propTypes = { static propTypes = {
@ -38,7 +38,7 @@ export class IgnoredUser extends React.Component {
render() { render() {
return ( return (
<div className='mx_SecuritySettingsTab_ignoredUser'> <div className='mx_SecurityUserSettingsTab_ignoredUser'>
<AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'> <AccessibleButton onClick={this._onUnignoreClicked} kind='primary_sm'>
{_t('Unignore')} {_t('Unignore')}
</AccessibleButton> </AccessibleButton>
@ -48,7 +48,7 @@ export class IgnoredUser extends React.Component {
} }
} }
export default class SecuritySettingsTab extends React.Component { export default class SecurityUserSettingsTab extends React.Component {
constructor() { constructor() {
super(); super();
@ -68,14 +68,14 @@ export default class SecuritySettingsTab extends React.Component {
_onExportE2eKeysClicked = () => { _onExportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Export E2E Keys', '', Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../../async-components/views/dialogs/ExportE2eKeysDialog'), import('../../../../../async-components/views/dialogs/ExportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()}, {matrixClient: MatrixClientPeg.get()},
); );
}; };
_onImportE2eKeysClicked = () => { _onImportE2eKeysClicked = () => {
Modal.createTrackedDialogAsync('Import E2E Keys', '', Modal.createTrackedDialogAsync('Import E2E Keys', '',
import('../../../../async-components/views/dialogs/ImportE2eKeysDialog'), import('../../../../../async-components/views/dialogs/ImportE2eKeysDialog'),
{matrixClient: MatrixClientPeg.get()}, {matrixClient: MatrixClientPeg.get()},
); );
}; };
@ -126,7 +126,7 @@ export default class SecuritySettingsTab extends React.Component {
let importExportButtons = null; let importExportButtons = null;
if (client.isCryptoEnabled()) { if (client.isCryptoEnabled()) {
importExportButtons = ( importExportButtons = (
<div className='mx_SecuritySettingsTab_importExportButtons'> <div className='mx_SecurityUserSettingsTab_importExportButtons'>
<AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}> <AccessibleButton kind='primary' onClick={this._onExportE2eKeysClicked}>
{_t("Export E2E room keys")} {_t("Export E2E room keys")}
</AccessibleButton> </AccessibleButton>
@ -140,7 +140,7 @@ export default class SecuritySettingsTab extends React.Component {
return ( return (
<div className='mx_SettingsTab_section'> <div className='mx_SettingsTab_section'>
<span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span> <span className='mx_SettingsTab_subheading'>{_t("Cryptography")}</span>
<ul className='mx_SettingsTab_subsectionText mx_SecuritySettingsTab_deviceInfo'> <ul className='mx_SettingsTab_subsectionText mx_SecurityUserSettingsTab_deviceInfo'>
<li> <li>
<label>{_t("Device ID:")}</label> <label>{_t("Device ID:")}</label>
<span><code>{deviceId}</code></span> <span><code>{deviceId}</code></span>
@ -207,7 +207,7 @@ export default class SecuritySettingsTab extends React.Component {
); );
return ( return (
<div className="mx_SettingsTab mx_SecuritySettingsTab"> <div className="mx_SettingsTab mx_SecurityUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div> <div className="mx_SettingsTab_heading">{_t("Security & Privacy")}</div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{_t("Devices")}</span> <span className="mx_SettingsTab_subheading">{_t("Devices")}</span>

View file

@ -15,16 +15,16 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import {_t} from "../../../../languageHandler"; import {_t} from "../../../../../languageHandler";
import CallMediaHandler from "../../../../CallMediaHandler"; import CallMediaHandler from "../../../../../CallMediaHandler";
import Field from "../../elements/Field"; import Field from "../../../elements/Field";
import AccessibleButton from "../../elements/AccessibleButton"; import AccessibleButton from "../../../elements/AccessibleButton";
import {SettingLevel} from "../../../../settings/SettingsStore"; import {SettingLevel} from "../../../../../settings/SettingsStore";
const Modal = require("../../../../Modal"); const Modal = require("../../../../../Modal");
const sdk = require("../../../../index"); const sdk = require("../../../../..");
const MatrixClientPeg = require("../../../../MatrixClientPeg"); const MatrixClientPeg = require("../../../../../MatrixClientPeg");
export default class VoiceSettingsTab extends React.Component { export default class VoiceUserSettingsTab extends React.Component {
constructor() { constructor() {
super(); super();
@ -103,7 +103,7 @@ export default class VoiceSettingsTab extends React.Component {
let webcamDropdown = null; let webcamDropdown = null;
if (this.state.mediaDevices === false) { if (this.state.mediaDevices === false) {
requestButton = ( requestButton = (
<div className='mx_VoiceSettingsTab_missingMediaPermissions'> <div className='mx_VoiceUserSettingsTab_missingMediaPermissions'>
<p>{_t("Missing media permissions, click the button below to request.")}</p> <p>{_t("Missing media permissions, click the button below to request.")}</p>
<AccessibleButton onClick={this._requestMediaPermissions} kind="primary"> <AccessibleButton onClick={this._requestMediaPermissions} kind="primary">
{_t("Request media permissions")} {_t("Request media permissions")}
@ -166,7 +166,7 @@ export default class VoiceSettingsTab extends React.Component {
} }
return ( return (
<div className="mx_SettingsTab mx_VoiceSettingsTab"> <div className="mx_SettingsTab mx_VoiceUserSettingsTab">
<div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div> <div className="mx_SettingsTab_heading">{_t("Voice & Video")}</div>
<div className="mx_SettingsTab_section"> <div className="mx_SettingsTab_section">
{requestButton} {requestButton}

View file

@ -306,6 +306,7 @@
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "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", "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs",
"Show developer tools": "Show developer tools", "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 app version information": "Collecting app version information",
"Collecting logs": "Collecting logs", "Collecting logs": "Collecting logs",
"Uploading report": "Uploading report", "Uploading report": "Uploading report",
@ -500,19 +501,7 @@
"Upload profile picture": "Upload profile picture", "Upload profile picture": "Upload profile picture",
"Display Name": "Display Name", "Display Name": "Display Name",
"Save": "Save", "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", "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?", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
"Success": "Success", "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", "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", "Account management": "Account management",
"Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!",
"Deactivate Account": "Deactivate Account", "Deactivate Account": "Deactivate Account",
"General": "General",
"Legal": "Legal", "Legal": "Legal",
"Credits": "Credits", "Credits": "Credits",
"For help with using Riot, click <a>here</a>.": "For help with using Riot, click <a>here</a>.", "For help with using Riot, click <a>here</a>.": "For help with using Riot, click <a>here</a>.",
@ -551,10 +541,50 @@
"Labs": "Labs", "Labs": "Labs",
"Notifications": "Notifications", "Notifications": "Notifications",
"Start automatically after system login": "Start automatically after system login", "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", "Preferences": "Preferences",
"Composer": "Composer", "Composer": "Composer",
"Timeline": "Timeline", "Timeline": "Timeline",
"Room list": "Room list",
"Autocomplete delay (ms)": "Autocomplete delay (ms)", "Autocomplete delay (ms)": "Autocomplete delay (ms)",
"Unignore": "Unignore",
"<not supported>": "<not supported>",
"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 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 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", "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 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 were invited)": "Members only (since they were invited)",
"Members only (since they joined)": "Members only (since they joined)", "Members only (since they joined)": "Members only (since they joined)",
"Security & Privacy": "Security & Privacy",
"Encryption": "Encryption", "Encryption": "Encryption",
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
"Encrypted": "Encrypted", "Encrypted": "Encrypted",
"Who can access this room?": "Who can access this room?", "Who can access this room?": "Who can access this room?",
"Who can read history?": "Who can read history?", "Who can read history?": "Who can read history?",
"Unignore": "Unignore",
"<not supported>": "<not supported>",
"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", "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",
@ -1202,6 +1205,7 @@
"View Decrypted Source": "View Decrypted Source", "View Decrypted Source": "View Decrypted Source",
"Unhide Preview": "Unhide Preview", "Unhide Preview": "Unhide Preview",
"Share Message": "Share Message", "Share Message": "Share Message",
"Share Permalink": "Share Permalink",
"Quote": "Quote", "Quote": "Quote",
"Source URL": "Source URL", "Source URL": "Source URL",
"Collapse Reply Thread": "Collapse Reply Thread", "Collapse Reply Thread": "Collapse Reply Thread",

View file

@ -1,6 +1,6 @@
/* /*
Copyright 2017 Travis Ralston 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"); 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.
@ -340,4 +340,9 @@ export const SETTINGS = {
displayName: _td('Show developer tools'), displayName: _td('Show developer tools'),
default: false, 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,
},
}; };

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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.
@ -23,6 +24,7 @@ import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler"; import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
import {_t} from '../languageHandler'; import {_t} from '../languageHandler';
import SdkConfig from "../SdkConfig"; import SdkConfig from "../SdkConfig";
import dis from '../dispatcher';
import {SETTINGS} from "./Settings"; import {SETTINGS} from "./Settings";
import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
@ -98,6 +100,121 @@ const LEVEL_ORDER = [
* be enabled). * be enabled).
*/ */
export default class SettingsStore { 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 || '<null room>'} 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 * Gets the translated display name for a given setting
* @param {string} settingName The setting to look up. * @param {string} settingName The setting to look up.

View file

@ -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);
}
}
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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,14 +15,49 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import {WatchManager} from "../WatchManager";
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
/** /**
* Gets and sets settings at the "account" level for the current user. * Gets and sets settings at the "account" level for the current user.
* This handler does not make use of the roomId parameter. * 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) { getValue(settingName, roomId) {
// Special case URL previews // Special case URL previews
if (settingName === "urlPreviewsEnabled") { if (settingName === "urlPreviewsEnabled") {
@ -67,6 +103,14 @@ export default class AccountSettingHandler extends SettingsHandler {
return cli !== undefined && cli !== null; 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") { _getSettings(eventType = "im.vector.web.settings") {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!cli) return null; if (!cli) return null;

View file

@ -47,4 +47,12 @@ export default class ConfigSettingsHandler extends SettingsHandler {
isSupported() { isSupported() {
return true; // SdkConfig is always there return true; // SdkConfig is always there
} }
watchSetting(settingName, roomId, cb) {
// no-op: no changes possible
}
unwatchSetting(cb) {
// no-op: no changes possible
}
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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.
@ -51,4 +52,12 @@ export default class DefaultSettingsHandler extends SettingsHandler {
isSupported() { isSupported() {
return true; return true;
} }
watchSetting(settingName, roomId, cb) {
// no-op: no changes possible
}
unwatchSetting(cb) {
// no-op: no changes possible
}
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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.
@ -17,6 +18,7 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler"; import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from "../../MatrixClientPeg"; import MatrixClientPeg from "../../MatrixClientPeg";
import {WatchManager} from "../WatchManager";
/** /**
* Gets and sets settings at the "device" level for the current device. * Gets and sets settings at the "device" level for the current device.
@ -31,6 +33,7 @@ export default class DeviceSettingsHandler extends SettingsHandler {
constructor(featureNames) { constructor(featureNames) {
super(); super();
this._featureNames = featureNames; this._featureNames = featureNames;
this._watchers = new WatchManager();
} }
getValue(settingName, roomId) { getValue(settingName, roomId) {
@ -66,18 +69,22 @@ export default class DeviceSettingsHandler extends SettingsHandler {
// Special case notifications // Special case notifications
if (settingName === "notificationsEnabled") { if (settingName === "notificationsEnabled") {
localStorage.setItem("notifications_enabled", newValue); localStorage.setItem("notifications_enabled", newValue);
this._watchers.notifyUpdate(settingName, null, newValue);
return Promise.resolve(); return Promise.resolve();
} else if (settingName === "notificationBodyEnabled") { } else if (settingName === "notificationBodyEnabled") {
localStorage.setItem("notifications_body_enabled", newValue); localStorage.setItem("notifications_body_enabled", newValue);
this._watchers.notifyUpdate(settingName, null, newValue);
return Promise.resolve(); return Promise.resolve();
} else if (settingName === "audioNotificationsEnabled") { } else if (settingName === "audioNotificationsEnabled") {
localStorage.setItem("audio_notifications_enabled", newValue); localStorage.setItem("audio_notifications_enabled", newValue);
this._watchers.notifyUpdate(settingName, null, newValue);
return Promise.resolve(); return Promise.resolve();
} }
const settings = this._getSettings() || {}; const settings = this._getSettings() || {};
settings[settingName] = newValue; settings[settingName] = newValue;
localStorage.setItem("mx_local_settings", JSON.stringify(settings)); localStorage.setItem("mx_local_settings", JSON.stringify(settings));
this._watchers.notifyUpdate(settingName, null, newValue);
return Promise.resolve(); return Promise.resolve();
} }
@ -90,6 +97,14 @@ export default class DeviceSettingsHandler extends SettingsHandler {
return localStorage !== undefined && localStorage !== null; return localStorage !== undefined && localStorage !== null;
} }
watchSetting(settingName, roomId, cb) {
this._watchers.watchSetting(settingName, roomId, cb);
}
unwatchSetting(cb) {
this._watchers.unwatchSetting(cb);
}
_getSettings() { _getSettings() {
const value = localStorage.getItem("mx_local_settings"); const value = localStorage.getItem("mx_local_settings");
if (!value) return null; if (!value) return null;
@ -111,5 +126,6 @@ export default class DeviceSettingsHandler extends SettingsHandler {
_writeFeature(featureName, enabled) { _writeFeature(featureName, enabled) {
localStorage.setItem("mx_labs_feature_" + featureName, enabled); localStorage.setItem("mx_labs_feature_" + featureName, enabled);
this._watchers.notifyUpdate(featureName, null, enabled);
} }
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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.
@ -66,4 +67,12 @@ export default class LocalEchoWrapper extends SettingsHandler {
isSupported() { isSupported() {
return this._handler.isSupported(); return this._handler.isSupported();
} }
watchSetting(settingName, roomId, cb) {
this._handler.watchSetting(settingName, roomId, cb);
}
unwatchSetting(cb) {
this._handler.unwatchSetting(cb);
}
} }

View file

@ -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");
}
}

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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,13 +15,51 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg'; 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. * 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) { getValue(settingName, roomId) {
// Special case URL previews // Special case URL previews
if (settingName === "urlPreviewsEnabled") { if (settingName === "urlPreviewsEnabled") {
@ -74,6 +113,14 @@ export default class RoomAccountSettingsHandler extends SettingsHandler {
return cli !== undefined && cli !== null; 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") { _getSettings(roomId, eventType = "im.vector.web.settings") {
const room = MatrixClientPeg.get().getRoom(roomId); const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return null; if (!room) return null;

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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.
@ -16,12 +17,19 @@ limitations under the License.
import Promise from 'bluebird'; import Promise from 'bluebird';
import SettingsHandler from "./SettingsHandler"; import SettingsHandler from "./SettingsHandler";
import {WatchManager} from "../WatchManager";
/** /**
* Gets and sets settings at the "room-device" level for the current device in a particular * Gets and sets settings at the "room-device" level for the current device in a particular
* room. * room.
*/ */
export default class RoomDeviceSettingsHandler extends SettingsHandler { export default class RoomDeviceSettingsHandler extends SettingsHandler {
constructor() {
super();
this._watchers = new WatchManager();
}
getValue(settingName, roomId) { getValue(settingName, roomId) {
// Special case blacklist setting to use legacy values // Special case blacklist setting to use legacy values
if (settingName === "blacklistUnverifiedDevices") { if (settingName === "blacklistUnverifiedDevices") {
@ -44,6 +52,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {}; if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {};
value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue; value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue;
localStorage.setItem("mx_local_settings", JSON.stringify(value)); localStorage.setItem("mx_local_settings", JSON.stringify(value));
this._watchers.notifyUpdate(settingName, roomId, newValue);
return Promise.resolve(); return Promise.resolve();
} }
@ -54,6 +63,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
localStorage.setItem(this._getKey(settingName, roomId), newValue); localStorage.setItem(this._getKey(settingName, roomId), newValue);
} }
this._watchers.notifyUpdate(settingName, roomId, newValue);
return Promise.resolve(); return Promise.resolve();
} }
@ -65,6 +75,14 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler {
return localStorage !== undefined && localStorage !== null; return localStorage !== undefined && localStorage !== null;
} }
watchSetting(settingName, roomId, cb) {
this._watchers.watchSetting(settingName, roomId, cb);
}
unwatchSetting(cb) {
this._watchers.unwatchSetting(cb);
}
_read(key) { _read(key) {
const rawValue = localStorage.getItem(key); const rawValue = localStorage.getItem(key);
if (!rawValue) return null; if (!rawValue) return null;

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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,13 +15,49 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import SettingsHandler from "./SettingsHandler";
import MatrixClientPeg from '../../MatrixClientPeg'; import MatrixClientPeg from '../../MatrixClientPeg';
import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler";
import {WatchManager} from "../WatchManager";
/** /**
* Gets and sets settings at the "room" level. * 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) { getValue(settingName, roomId) {
// Special case URL previews // Special case URL previews
if (settingName === "urlPreviewsEnabled") { if (settingName === "urlPreviewsEnabled") {
@ -64,6 +101,14 @@ export default class RoomSettingsHandler extends SettingsHandler {
return cli !== undefined && cli !== null; 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") { _getSettings(roomId, eventType = "im.vector.web.settings") {
const room = MatrixClientPeg.get().getRoom(roomId); const room = MatrixClientPeg.get().getRoom(roomId);
if (!room) return null; if (!room) return null;

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2017 Travis Ralston Copyright 2017 Travis Ralston
Copyright 2019 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.
@ -68,4 +69,27 @@ export default class SettingsHandler {
isSupported() { isSupported() {
return false; 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");
}
} }

View file

@ -59,6 +59,22 @@ class RoomListStore extends Store {
this._recentsComparator = this._recentsComparator.bind(this); 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() { _init() {
// Initialise state // Initialise state
const defaultLists = { const defaultLists = {
@ -77,7 +93,10 @@ class RoomListStore extends Store {
presentationLists: defaultLists, // like `lists`, but with arrays of rooms instead presentationLists: defaultLists, // like `lists`, but with arrays of rooms instead
ready: false, ready: false,
stickyRoomId: null, stickyRoomId: null,
orderRoomsByImportance: true,
}; };
SettingsStore.monitorSetting('RoomList.orderByImportance', null);
} }
_setState(newState) { _setState(newState) {
@ -99,6 +118,11 @@ class RoomListStore extends Store {
__onDispatch(payload) { __onDispatch(payload) {
const logicallyReady = this._matrixClient && this._state.ready; const logicallyReady = this._matrixClient && this._state.ready;
switch (payload.action) { switch (payload.action) {
case 'setting_updated': {
if (payload.settingName !== 'RoomList.orderByImportance') break;
this.updateSortingAlgorithm();
}
break;
// Initialise state after initial sync // Initialise state after initial sync
case 'MatrixActions.sync': { case 'MatrixActions.sync': {
if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) { if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) {
@ -106,7 +130,7 @@ class RoomListStore extends Store {
} }
this._matrixClient = payload.matrixClient; this._matrixClient = payload.matrixClient;
this._generateInitialRoomLists(); this.updateSortingAlgorithm(/*force=*/true);
} }
break; break;
case 'MatrixActions.Room.receipt': { case 'MatrixActions.Room.receipt': {
@ -517,6 +541,14 @@ class RoomListStore extends Store {
} }
_calculateCategory(room) { _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; const mentions = room.getUnreadNotificationCount("highlight") > 0;
if (mentions) return CATEGORY_RED; if (mentions) return CATEGORY_RED;