From 27abd7d507a8e6a878546d15b9d72dc4e955bff8 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 21 Feb 2019 11:35:50 +0000 Subject: [PATCH 01/63] Update validation order to match field order Validation is meant to run in reverse order of the fields (so that the last message emitted is for the first invalid field). --- src/components/views/auth/RegistrationForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index eabdcd0dd2..c4fe31f4c6 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -82,11 +82,11 @@ module.exports = React.createClass({ // is the one from the first invalid field. // It's not super ideal that this just calls // onError once for each invalid field. + this.validateField(FIELD_PHONE_NUMBER, ev.type); + this.validateField(FIELD_EMAIL, ev.type); this.validateField(FIELD_PASSWORD_CONFIRM, ev.type); this.validateField(FIELD_PASSWORD, ev.type); this.validateField(FIELD_USERNAME, ev.type); - this.validateField(FIELD_PHONE_NUMBER, ev.type); - this.validateField(FIELD_EMAIL, ev.type); const self = this; if (this.allFieldsValid()) { From acae2e9976c2d5267903cfcd353c977e4c9fa488 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 21 Feb 2019 12:36:31 +0000 Subject: [PATCH 02/63] Wait until password confirm is non-empty If password confirm is empty on blur, there's no reason to try validating it. The user may just be tabbing through fields. --- src/components/views/auth/RegistrationForm.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index c4fe31f4c6..bd597de66a 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -206,10 +206,14 @@ module.exports = React.createClass({ } break; case FIELD_PASSWORD_CONFIRM: - this.markFieldValid( - fieldID, pwd1 == pwd2, - "RegistrationForm.ERR_PASSWORD_MISMATCH", - ); + if (allowEmpty && pwd2 === "") { + this.markFieldValid(fieldID, true); + } else { + this.markFieldValid( + fieldID, pwd1 == pwd2, + "RegistrationForm.ERR_PASSWORD_MISMATCH", + ); + } break; } }, From 86a375c7dacd1d02e7a363e49e4054cb23e04627 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 21 Feb 2019 14:31:11 +0000 Subject: [PATCH 03/63] Report validity state of all registration fields on any change This passes the validity state of all fields to the consumer of `RegistrationForm` via the `onValdiationChange` callback, instead of just the most recent error. In addition, we notify the consumer for any validation change, whether success or failure. This allows old validation messages to be properly cleared. It also allows the consumer to be aware of multiple validation errors and display the next one after you have fixed the first. Fixes https://github.com/vector-im/riot-web/issues/8769 --- .../structures/auth/Registration.js | 13 ++++++-- src/components/views/auth/RegistrationForm.js | 33 ++++++++++--------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 30b553a5b1..b00c0c193f 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -288,7 +288,16 @@ module.exports = React.createClass({ }); }, - onFormValidationFailed: function(errCode) { + onFormValidationChange: function(fieldErrors) { + // Find the first error and show that. + const errCode = Object.values(fieldErrors).find(value => value); + if (!errCode) { + this.setState({ + errorText: null, + }); + return; + } + let errMsg; switch (errCode) { case "RegistrationForm.ERR_PASSWORD_MISSING": @@ -490,7 +499,7 @@ module.exports = React.createClass({ defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPassword={this.state.formVals.password} minPasswordLength={MIN_PASSWORD_LENGTH} - onError={this.onFormValidationFailed} + onValidationChange={this.onFormValidationChange} onRegisterClick={this.onFormSubmit} onEditServerDetailsClick={onEditServerDetailsClick} flows={this.state.flows} diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index bd597de66a..d031dc7bdd 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -46,7 +46,7 @@ module.exports = React.createClass({ defaultUsername: PropTypes.string, defaultPassword: PropTypes.string, minPasswordLength: PropTypes.number, - onError: PropTypes.func, + onValidationChange: PropTypes.func, onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, @@ -60,15 +60,14 @@ module.exports = React.createClass({ getDefaultProps: function() { return { minPasswordLength: 6, - onError: function(e) { - console.error(e); - }, + onValidationChange: console.error, }; }, getInitialState: function() { return { - fieldValid: {}, + // Field error codes by field ID + fieldErrors: {}, // The ISO2 country code selected in the phone number entry phoneCountry: this.props.defaultPhoneCountry, }; @@ -81,7 +80,7 @@ module.exports = React.createClass({ // the error that ends up being displayed // is the one from the first invalid field. // It's not super ideal that this just calls - // onError once for each invalid field. + // onValidationChange once for each invalid field. this.validateField(FIELD_PHONE_NUMBER, ev.type); this.validateField(FIELD_EMAIL, ev.type); this.validateField(FIELD_PASSWORD_CONFIRM, ev.type); @@ -134,9 +133,9 @@ module.exports = React.createClass({ * @returns {boolean} true if all fields were valid last time they were validated. */ allFieldsValid: function() { - const keys = Object.keys(this.state.fieldValid); + const keys = Object.keys(this.state.fieldErrors); for (let i = 0; i < keys.length; ++i) { - if (this.state.fieldValid[keys[i]] == false) { + if (this.state.fieldErrors[keys[i]]) { return false; } } @@ -218,13 +217,17 @@ module.exports = React.createClass({ } }, - markFieldValid: function(fieldID, val, errorCode) { - const fieldValid = this.state.fieldValid; - fieldValid[fieldID] = val; - this.setState({fieldValid: fieldValid}); - if (!val) { - this.props.onError(errorCode); + markFieldValid: function(fieldID, valid, errorCode) { + const { fieldErrors } = this.state; + if (valid) { + fieldErrors[fieldID] = null; + } else { + fieldErrors[fieldID] = errorCode; } + this.setState({ + fieldErrors, + }); + this.props.onValidationChange(fieldErrors); }, fieldElementById(fieldID) { @@ -244,7 +247,7 @@ module.exports = React.createClass({ _classForField: function(fieldID, ...baseClasses) { let cls = baseClasses.join(' '); - if (this.state.fieldValid[fieldID] === false) { + if (this.state.fieldErrors[fieldID]) { if (cls) cls += ' '; cls += 'error'; } From 8e32798f45ad47804b4d5a3ebe7bbc05d5a8ffd1 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 21 Feb 2019 14:41:42 +0000 Subject: [PATCH 04/63] Ensure fields with errors are clearly visible Until we have better validation, let's at least ensure fields with errors are properly marked via color. --- res/css/views/auth/_AuthBody.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/res/css/views/auth/_AuthBody.scss b/res/css/views/auth/_AuthBody.scss index 6216bdd4b8..778f5f6a4d 100644 --- a/res/css/views/auth/_AuthBody.scss +++ b/res/css/views/auth/_AuthBody.scss @@ -58,6 +58,10 @@ limitations under the License. background-color: $authpage-body-bg-color; } +.mx_AuthBody input.error { + color: $warning-color; +} + .mx_AuthBody_editServerDetails { padding-left: 1em; font-size: 12px; From 20cd1987846485206db27c754a8ce4a3deabbc7c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Feb 2019 10:31:14 -0700 Subject: [PATCH 05/63] Only set e2e info callback if the event is encrypted Fixes https://github.com/vector-im/riot-web/issues/8551 --- src/components/views/rooms/EventTile.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 15b8357b24..a537c4ca5d 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -321,6 +321,9 @@ module.exports = withMatrixClient(React.createClass({ const {tile, replyThread} = this.refs; + let e2eInfoCallback = null; + if (this.props.mxEvent.isEncrypted()) e2eInfoCallback = () => this.onCryptoClicked(); + ContextualMenu.createMenu(MessageContextMenu, { chevronOffset: 10, mxEvent: this.props.mxEvent, @@ -328,7 +331,7 @@ module.exports = withMatrixClient(React.createClass({ top: y, eventTileOps: tile && tile.getEventTileOps ? tile.getEventTileOps() : undefined, collapseReplyThread: replyThread && replyThread.canCollapse() ? replyThread.collapse : undefined, - e2eInfoCallback: () => this.onCryptoClicked(), + e2eInfoCallback: e2eInfoCallback, onFinished: function() { self.setState({menu: false}); }, From bd54a401bc823c62dd8e9b74199161ff55a636a9 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Feb 2019 10:47:18 -0700 Subject: [PATCH 06/63] Sort settings tabs into a logical structure Fixes https://github.com/vector-im/riot-web/issues/8864 --- res/css/_components.scss | 18 ++--- .../{ => room}/_GeneralRoomSettingsTab.scss | 0 .../{ => room}/_RolesRoomSettingsTab.scss | 0 .../{ => room}/_SecurityRoomSettingsTab.scss | 0 .../{ => user}/_GeneralUserSettingsTab.scss | 0 .../_HelpUserSettingsTab.scss} | 4 +- .../_NotificationUserSettingsTab.scss} | 2 +- .../_PreferencesUserSettingsTab.scss} | 4 +- .../_SecurityUserSettingsTab.scss} | 18 ++--- .../_VoiceUserSettingsTab.scss} | 6 +- .../views/dialogs/RoomSettingsDialog.js | 8 +- .../views/dialogs/UserSettingsDialog.js | 30 +++---- .../{ => room}/AdvancedRoomSettingsTab.js | 10 +-- .../tabs/{ => room}/GeneralRoomSettingsTab.js | 14 ++-- .../tabs/{ => room}/RolesRoomSettingsTab.js | 10 +-- .../{ => room}/SecurityRoomSettingsTab.js | 14 ++-- .../FlairUserSettingsTab.js} | 8 +- .../tabs/{ => user}/GeneralUserSettingsTab.js | 30 +++---- .../HelpUserSettingsTab.js} | 32 ++++---- .../LabsUserSettingsTab.js} | 10 +-- .../NotificationUserSettingsTab.js} | 8 +- .../PreferencesUserSettingsTab.js} | 24 +++--- .../SecurityUserSettingsTab.js} | 30 +++---- .../VoiceUserSettingsTab.js} | 22 +++--- src/i18n/strings/en_EN.json | 78 +++++++++---------- 25 files changed, 190 insertions(+), 190 deletions(-) rename res/css/views/settings/tabs/{ => room}/_GeneralRoomSettingsTab.scss (100%) rename res/css/views/settings/tabs/{ => room}/_RolesRoomSettingsTab.scss (100%) rename res/css/views/settings/tabs/{ => room}/_SecurityRoomSettingsTab.scss (100%) rename res/css/views/settings/tabs/{ => user}/_GeneralUserSettingsTab.scss (100%) rename res/css/views/settings/tabs/{_HelpSettingsTab.scss => user/_HelpUserSettingsTab.scss} (87%) rename res/css/views/settings/tabs/{_NotificationSettingsTab.scss => user/_NotificationUserSettingsTab.scss} (91%) rename res/css/views/settings/tabs/{_PreferencesSettingsTab.scss => user/_PreferencesUserSettingsTab.scss} (89%) rename res/css/views/settings/tabs/{_SecuritySettingsTab.scss => user/_SecurityUserSettingsTab.scss} (67%) rename res/css/views/settings/tabs/{_VoiceSettingsTab.scss => user/_VoiceUserSettingsTab.scss} (84%) rename src/components/views/settings/tabs/{ => room}/AdvancedRoomSettingsTab.js (93%) rename src/components/views/settings/tabs/{ => room}/GeneralRoomSettingsTab.js (92%) rename src/components/views/settings/tabs/{ => room}/RolesRoomSettingsTab.js (98%) rename src/components/views/settings/tabs/{ => room}/SecurityRoomSettingsTab.js (96%) rename src/components/views/settings/tabs/{FlairSettingsTab.js => user/FlairUserSettingsTab.js} (84%) rename src/components/views/settings/tabs/{ => user}/GeneralUserSettingsTab.js (88%) rename src/components/views/settings/tabs/{HelpSettingsTab.js => user/HelpUserSettingsTab.js} (90%) rename src/components/views/settings/tabs/{LabsSettingsTab.js => user/LabsUserSettingsTab.js} (85%) rename src/components/views/settings/tabs/{NotificationSettingsTab.js => user/NotificationUserSettingsTab.js} (80%) rename src/components/views/settings/tabs/{PreferencesSettingsTab.js => user/PreferencesUserSettingsTab.js} (81%) rename src/components/views/settings/tabs/{SecuritySettingsTab.js => user/SecurityUserSettingsTab.js} (89%) rename src/components/views/settings/tabs/{VoiceSettingsTab.js => user/VoiceUserSettingsTab.js} (90%) diff --git a/res/css/_components.scss b/res/css/_components.scss index 6aed78a627..f3b07255ae 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -150,16 +150,16 @@ @import "./views/settings/_Notifications.scss"; @import "./views/settings/_PhoneNumbers.scss"; @import "./views/settings/_ProfileSettings.scss"; -@import "./views/settings/tabs/_GeneralRoomSettingsTab.scss"; -@import "./views/settings/tabs/_GeneralUserSettingsTab.scss"; -@import "./views/settings/tabs/_HelpSettingsTab.scss"; -@import "./views/settings/tabs/_NotificationSettingsTab.scss"; -@import "./views/settings/tabs/_PreferencesSettingsTab.scss"; -@import "./views/settings/tabs/_RolesRoomSettingsTab.scss"; -@import "./views/settings/tabs/_SecurityRoomSettingsTab.scss"; -@import "./views/settings/tabs/_SecuritySettingsTab.scss"; @import "./views/settings/tabs/_SettingsTab.scss"; -@import "./views/settings/tabs/_VoiceSettingsTab.scss"; +@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss"; +@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss"; +@import "./views/settings/tabs/room/_SecurityRoomSettingsTab.scss"; +@import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_HelpUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; +@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; @import "./views/verification/_VerificationShowSas.scss"; @import "./views/voip/_CallView.scss"; @import "./views/voip/_IncomingCallbox.scss"; diff --git a/res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss similarity index 100% rename from res/css/views/settings/tabs/_GeneralRoomSettingsTab.scss rename to res/css/views/settings/tabs/room/_GeneralRoomSettingsTab.scss diff --git a/res/css/views/settings/tabs/_RolesRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_RolesRoomSettingsTab.scss similarity index 100% rename from res/css/views/settings/tabs/_RolesRoomSettingsTab.scss rename to res/css/views/settings/tabs/room/_RolesRoomSettingsTab.scss diff --git a/res/css/views/settings/tabs/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss similarity index 100% rename from res/css/views/settings/tabs/_SecurityRoomSettingsTab.scss rename to res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss diff --git a/res/css/views/settings/tabs/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss similarity index 100% rename from res/css/views/settings/tabs/_GeneralUserSettingsTab.scss rename to res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss diff --git a/res/css/views/settings/tabs/_HelpSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss similarity index 87% rename from res/css/views/settings/tabs/_HelpSettingsTab.scss rename to res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss index 249f06ca95..fa0d0edeb7 100644 --- a/res/css/views/settings/tabs/_HelpSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_HelpSettingsTab_debugButton { +.mx_HelpUserSettingsTab_debugButton { margin-bottom: 5px; margin-top: 5px; } -.mx_HelpSettingsTab span.mx_AccessibleButton { +.mx_HelpUserSettingsTab span.mx_AccessibleButton { word-break: break-word; } \ No newline at end of file diff --git a/res/css/views/settings/tabs/_NotificationSettingsTab.scss b/res/css/views/settings/tabs/user/_NotificationUserSettingsTab.scss similarity index 91% rename from res/css/views/settings/tabs/_NotificationSettingsTab.scss rename to res/css/views/settings/tabs/user/_NotificationUserSettingsTab.scss index 8fdb688496..3cebd2958e 100644 --- a/res/css/views/settings/tabs/_NotificationSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_NotificationUserSettingsTab.scss @@ -14,6 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_NotificationSettingsTab .mx_SettingsTab_heading { +.mx_NotificationUserSettingsTab .mx_SettingsTab_heading { margin-bottom: 10px; // Give some spacing between the title and the first elements } \ No newline at end of file diff --git a/res/css/views/settings/tabs/_PreferencesSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss similarity index 89% rename from res/css/views/settings/tabs/_PreferencesSettingsTab.scss rename to res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index b59b69e63b..f447221b7a 100644 --- a/res/css/views/settings/tabs/_PreferencesSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -14,11 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_PreferencesSettingsTab .mx_Field { +.mx_PreferencesUserSettingsTab .mx_Field { margin-right: 100px; // Align with the rest of the controls } -.mx_PreferencesSettingsTab .mx_Field input { +.mx_PreferencesUserSettingsTab .mx_Field input { display: block; // Subtract 10px padding on left and right diff --git a/res/css/views/settings/tabs/_SecuritySettingsTab.scss b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss similarity index 67% rename from res/css/views/settings/tabs/_SecuritySettingsTab.scss rename to res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss index ba357f16c3..4835640904 100644 --- a/res/css/views/settings/tabs/_SecuritySettingsTab.scss +++ b/res/css/views/settings/tabs/user/_SecurityUserSettingsTab.scss @@ -14,40 +14,40 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_SecuritySettingsTab .mx_DevicesPanel { +.mx_SecurityUserSettingsTab .mx_DevicesPanel { // Normally the panel is 880px, however this can easily overflow the container. // TODO: Fix the table to not be squishy width: auto; max-width: 880px; } -.mx_SecuritySettingsTab_deviceInfo { +.mx_SecurityUserSettingsTab_deviceInfo { display: table; padding-left: 0; } -.mx_SecuritySettingsTab_deviceInfo > li { +.mx_SecurityUserSettingsTab_deviceInfo > li { display: table-row; } -.mx_SecuritySettingsTab_deviceInfo > li > label, -.mx_SecuritySettingsTab_deviceInfo > li > span { +.mx_SecurityUserSettingsTab_deviceInfo > li > label, +.mx_SecurityUserSettingsTab_deviceInfo > li > span { display: table-cell; padding-right: 1em; } -.mx_SecuritySettingsTab_importExportButtons .mx_AccessibleButton { +.mx_SecurityUserSettingsTab_importExportButtons .mx_AccessibleButton { margin-right: 10px; } -.mx_SecuritySettingsTab_importExportButtons { +.mx_SecurityUserSettingsTab_importExportButtons { margin-bottom: 15px; } -.mx_SecuritySettingsTab_ignoredUser { +.mx_SecurityUserSettingsTab_ignoredUser { margin-bottom: 5px; } -.mx_SecuritySettingsTab_ignoredUser .mx_AccessibleButton { +.mx_SecurityUserSettingsTab_ignoredUser .mx_AccessibleButton { margin-right: 10px; } \ No newline at end of file diff --git a/res/css/views/settings/tabs/_VoiceSettingsTab.scss b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss similarity index 84% rename from res/css/views/settings/tabs/_VoiceSettingsTab.scss rename to res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss index 5ddd57b0e2..f5dba9831e 100644 --- a/res/css/views/settings/tabs/_VoiceSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_VoiceUserSettingsTab.scss @@ -14,15 +14,15 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_VoiceSettingsTab .mx_Field select { +.mx_VoiceUserSettingsTab .mx_Field select { width: 100%; max-width: 100%; } -.mx_VoiceSettingsTab .mx_Field { +.mx_VoiceUserSettingsTab .mx_Field { margin-right: 100px; // align with the rest of the fields } -.mx_VoiceSettingsTab_missingMediaPermissions { +.mx_VoiceUserSettingsTab_missingMediaPermissions { margin-bottom: 15px; } diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index c73edb179c..1f319176c2 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -18,10 +18,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Tab, TabbedView} from "../../structures/TabbedView"; import {_t, _td} from "../../../languageHandler"; -import AdvancedRoomSettingsTab from "../settings/tabs/AdvancedRoomSettingsTab"; -import RolesRoomSettingsTab from "../settings/tabs/RolesRoomSettingsTab"; -import GeneralRoomSettingsTab from "../settings/tabs/GeneralRoomSettingsTab"; -import SecurityRoomSettingsTab from "../settings/tabs/SecurityRoomSettingsTab"; +import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab"; +import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab"; +import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab"; +import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab"; import sdk from "../../../index"; export default class RoomSettingsDialog extends React.Component { diff --git a/src/components/views/dialogs/UserSettingsDialog.js b/src/components/views/dialogs/UserSettingsDialog.js index dc72acda12..1c4509eee5 100644 --- a/src/components/views/dialogs/UserSettingsDialog.js +++ b/src/components/views/dialogs/UserSettingsDialog.js @@ -18,15 +18,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import {Tab, TabbedView} from "../../structures/TabbedView"; import {_t, _td} from "../../../languageHandler"; -import GeneralUserSettingsTab from "../settings/tabs/GeneralUserSettingsTab"; +import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; import SettingsStore from "../../../settings/SettingsStore"; -import LabsSettingsTab from "../settings/tabs/LabsSettingsTab"; -import SecuritySettingsTab from "../settings/tabs/SecuritySettingsTab"; -import NotificationSettingsTab from "../settings/tabs/NotificationSettingsTab"; -import PreferencesSettingsTab from "../settings/tabs/PreferencesSettingsTab"; -import VoiceSettingsTab from "../settings/tabs/VoiceSettingsTab"; -import HelpSettingsTab from "../settings/tabs/HelpSettingsTab"; -import FlairSettingsTab from "../settings/tabs/FlairSettingsTab"; +import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; +import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; +import NotificationUserSettingsTab from "../settings/tabs/user/NotificationUserSettingsTab"; +import PreferencesUserSettingsTab from "../settings/tabs/user/PreferencesUserSettingsTab"; +import VoiceUserSettingsTab from "../settings/tabs/user/VoiceUserSettingsTab"; +import HelpUserSettingsTab from "../settings/tabs/user/HelpUserSettingsTab"; +import FlairUserSettingsTab from "../settings/tabs/user/FlairUserSettingsTab"; import sdk from "../../../index"; export default class UserSettingsDialog extends React.Component { @@ -45,39 +45,39 @@ export default class UserSettingsDialog extends React.Component { tabs.push(new Tab( _td("Flair"), "mx_UserSettingsDialog_flairIcon", - , + , )); tabs.push(new Tab( _td("Notifications"), "mx_UserSettingsDialog_bellIcon", - , + , )); tabs.push(new Tab( _td("Preferences"), "mx_UserSettingsDialog_preferencesIcon", - , + , )); tabs.push(new Tab( _td("Voice & Video"), "mx_UserSettingsDialog_voiceIcon", - , + , )); tabs.push(new Tab( _td("Security & Privacy"), "mx_UserSettingsDialog_securityIcon", - , + , )); if (SettingsStore.getLabsFeatures().length > 0) { tabs.push(new Tab( _td("Labs"), "mx_UserSettingsDialog_labsIcon", - , + , )); } tabs.push(new Tab( _td("Help & About"), "mx_UserSettingsDialog_helpIcon", - , + , )); return tabs; diff --git a/src/components/views/settings/tabs/AdvancedRoomSettingsTab.js b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js similarity index 93% rename from src/components/views/settings/tabs/AdvancedRoomSettingsTab.js rename to src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js index 9b99622516..3c6a7addc3 100644 --- a/src/components/views/settings/tabs/AdvancedRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/AdvancedRoomSettingsTab.js @@ -16,11 +16,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from "../../../../languageHandler"; -import MatrixClientPeg from "../../../../MatrixClientPeg"; -import sdk from "../../../../index"; -import AccessibleButton from "../../elements/AccessibleButton"; -import Modal from "../../../../Modal"; +import {_t} from "../../../../../languageHandler"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; +import sdk from "../../../../.."; +import AccessibleButton from "../../../elements/AccessibleButton"; +import Modal from "../../../../../Modal"; export default class AdvancedRoomSettingsTab extends React.Component { static propTypes = { diff --git a/src/components/views/settings/tabs/GeneralRoomSettingsTab.js b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js similarity index 92% rename from src/components/views/settings/tabs/GeneralRoomSettingsTab.js rename to src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js index f43fc8a682..5d707fcf16 100644 --- a/src/components/views/settings/tabs/GeneralRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/GeneralRoomSettingsTab.js @@ -16,14 +16,14 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from "../../../../languageHandler"; -import RoomProfileSettings from "../../room_settings/RoomProfileSettings"; -import MatrixClientPeg from "../../../../MatrixClientPeg"; -import sdk from "../../../../index"; -import AccessibleButton from "../../elements/AccessibleButton"; +import {_t} from "../../../../../languageHandler"; +import RoomProfileSettings from "../../../room_settings/RoomProfileSettings"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; +import sdk from "../../../../.."; +import AccessibleButton from "../../../elements/AccessibleButton"; import {MatrixClient} from "matrix-js-sdk"; -import dis from "../../../../dispatcher"; -import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; +import dis from "../../../../../dispatcher"; +import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; export default class GeneralRoomSettingsTab extends React.Component { static childContextTypes = { diff --git a/src/components/views/settings/tabs/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js similarity index 98% rename from src/components/views/settings/tabs/RolesRoomSettingsTab.js rename to src/components/views/settings/tabs/room/RolesRoomSettingsTab.js index d223e8f2e9..a6dac5a147 100644 --- a/src/components/views/settings/tabs/RolesRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js @@ -16,11 +16,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t, _td} from "../../../../languageHandler"; -import MatrixClientPeg from "../../../../MatrixClientPeg"; -import sdk from "../../../../index"; -import AccessibleButton from "../../elements/AccessibleButton"; -import Modal from "../../../../Modal"; +import {_t, _td} from "../../../../../languageHandler"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; +import sdk from "../../../../.."; +import AccessibleButton from "../../../elements/AccessibleButton"; +import Modal from "../../../../../Modal"; const plEventsToLabels = { // These will be translated for us later. diff --git a/src/components/views/settings/tabs/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js similarity index 96% rename from src/components/views/settings/tabs/SecurityRoomSettingsTab.js rename to src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js index 698f67dd18..a6eca3bf19 100644 --- a/src/components/views/settings/tabs/SecurityRoomSettingsTab.js +++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js @@ -16,11 +16,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t} from "../../../../languageHandler"; -import MatrixClientPeg from "../../../../MatrixClientPeg"; -import sdk from "../../../../index"; -import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; -import {SettingLevel} from "../../../../settings/SettingsStore"; +import {_t} from "../../../../../languageHandler"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; +import sdk from "../../../../.."; +import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; +import {SettingLevel} from "../../../../../settings/SettingsStore"; export default class SecurityRoomSettingsTab extends React.Component { static propTypes = { @@ -188,7 +188,7 @@ export default class SecurityRoomSettingsTab extends React.Component { if (joinRule !== 'public' && guestAccess === 'forbidden') { guestWarning = (
- + {_t("Guests cannot join this room even if explicitly invited.")}  {_t("Click here to fix")} @@ -201,7 +201,7 @@ export default class SecurityRoomSettingsTab extends React.Component { if (joinRule === 'public' && !hasAliases) { aliasWarning = (
- + {_t("To link to this room, please add an alias.")} diff --git a/src/components/views/settings/tabs/FlairSettingsTab.js b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js similarity index 84% rename from src/components/views/settings/tabs/FlairSettingsTab.js rename to src/components/views/settings/tabs/user/FlairUserSettingsTab.js index db513a161a..0daa20b8b3 100644 --- a/src/components/views/settings/tabs/FlairSettingsTab.js +++ b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js @@ -15,14 +15,14 @@ limitations under the License. */ import React from 'react'; -import {_t} from "../../../../languageHandler"; +import {_t} from "../../../../../languageHandler"; import {DragDropContext} from "react-beautiful-dnd"; -import GroupUserSettings from "../../groups/GroupUserSettings"; -import MatrixClientPeg from "../../../../MatrixClientPeg"; +import GroupUserSettings from "../../../groups/GroupUserSettings"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; import PropTypes from "prop-types"; import {MatrixClient} from "matrix-js-sdk"; -export default class FlairSettingsTab extends React.Component { +export default class FlairUserSettingsTab extends React.Component { static childContextTypes = { matrixClient: PropTypes.instanceOf(MatrixClient), }; diff --git a/src/components/views/settings/tabs/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js similarity index 88% rename from src/components/views/settings/tabs/GeneralUserSettingsTab.js rename to src/components/views/settings/tabs/user/GeneralUserSettingsTab.js index fd3274c9e0..093160e330 100644 --- a/src/components/views/settings/tabs/GeneralUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/GeneralUserSettingsTab.js @@ -15,21 +15,21 @@ limitations under the License. */ import React from 'react'; -import {_t} from "../../../../languageHandler"; -import ProfileSettings from "../ProfileSettings"; -import EmailAddresses from "../EmailAddresses"; -import PhoneNumbers from "../PhoneNumbers"; -import Field from "../../elements/Field"; -import * as languageHandler from "../../../../languageHandler"; -import {SettingLevel} from "../../../../settings/SettingsStore"; -import SettingsStore from "../../../../settings/SettingsStore"; -import LanguageDropdown from "../../elements/LanguageDropdown"; -import AccessibleButton from "../../elements/AccessibleButton"; -import DeactivateAccountDialog from "../../dialogs/DeactivateAccountDialog"; -const PlatformPeg = require("../../../../PlatformPeg"); -const sdk = require('../../../../index'); -const Modal = require("../../../../Modal"); -const dis = require("../../../../dispatcher"); +import {_t} from "../../../../../languageHandler"; +import ProfileSettings from "../../ProfileSettings"; +import EmailAddresses from "../../EmailAddresses"; +import PhoneNumbers from "../../PhoneNumbers"; +import Field from "../../../elements/Field"; +import * as languageHandler from "../../../../../languageHandler"; +import {SettingLevel} from "../../../../../settings/SettingsStore"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import LanguageDropdown from "../../../elements/LanguageDropdown"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import DeactivateAccountDialog from "../../../dialogs/DeactivateAccountDialog"; +const PlatformPeg = require("../../../../../PlatformPeg"); +const sdk = require('../../../../..'); +const Modal = require("../../../../../Modal"); +const dis = require("../../../../../dispatcher"); export default class GeneralUserSettingsTab extends React.Component { constructor() { diff --git a/src/components/views/settings/tabs/HelpSettingsTab.js b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js similarity index 90% rename from src/components/views/settings/tabs/HelpSettingsTab.js rename to src/components/views/settings/tabs/user/HelpUserSettingsTab.js index 4ad62451cb..d001a3f2e6 100644 --- a/src/components/views/settings/tabs/HelpSettingsTab.js +++ b/src/components/views/settings/tabs/user/HelpUserSettingsTab.js @@ -16,15 +16,15 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import {_t, getCurrentLanguage} from "../../../../languageHandler"; -import MatrixClientPeg from "../../../../MatrixClientPeg"; -import AccessibleButton from "../../elements/AccessibleButton"; -import SdkConfig from "../../../../SdkConfig"; -import createRoom from "../../../../createRoom"; -const packageJson = require('../../../../../package.json'); -const Modal = require("../../../../Modal"); -const sdk = require("../../../../index"); -const PlatformPeg = require("../../../../PlatformPeg"); +import {_t, getCurrentLanguage} from "../../../../../languageHandler"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import SdkConfig from "../../../../../SdkConfig"; +import createRoom from "../../../../../createRoom"; +const packageJson = require('../../../../../../package.json'); +const Modal = require("../../../../../Modal"); +const sdk = require("../../../../.."); +const PlatformPeg = require("../../../../../PlatformPeg"); // if this looks like a release, use the 'version' from package.json; else use // the git sha. Prepend version with v, to look like riot-web version @@ -45,7 +45,7 @@ const ghVersionLabel = function(repo, token='') { return { token }; }; -export default class HelpSettingsTab extends React.Component { +export default class HelpUserSettingsTab extends React.Component { static propTypes = { closeSettingsFn: PropTypes.func.isRequired, }; @@ -117,7 +117,7 @@ export default class HelpSettingsTab extends React.Component { } return ( -
+
{_t("Legal")}
{legalLinks} @@ -190,7 +190,7 @@ export default class HelpSettingsTab extends React.Component { } return ( -
+
{_t("Help & About")}
{_t('Bug reporting')} @@ -203,12 +203,12 @@ export default class HelpSettingsTab extends React.Component { "other users. They do not contain messages.", ) } -
+
{_t("Submit debug logs")}
-
+
{_t("Clear Cache and Reload")} @@ -221,7 +221,7 @@ export default class HelpSettingsTab extends React.Component { {faqText}
-
+
{_t("Versions")}
{_t("matrix-react-sdk version:")} {reactSdkVersion}
@@ -232,7 +232,7 @@ export default class HelpSettingsTab extends React.Component {
{this._renderLegal()} {this._renderCredits()} -
+
{_t("Advanced")}
{_t("Homeserver is")} {MatrixClientPeg.get().getHomeserverUrl()}
diff --git a/src/components/views/settings/tabs/LabsSettingsTab.js b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js similarity index 85% rename from src/components/views/settings/tabs/LabsSettingsTab.js rename to src/components/views/settings/tabs/user/LabsUserSettingsTab.js index e06f87460b..c2e62044a3 100644 --- a/src/components/views/settings/tabs/LabsSettingsTab.js +++ b/src/components/views/settings/tabs/user/LabsUserSettingsTab.js @@ -15,11 +15,11 @@ limitations under the License. */ import React from 'react'; -import {_t} from "../../../../languageHandler"; +import {_t} from "../../../../../languageHandler"; import PropTypes from "prop-types"; -import SettingsStore, {SettingLevel} from "../../../../settings/SettingsStore"; -import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; -const sdk = require("../../../../index"); +import SettingsStore, {SettingLevel} from "../../../../../settings/SettingsStore"; +import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; +const sdk = require("../../../../.."); export class LabsSettingToggle extends React.Component { 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() { super(); } diff --git a/src/components/views/settings/tabs/NotificationSettingsTab.js b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js similarity index 80% rename from src/components/views/settings/tabs/NotificationSettingsTab.js rename to src/components/views/settings/tabs/user/NotificationUserSettingsTab.js index 42d495f6ec..970659af6e 100644 --- a/src/components/views/settings/tabs/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/user/NotificationUserSettingsTab.js @@ -15,10 +15,10 @@ limitations under the License. */ import React from 'react'; -import {_t} from "../../../../languageHandler"; -const sdk = require("../../../../index"); +import {_t} from "../../../../../languageHandler"; +const sdk = require("../../../../.."); -export default class NotificationSettingsTab extends React.Component { +export default class NotificationUserSettingsTab extends React.Component { constructor() { super(); } @@ -26,7 +26,7 @@ export default class NotificationSettingsTab extends React.Component { render() { const Notifications = sdk.getComponent("views.settings.Notifications"); return ( -
+
{_t("Notifications")}
diff --git a/src/components/views/settings/tabs/PreferencesSettingsTab.js b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js similarity index 81% rename from src/components/views/settings/tabs/PreferencesSettingsTab.js rename to src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js index d76dc8f3dd..0b472cc366 100644 --- a/src/components/views/settings/tabs/PreferencesSettingsTab.js +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.js @@ -15,15 +15,15 @@ limitations under the License. */ import React from 'react'; -import {_t} from "../../../../languageHandler"; -import {SettingLevel} from "../../../../settings/SettingsStore"; -import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch"; -import SettingsStore from "../../../../settings/SettingsStore"; -import Field from "../../elements/Field"; -const sdk = require("../../../../index"); -const PlatformPeg = require("../../../../PlatformPeg"); +import {_t} from "../../../../../languageHandler"; +import {SettingLevel} from "../../../../../settings/SettingsStore"; +import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch"; +import SettingsStore from "../../../../../settings/SettingsStore"; +import Field from "../../../elements/Field"; +const sdk = require("../../../../.."); +const PlatformPeg = require("../../../../../PlatformPeg"); -export default class PreferencesSettingsTab extends React.Component { +export default class PreferencesUserSettingsTab extends React.Component { static COMPOSER_SETTINGS = [ 'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.suggestEmoji', @@ -95,17 +95,17 @@ export default class PreferencesSettingsTab extends React.Component { } return ( -
+
{_t("Preferences")}
{_t("Composer")} - {this._renderGroup(PreferencesSettingsTab.COMPOSER_SETTINGS)} + {this._renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)} {_t("Timeline")} - {this._renderGroup(PreferencesSettingsTab.TIMELINE_SETTINGS)} + {this._renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)} {_t("Advanced")} - {this._renderGroup(PreferencesSettingsTab.ADVANCED_SETTINGS)} + {this._renderGroup(PreferencesUserSettingsTab.ADVANCED_SETTINGS)} {autoLaunchOption} +
{_t('Unignore')} @@ -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() { super(); @@ -68,14 +68,14 @@ export default class SecuritySettingsTab extends React.Component { _onExportE2eKeysClicked = () => { Modal.createTrackedDialogAsync('Export E2E Keys', '', - import('../../../../async-components/views/dialogs/ExportE2eKeysDialog'), + import('../../../../../async-components/views/dialogs/ExportE2eKeysDialog'), {matrixClient: MatrixClientPeg.get()}, ); }; _onImportE2eKeysClicked = () => { Modal.createTrackedDialogAsync('Import E2E Keys', '', - import('../../../../async-components/views/dialogs/ImportE2eKeysDialog'), + import('../../../../../async-components/views/dialogs/ImportE2eKeysDialog'), {matrixClient: MatrixClientPeg.get()}, ); }; @@ -126,7 +126,7 @@ export default class SecuritySettingsTab extends React.Component { let importExportButtons = null; if (client.isCryptoEnabled()) { importExportButtons = ( -
+
{_t("Export E2E room keys")} @@ -140,7 +140,7 @@ export default class SecuritySettingsTab extends React.Component { return (
{_t("Cryptography")} -
    +
    • {deviceId} @@ -207,7 +207,7 @@ export default class SecuritySettingsTab extends React.Component { ); return ( -
      +
      {_t("Security & Privacy")}
      {_t("Devices")} diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js similarity index 90% rename from src/components/views/settings/tabs/VoiceSettingsTab.js rename to src/components/views/settings/tabs/user/VoiceUserSettingsTab.js index aefb114dd3..31791708e0 100644 --- a/src/components/views/settings/tabs/VoiceSettingsTab.js +++ b/src/components/views/settings/tabs/user/VoiceUserSettingsTab.js @@ -15,16 +15,16 @@ limitations under the License. */ import React from 'react'; -import {_t} from "../../../../languageHandler"; -import CallMediaHandler from "../../../../CallMediaHandler"; -import Field from "../../elements/Field"; -import AccessibleButton from "../../elements/AccessibleButton"; -import {SettingLevel} from "../../../../settings/SettingsStore"; -const Modal = require("../../../../Modal"); -const sdk = require("../../../../index"); -const MatrixClientPeg = require("../../../../MatrixClientPeg"); +import {_t} from "../../../../../languageHandler"; +import CallMediaHandler from "../../../../../CallMediaHandler"; +import Field from "../../../elements/Field"; +import AccessibleButton from "../../../elements/AccessibleButton"; +import {SettingLevel} from "../../../../../settings/SettingsStore"; +const Modal = require("../../../../../Modal"); +const sdk = require("../../../../.."); +const MatrixClientPeg = require("../../../../../MatrixClientPeg"); -export default class VoiceSettingsTab extends React.Component { +export default class VoiceUserSettingsTab extends React.Component { constructor() { super(); @@ -103,7 +103,7 @@ export default class VoiceSettingsTab extends React.Component { let webcamDropdown = null; if (this.state.mediaDevices === false) { requestButton = ( -
      +

      {_t("Missing media permissions, click the button below to request.")}

      {_t("Request media permissions")} @@ -166,7 +166,7 @@ export default class VoiceSettingsTab extends React.Component { } return ( -
      +
      {_t("Voice & Video")}
      {requestButton} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 84c9dacd07..5e4765b3af 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -500,19 +500,7 @@ "Upload profile picture": "Upload profile picture", "Display Name": "Display Name", "Save": "Save", - "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", - "Upgrade room to version %(ver)s": "Upgrade room to version %(ver)s", - "Room information": "Room information", - "Internal room ID:": "Internal room ID:", - "Room version": "Room version", - "Room version:": "Room version:", - "Developer options": "Developer options", - "Open Devtools": "Open Devtools", "Flair": "Flair", - "General": "General", - "Room Addresses": "Room Addresses", - "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", - "URL Previews": "URL Previews", "Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?", "Success": "Success", "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them": "Your password was successfully changed. You will not receive push notifications on other devices until you log back in to them", @@ -528,6 +516,7 @@ "Account management": "Account management", "Deactivating your account is a permanent action - be careful!": "Deactivating your account is a permanent action - be careful!", "Deactivate Account": "Deactivate Account", + "General": "General", "Legal": "Legal", "Credits": "Credits", "For help with using Riot, click here.": "For help with using Riot, click here.", @@ -555,6 +544,44 @@ "Composer": "Composer", "Timeline": "Timeline", "Autocomplete delay (ms)": "Autocomplete delay (ms)", + "Unignore": "Unignore", + "": "", + "Import E2E room keys": "Import E2E room keys", + "Cryptography": "Cryptography", + "Device ID:": "Device ID:", + "Device key:": "Device key:", + "Ignored users": "Ignored users", + "Bulk options": "Bulk options", + "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", + "Key backup": "Key backup", + "Security & Privacy": "Security & Privacy", + "Devices": "Devices", + "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", + "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", + "Learn more about how we use analytics.": "Learn more about how we use analytics.", + "No media permissions": "No media permissions", + "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", + "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", + "Request media permissions": "Request media permissions", + "No Audio Outputs detected": "No Audio Outputs detected", + "No Microphones detected": "No Microphones detected", + "No Webcams detected": "No Webcams detected", + "Default Device": "Default Device", + "Audio Output": "Audio Output", + "Microphone": "Microphone", + "Camera": "Camera", + "Voice & Video": "Voice & Video", + "This room is not accessible by remote Matrix servers": "This room is not accessible by remote Matrix servers", + "Upgrade room to version %(ver)s": "Upgrade room to version %(ver)s", + "Room information": "Room information", + "Internal room ID:": "Internal room ID:", + "Room version": "Room version", + "Room version:": "Room version:", + "Developer options": "Developer options", + "Open Devtools": "Open Devtools", + "Room Addresses": "Room Addresses", + "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", + "URL Previews": "URL Previews", "To change the room's avatar, you must be a": "To change the room's avatar, you must be a", "To change the room's name, you must be a": "To change the room's name, you must be a", "To change the room's main address, you must be a": "To change the room's main address, you must be a", @@ -592,38 +619,11 @@ "Members only (since the point in time of selecting this option)": "Members only (since the point in time of selecting this option)", "Members only (since they were invited)": "Members only (since they were invited)", "Members only (since they joined)": "Members only (since they joined)", - "Security & Privacy": "Security & Privacy", "Encryption": "Encryption", "Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.", "Encrypted": "Encrypted", "Who can access this room?": "Who can access this room?", "Who can read history?": "Who can read history?", - "Unignore": "Unignore", - "": "", - "Import E2E room keys": "Import E2E room keys", - "Cryptography": "Cryptography", - "Device ID:": "Device ID:", - "Device key:": "Device key:", - "Ignored users": "Ignored users", - "Bulk options": "Bulk options", - "Reject all %(invitedRooms)s invites": "Reject all %(invitedRooms)s invites", - "Key backup": "Key backup", - "Devices": "Devices", - "Riot collects anonymous analytics to allow us to improve the application.": "Riot collects anonymous analytics to allow us to improve the application.", - "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.": "Privacy is important to us, so we don't collect any personal or identifiable data for our analytics.", - "Learn more about how we use analytics.": "Learn more about how we use analytics.", - "No media permissions": "No media permissions", - "You may need to manually permit Riot to access your microphone/webcam": "You may need to manually permit Riot to access your microphone/webcam", - "Missing media permissions, click the button below to request.": "Missing media permissions, click the button below to request.", - "Request media permissions": "Request media permissions", - "No Audio Outputs detected": "No Audio Outputs detected", - "No Microphones detected": "No Microphones detected", - "No Webcams detected": "No Webcams detected", - "Default Device": "Default Device", - "Audio Output": "Audio Output", - "Microphone": "Microphone", - "Camera": "Camera", - "Voice & Video": "Voice & Video", "Cannot add any more widgets": "Cannot add any more widgets", "The maximum permitted number of widgets have already been added to this room.": "The maximum permitted number of widgets have already been added to this room.", "Add a widget": "Add a widget", From 014e4a2ccf2bf1010e694020563f323613600c6f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Feb 2019 11:31:17 -0700 Subject: [PATCH 07/63] Remove DragDropContext from FlairSettings This also fixes a technical bug where one could drag a community from the settings to the LLP --- .../views/groups/GroupPublicityToggle.js | 4 +- src/components/views/groups/GroupTile.js | 92 ++++++++++--------- .../tabs/user/FlairUserSettingsTab.js | 5 +- 3 files changed, 51 insertions(+), 50 deletions(-) diff --git a/src/components/views/groups/GroupPublicityToggle.js b/src/components/views/groups/GroupPublicityToggle.js index e27bf9a9d5..98fa598e20 100644 --- a/src/components/views/groups/GroupPublicityToggle.js +++ b/src/components/views/groups/GroupPublicityToggle.js @@ -68,7 +68,9 @@ export default React.createClass({ render() { const GroupTile = sdk.getComponent('groups.GroupTile'); return
      - + diff --git a/src/components/views/groups/GroupTile.js b/src/components/views/groups/GroupTile.js index 509c209baa..18ef5a5637 100644 --- a/src/components/views/groups/GroupTile.js +++ b/src/components/views/groups/GroupTile.js @@ -33,6 +33,7 @@ const GroupTile = React.createClass({ showDescription: PropTypes.bool, // Height of the group avatar in pixels avatarHeight: PropTypes.number, + draggable: PropTypes.bool, }, contextTypes: { @@ -49,6 +50,7 @@ const GroupTile = React.createClass({ return { showDescription: true, avatarHeight: 50, + draggable: true, }; }, @@ -78,54 +80,54 @@ const GroupTile = React.createClass({
      { profile.shortDescription }
      :
      ; const httpUrl = profile.avatarUrl ? this.context.matrixClient.mxcUrlToHttp( - profile.avatarUrl, avatarHeight, avatarHeight, "crop", - ) : null; + profile.avatarUrl, avatarHeight, avatarHeight, "crop") : null; + + let avatarElement = ( +
      + +
      + ); + if (this.props.draggable) { + const avatarClone = avatarElement; + avatarElement = ( + + { (droppableProvided, droppableSnapshot) => ( +
      + + { (provided, snapshot) => ( +
      +
      + {avatarClone} +
      + { /* Instead of a blank placeholder, use a copy of the avatar itself. */ } + { provided.placeholder ? avatarClone :
      } +
      + ) } + +
      + ) } + + ); + } + // 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 - - { (droppableProvided, droppableSnapshot) => ( -
      - - { (provided, snapshot) => ( -
      -
      -
      - -
      -
      - { /* Instead of a blank placeholder, use a copy of the avatar itself. */ } - { provided.placeholder ? -
      - -
      : -
      - } -
      - ) } - -
      - ) } - + { avatarElement }
      { name }
      { descElement } diff --git a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js index 0daa20b8b3..0063a9a981 100644 --- a/src/components/views/settings/tabs/user/FlairUserSettingsTab.js +++ b/src/components/views/settings/tabs/user/FlairUserSettingsTab.js @@ -16,7 +16,6 @@ limitations under the License. import React from 'react'; import {_t} from "../../../../../languageHandler"; -import {DragDropContext} from "react-beautiful-dnd"; import GroupUserSettings from "../../../groups/GroupUserSettings"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import PropTypes from "prop-types"; @@ -42,9 +41,7 @@ export default class FlairUserSettingsTab extends React.Component {
      {_t("Flair")}
      - - - +
      ); From 7ea4008daa733b45e39a5aec3dad2e2aa9ee857c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Feb 2019 16:33:20 -0700 Subject: [PATCH 08/63] Implement support for watching for changes in settings This implements a dream of one day being able to listen for changes in a settings to react to them, regardless of which device actually changed the setting. The use case for this kind of thing is extremely limited, but when it is needed it should be more than powerful enough. --- docs/settings.md | 33 +++++ src/MatrixClientPeg.js | 4 +- src/settings/SettingsStore.js | 117 ++++++++++++++++++ src/settings/WatchManager.js | 57 +++++++++ .../handlers/AccountSettingsHandler.js | 48 ++++++- .../handlers/ConfigSettingsHandler.js | 8 ++ .../handlers/DefaultSettingsHandler.js | 9 ++ .../handlers/DeviceSettingsHandler.js | 16 +++ src/settings/handlers/LocalEchoWrapper.js | 9 ++ .../MatrixClientBackedSettingsHandler.js | 48 +++++++ .../handlers/RoomAccountSettingsHandler.js | 51 +++++++- .../handlers/RoomDeviceSettingsHandler.js | 18 +++ src/settings/handlers/RoomSettingsHandler.js | 49 +++++++- src/settings/handlers/SettingsHandler.js | 24 ++++ 14 files changed, 484 insertions(+), 7 deletions(-) create mode 100644 src/settings/WatchManager.js create mode 100644 src/settings/handlers/MatrixClientBackedSettingsHandler.js diff --git a/docs/settings.md b/docs/settings.md index cdba01e04a..9762e7a73e 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -131,6 +131,32 @@ SettingsStore.getValue(...); // this will return the value set in `setValue` abo ``` +## Watching for changes + +Most use cases do not need to set up a watcher because they are able to react to changes as they are made, or the changes which are made are not significant enough for it to matter. Watchers are intended to be used in scenarios where it is important to react to changes made by other logged in devices. Typically, this would be done within the component itself, however the component should not be aware of the intricacies of setting inversion or remapping to particular data structures. Instead, a generic watcher interface is provided on `SettingsStore` to watch (and subsequently unwatch) for changes in a setting. + +An example of a watcher in action would be: + +```javascript +class MyComponent extends React.Component { + + settingWatcherRef = null; + + componentWillMount() { + this.settingWatcherRef = SettingsStore.watchSetting("roomColor", "!example:matrix.org", (settingName, roomId, level, newVal) => { + // Always re-read the setting value from the store to avoid reacting to changes which do not have a consequence. For example, the + // room color could have been changed at the device level, but an account override prevents that change from making a difference. + const actualVal = SettingsStore.getValue(settingName, "!example:matrix.org"); + if (actualVal !== this.state.color) this.setState({color: actualVal}); + }); + } + + componentWillUnmount() { + SettingsStore.unwatchSetting(this.settingWatcherRef); + } +} +``` + # Maintainers Reference @@ -159,3 +185,10 @@ Features automatically get considered as `disabled` if they are not listed in th ``` If `enableLabs` is true in the configuration, the default for features becomes `"labs"`. + +### Watchers + +Watchers can appear complicated under the hood: the request to watch a setting is actually forked off to individual handlers for watching. This means that the handlers need to track their changes and listen for remote changes where possible, but also makes it much easier for the `SettingsStore` to react to changes. The handler is going to know the best things to listen for (specific events, account data, etc) and thus it is left as a responsibility for the handler to track changes. + +In practice, handlers which rely on remote changes (account data, room events, etc) will always attach a listener to the `MatrixClient`. They then watch for changes to events they care about and send off appropriate updates to the generalized `WatchManager` - a class specifically designed to deduplicate the logic of managing watchers. The handlers which are localized to the local client (device) generally just trigger the `WatchManager` when they manipulate the setting themselves as there's nothing to really 'watch'. + \ No newline at end of file diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index e36034c69d..1cf29c3e82 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -30,6 +30,7 @@ import MatrixActionCreators from './actions/MatrixActionCreators'; import {phasedRollOutExpiredForUser} from "./PhasedRollOut"; import Modal from './Modal'; import {verificationMethods} from 'matrix-js-sdk/lib/crypto'; +import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler"; interface MatrixClientCreds { homeserverUrl: string, @@ -137,8 +138,9 @@ class MatrixClientPeg { opts.pendingEventOrdering = "detached"; opts.lazyLoadMembers = true; - // Connect the matrix client to the dispatcher + // Connect the matrix client to the dispatcher and setting handlers MatrixActionCreators.start(this.matrixClient); + MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient; console.log(`MatrixClientPeg: really starting MatrixClient`); await this.get().startClient(opts); diff --git a/src/settings/SettingsStore.js b/src/settings/SettingsStore.js index 1bdd72dc5f..1704ad9db2 100644 --- a/src/settings/SettingsStore.js +++ b/src/settings/SettingsStore.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,6 +24,7 @@ import RoomSettingsHandler from "./handlers/RoomSettingsHandler"; import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler"; import {_t} from '../languageHandler'; import SdkConfig from "../SdkConfig"; +import dis from '../dispatcher'; import {SETTINGS} from "./Settings"; import LocalEchoWrapper from "./handlers/LocalEchoWrapper"; @@ -98,6 +100,121 @@ const LEVEL_ORDER = [ * be enabled). */ export default class SettingsStore { + // We support watching settings for changes, and do so only at the levels which are + // relevant to the setting. We pass the watcher on to the handlers and aggregate it + // before sending it off to the caller. We need to track which callback functions we + // provide to the handlers though so we can unwatch it on demand. In practice, we + // return a "callback reference" to the caller which correlates to an entry in this + // dictionary for each handler's callback function. + // + // We also maintain a list of monitors which are special watchers: they cause dispatches + // when the setting changes. We track which rooms we're monitoring though to ensure we + // don't duplicate updates on the bus. + static _watchers = {}; // { callbackRef => { level => callbackFn } } + static _monitors = {}; // { settingName => { roomId => callbackRef } } + + /** + * Watches for changes in a particular setting. This is done without any local echo + * wrapping and fires whenever a change is detected in a setting's value. Watching + * is intended to be used in scenarios where the app needs to react to changes made + * by other devices. It is otherwise expected that callers will be able to use the + * Controller system or track their own changes to settings. Callers should retain + * @param {string} settingName The setting name to watch + * @param {String} roomId The room ID to watch for changes in. May be null for 'all'. + * @param {function} callbackFn A function to be called when a setting change is + * detected. Four arguments can be expected: the setting name, the room ID (may be null), + * the level the change happened at, and finally the new value for those arguments. The + * callback may need to do a call to #getValue() to see if a consequential change has + * occurred. + * @returns {string} A reference to the watcher that was employed. + */ + static watchSetting(settingName, roomId, callbackFn) { + const setting = SETTINGS[settingName]; + const originalSettingName = settingName; + if (!setting) throw new Error(`${settingName} is not a setting`); + + if (setting.invertedSettingName) { + settingName = setting.invertedSettingName; + } + + const watcherId = `${new Date().getTime()}_${settingName}_${roomId}`; + SettingsStore._watchers[watcherId] = {}; + + const levels = Object.keys(LEVEL_HANDLERS); + for (const level of levels) { + const handler = SettingsStore._getHandler(originalSettingName, level); + if (!handler) continue; + + const localizedCallback = (changedInRoomId, newVal) => { + callbackFn(originalSettingName, changedInRoomId, level, newVal); + }; + + console.log(`Starting watcher for ${settingName}@${roomId || ''} at level ${level}`); + SettingsStore._watchers[watcherId][level] = localizedCallback; + handler.watchSetting(settingName, roomId, localizedCallback); + } + + return watcherId; + } + + /** + * Stops the SettingsStore from watching a setting. This is a no-op if the watcher + * provided is not found. + * @param {string} watcherReference The watcher reference (received from #watchSetting) + * to cancel. + */ + static unwatchSetting(watcherReference) { + if (!SettingsStore._watchers[watcherReference]) return; + + for (const handlerName of Object.keys(SettingsStore._watchers[watcherReference])) { + const handler = LEVEL_HANDLERS[handlerName]; + if (!handler) continue; + handler.unwatchSetting(SettingsStore._watchers[watcherReference][handlerName]); + } + + delete SettingsStore._watchers[watcherReference]; + } + + /** + * Sets up a monitor for a setting. This behaves similar to #watchSetting except instead + * of making a call to a callback, it forwards all changes to the dispatcher. Callers can + * expect to listen for the 'setting_updated' action with an object containing settingName, + * roomId, level, and newValue. + * @param {string} settingName The setting name to monitor. + * @param {String} roomId The room ID to monitor for changes in. Use null for all rooms. + */ + static monitorSetting(settingName, roomId) { + if (!this._monitors[settingName]) this._monitors[settingName] = {}; + + const registerWatcher = () => { + this._monitors[settingName][roomId] = SettingsStore.watchSetting( + settingName, roomId, (settingName, inRoomId, level, newValue) => { + dis.dispatch({ + action: 'setting_updated', + settingName, + roomId: inRoomId, + level, + newValue, + }); + }, + ); + }; + + const hasRoom = Object.keys(this._monitors[settingName]).find((r) => r === roomId || r === null); + if (!hasRoom) { + registerWatcher(); + } else { + if (roomId === null) { + // Unregister all existing watchers and register the new one + for (const roomId of Object.keys(this._monitors[settingName])) { + SettingsStore.unwatchSetting(this._monitors[settingName][roomId]); + } + this._monitors[settingName] = {}; + registerWatcher(); + } // else a watcher is already registered for the room, so don't bother registering it again + } + } + /** * Gets the translated display name for a given setting * @param {string} settingName The setting to look up. diff --git a/src/settings/WatchManager.js b/src/settings/WatchManager.js new file mode 100644 index 0000000000..0561529392 --- /dev/null +++ b/src/settings/WatchManager.js @@ -0,0 +1,57 @@ +/* +Copyright 2019 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Generalized management class for dealing with watchers on a per-handler (per-level) + * basis without duplicating code. Handlers are expected to push updates through this + * class, which are then proxied outwards to any applicable watchers. + */ +export class WatchManager { + _watchers = {}; // { settingName: { roomId: callbackFns[] } } + + // Proxy for handlers to delegate changes to this manager + watchSetting(settingName, roomId, cb) { + if (!this._watchers[settingName]) this._watchers[settingName] = {}; + if (!this._watchers[settingName][roomId]) this._watchers[settingName][roomId] = []; + this._watchers[settingName][roomId].push(cb); + } + + // Proxy for handlers to delegate changes to this manager + unwatchSetting(cb) { + for (const settingName of Object.keys(this._watchers)) { + for (const roomId of Object.keys(this._watchers[settingName])) { + let idx; + while ((idx = this._watchers[settingName][roomId].indexOf(cb)) !== -1) { + this._watchers[settingName][roomId].splice(idx, 1); + } + } + } + } + + notifyUpdate(settingName, inRoomId, newValue) { + if (!this._watchers[settingName]) return; + + const roomWatchers = this._watchers[settingName]; + const callbacks = []; + + if (inRoomId !== null && roomWatchers[inRoomId]) callbacks.push(...roomWatchers[inRoomId]); + if (roomWatchers[null]) callbacks.push(...roomWatchers[null]); + + for (const callback of callbacks) { + callback(inRoomId, newValue); + } + } +} diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index b822709573..4bef585e6b 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,14 +15,49 @@ See the License for the specific language governing permissions and limitations under the License. */ -import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from '../../MatrixClientPeg'; +import {WatchManager} from "../WatchManager"; +import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; /** * Gets and sets settings at the "account" level for the current user. * This handler does not make use of the roomId parameter. */ -export default class AccountSettingHandler extends SettingsHandler { +export default class AccountSettingsHandler extends MatrixClientBackedSettingsHandler { + constructor() { + super(); + + this._watchers = new WatchManager(); + this._onAccountData = this._onAccountData.bind(this); + } + + initMatrixClient(oldClient, newClient) { + if (oldClient) { + oldClient.removeListener("accountData", this._onAccountData); + } + + newClient.on("accountData", this._onAccountData); + } + + _onAccountData(event) { + if (event.getType() === "org.matrix.preview_urls") { + let val = event.getContent()['disable']; + if (typeof(val) !== "boolean") { + val = null; + } else { + val = !val; + } + + this._watchers.notifyUpdate("urlPreviewsEnabled", null, val); + } else if (event.getType() === "im.vector.web.settings") { + // We can't really discern what changed, so trigger updates for everything + for (const settingName of Object.keys(event.getContent())) { + console.log(settingName); + this._watchers.notifyUpdate(settingName, null, event.getContent()[settingName]); + } + } + } + getValue(settingName, roomId) { // Special case URL previews if (settingName === "urlPreviewsEnabled") { @@ -67,6 +103,14 @@ export default class AccountSettingHandler extends SettingsHandler { return cli !== undefined && cli !== null; } + watchSetting(settingName, roomId, cb) { + this._watchers.watchSetting(settingName, roomId, cb); + } + + unwatchSetting(cb) { + this._watchers.unwatchSetting(cb); + } + _getSettings(eventType = "im.vector.web.settings") { const cli = MatrixClientPeg.get(); if (!cli) return null; diff --git a/src/settings/handlers/ConfigSettingsHandler.js b/src/settings/handlers/ConfigSettingsHandler.js index a54ad1cef6..095347a542 100644 --- a/src/settings/handlers/ConfigSettingsHandler.js +++ b/src/settings/handlers/ConfigSettingsHandler.js @@ -47,4 +47,12 @@ export default class ConfigSettingsHandler extends SettingsHandler { isSupported() { return true; // SdkConfig is always there } + + watchSetting(settingName, roomId, cb) { + // no-op: no changes possible + } + + unwatchSetting(cb) { + // no-op: no changes possible + } } diff --git a/src/settings/handlers/DefaultSettingsHandler.js b/src/settings/handlers/DefaultSettingsHandler.js index 11e8b729bc..83824850b3 100644 --- a/src/settings/handlers/DefaultSettingsHandler.js +++ b/src/settings/handlers/DefaultSettingsHandler.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -51,4 +52,12 @@ export default class DefaultSettingsHandler extends SettingsHandler { isSupported() { return true; } + + watchSetting(settingName, roomId, cb) { + // no-op: no changes possible + } + + unwatchSetting(cb) { + // no-op: no changes possible + } } diff --git a/src/settings/handlers/DeviceSettingsHandler.js b/src/settings/handlers/DeviceSettingsHandler.js index b2a225e190..457fb888e9 100644 --- a/src/settings/handlers/DeviceSettingsHandler.js +++ b/src/settings/handlers/DeviceSettingsHandler.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,6 +18,7 @@ limitations under the License. import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from "../../MatrixClientPeg"; +import {WatchManager} from "../WatchManager"; /** * Gets and sets settings at the "device" level for the current device. @@ -31,6 +33,7 @@ export default class DeviceSettingsHandler extends SettingsHandler { constructor(featureNames) { super(); this._featureNames = featureNames; + this._watchers = new WatchManager(); } getValue(settingName, roomId) { @@ -66,18 +69,22 @@ export default class DeviceSettingsHandler extends SettingsHandler { // Special case notifications if (settingName === "notificationsEnabled") { localStorage.setItem("notifications_enabled", newValue); + this._watchers.notifyUpdate(settingName, null, newValue); return Promise.resolve(); } else if (settingName === "notificationBodyEnabled") { localStorage.setItem("notifications_body_enabled", newValue); + this._watchers.notifyUpdate(settingName, null, newValue); return Promise.resolve(); } else if (settingName === "audioNotificationsEnabled") { localStorage.setItem("audio_notifications_enabled", newValue); + this._watchers.notifyUpdate(settingName, null, newValue); return Promise.resolve(); } const settings = this._getSettings() || {}; settings[settingName] = newValue; localStorage.setItem("mx_local_settings", JSON.stringify(settings)); + this._watchers.notifyUpdate(settingName, null, newValue); return Promise.resolve(); } @@ -90,6 +97,14 @@ export default class DeviceSettingsHandler extends SettingsHandler { return localStorage !== undefined && localStorage !== null; } + watchSetting(settingName, roomId, cb) { + this._watchers.watchSetting(settingName, roomId, cb); + } + + unwatchSetting(cb) { + this._watchers.unwatchSetting(cb); + } + _getSettings() { const value = localStorage.getItem("mx_local_settings"); if (!value) return null; @@ -111,5 +126,6 @@ export default class DeviceSettingsHandler extends SettingsHandler { _writeFeature(featureName, enabled) { localStorage.setItem("mx_labs_feature_" + featureName, enabled); + this._watchers.notifyUpdate(featureName, null, enabled); } } diff --git a/src/settings/handlers/LocalEchoWrapper.js b/src/settings/handlers/LocalEchoWrapper.js index d616edd9fb..3b1200f0b7 100644 --- a/src/settings/handlers/LocalEchoWrapper.js +++ b/src/settings/handlers/LocalEchoWrapper.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -66,4 +67,12 @@ export default class LocalEchoWrapper extends SettingsHandler { isSupported() { return this._handler.isSupported(); } + + watchSetting(settingName, roomId, cb) { + this._handler.watchSetting(settingName, roomId, cb); + } + + unwatchSetting(cb) { + this._handler.unwatchSetting(cb); + } } diff --git a/src/settings/handlers/MatrixClientBackedSettingsHandler.js b/src/settings/handlers/MatrixClientBackedSettingsHandler.js new file mode 100644 index 0000000000..effe7ae9a7 --- /dev/null +++ b/src/settings/handlers/MatrixClientBackedSettingsHandler.js @@ -0,0 +1,48 @@ +/* +Copyright 2019 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import SettingsHandler from "./SettingsHandler"; + +// Dev note: This whole class exists in the event someone logs out and back in - we want +// to make sure the right MatrixClient is listening for changes. + +/** + * Represents the base class for settings handlers which need access to a MatrixClient. + * This class performs no logic and should be overridden. + */ +export default class MatrixClientBackedSettingsHandler extends SettingsHandler { + static _matrixClient; + static _instances = []; + + static set matrixClient(client) { + const oldClient = MatrixClientBackedSettingsHandler._matrixClient; + MatrixClientBackedSettingsHandler._matrixClient = client; + + for (const instance of MatrixClientBackedSettingsHandler._instances) { + instance.initMatrixClient(oldClient, client); + } + } + + constructor() { + super(); + + MatrixClientBackedSettingsHandler._instances.push(this); + } + + initMatrixClient() { + console.warn("initMatrixClient not overridden"); + } +} diff --git a/src/settings/handlers/RoomAccountSettingsHandler.js b/src/settings/handlers/RoomAccountSettingsHandler.js index d0dadc2de7..448b42f61e 100644 --- a/src/settings/handlers/RoomAccountSettingsHandler.js +++ b/src/settings/handlers/RoomAccountSettingsHandler.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,13 +15,51 @@ See the License for the specific language governing permissions and limitations under the License. */ -import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from '../../MatrixClientPeg'; +import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; +import {WatchManager} from "../WatchManager"; /** * Gets and sets settings at the "room-account" level for the current user. */ -export default class RoomAccountSettingsHandler extends SettingsHandler { +export default class RoomAccountSettingsHandler extends MatrixClientBackedSettingsHandler { + constructor() { + super(); + + this._watchers = new WatchManager(); + this._onAccountData = this._onAccountData.bind(this); + } + + initMatrixClient(oldClient, newClient) { + if (oldClient) { + oldClient.removeListener("Room.accountData", this._onAccountData); + } + + newClient.on("Room.accountData", this._onAccountData); + } + + _onAccountData(event, room) { + const roomId = room.roomId; + + if (event.getType() === "org.matrix.room.preview_urls") { + let val = event.getContent()['disable']; + if (typeof (val) !== "boolean") { + val = null; + } else { + val = !val; + } + + this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, val); + } else if (event.getType() === "org.matrix.room.color_scheme") { + this._watchers.notifyUpdate("roomColor", roomId, event.getContent()); + } else if (event.getType() === "im.vector.web.settings") { + // We can't really discern what changed, so trigger updates for everything + for (const settingName of Object.keys(event.getContent())) { + this._watchers.notifyUpdate(settingName, roomId, event.getContent()[settingName]); + } + } + } + getValue(settingName, roomId) { // Special case URL previews if (settingName === "urlPreviewsEnabled") { @@ -74,6 +113,14 @@ export default class RoomAccountSettingsHandler extends SettingsHandler { return cli !== undefined && cli !== null; } + watchSetting(settingName, roomId, cb) { + this._watchers.watchSetting(settingName, roomId, cb); + } + + unwatchSetting(cb) { + this._watchers.unwatchSetting(cb); + } + _getSettings(roomId, eventType = "im.vector.web.settings") { const room = MatrixClientPeg.get().getRoom(roomId); if (!room) return null; diff --git a/src/settings/handlers/RoomDeviceSettingsHandler.js b/src/settings/handlers/RoomDeviceSettingsHandler.js index 186be3041f..710c5e6255 100644 --- a/src/settings/handlers/RoomDeviceSettingsHandler.js +++ b/src/settings/handlers/RoomDeviceSettingsHandler.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,12 +17,19 @@ limitations under the License. import Promise from 'bluebird'; import SettingsHandler from "./SettingsHandler"; +import {WatchManager} from "../WatchManager"; /** * Gets and sets settings at the "room-device" level for the current device in a particular * room. */ export default class RoomDeviceSettingsHandler extends SettingsHandler { + constructor() { + super(); + + this._watchers = new WatchManager(); + } + getValue(settingName, roomId) { // Special case blacklist setting to use legacy values if (settingName === "blacklistUnverifiedDevices") { @@ -44,6 +52,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { if (!value["blacklistUnverifiedDevicesPerRoom"]) value["blacklistUnverifiedDevicesPerRoom"] = {}; value["blacklistUnverifiedDevicesPerRoom"][roomId] = newValue; localStorage.setItem("mx_local_settings", JSON.stringify(value)); + this._watchers.notifyUpdate(settingName, roomId, newValue); return Promise.resolve(); } @@ -54,6 +63,7 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { localStorage.setItem(this._getKey(settingName, roomId), newValue); } + this._watchers.notifyUpdate(settingName, roomId, newValue); return Promise.resolve(); } @@ -65,6 +75,14 @@ export default class RoomDeviceSettingsHandler extends SettingsHandler { return localStorage !== undefined && localStorage !== null; } + watchSetting(settingName, roomId, cb) { + this._watchers.watchSetting(settingName, roomId, cb); + } + + unwatchSetting(cb) { + this._watchers.unwatchSetting(cb); + } + _read(key) { const rawValue = localStorage.getItem(key); if (!rawValue) return null; diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 71abff94f6..1622b44dd0 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,13 +15,49 @@ See the License for the specific language governing permissions and limitations under the License. */ -import SettingsHandler from "./SettingsHandler"; import MatrixClientPeg from '../../MatrixClientPeg'; +import MatrixClientBackedSettingsHandler from "./MatrixClientBackedSettingsHandler"; +import {WatchManager} from "../WatchManager"; /** * Gets and sets settings at the "room" level. */ -export default class RoomSettingsHandler extends SettingsHandler { +export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandler { + constructor() { + super(); + + this._watchers = new WatchManager(); + this._onEvent = this._onEvent.bind(this); + } + + initMatrixClient(oldClient, newClient) { + if (oldClient) { + oldClient.removeListener("RoomState.events", this._onEvent); + } + + newClient.on("RoomState.events", this._onEvent); + } + + _onEvent(event) { + const roomId = event.getRoomId(); + + if (event.getType() === "org.matrix.room.preview_urls") { + let val = event.getContent()['disable']; + if (typeof (val) !== "boolean") { + val = null; + } else { + val = !val; + } + + this._watchers.notifyUpdate("urlPreviewsEnabled", roomId, val); + } else if (event.getType() === "im.vector.web.settings") { + // We can't really discern what changed, so trigger updates for everything + for (const settingName of Object.keys(event.getContent())) { + this._watchers.notifyUpdate(settingName, roomId, event.getContent()[settingName]); + } + } + } + getValue(settingName, roomId) { // Special case URL previews if (settingName === "urlPreviewsEnabled") { @@ -64,6 +101,14 @@ export default class RoomSettingsHandler extends SettingsHandler { return cli !== undefined && cli !== null; } + watchSetting(settingName, roomId, cb) { + this._watchers.watchSetting(settingName, roomId, cb); + } + + unwatchSetting(cb) { + this._watchers.unwatchSetting(cb); + } + _getSettings(roomId, eventType = "im.vector.web.settings") { const room = MatrixClientPeg.get().getRoom(roomId); if (!room) return null; diff --git a/src/settings/handlers/SettingsHandler.js b/src/settings/handlers/SettingsHandler.js index 69f633c650..0a704d5be7 100644 --- a/src/settings/handlers/SettingsHandler.js +++ b/src/settings/handlers/SettingsHandler.js @@ -1,5 +1,6 @@ /* Copyright 2017 Travis Ralston +Copyright 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -68,4 +69,27 @@ export default class SettingsHandler { isSupported() { return false; } + + /** + * Watches for a setting change within this handler. The caller should preserve + * a reference to the callback so that it may be unwatched. The caller should + * additionally provide a unique callback for multiple watchers on the same setting. + * @param {string} settingName The setting name to watch for changes in. + * @param {String} roomId The room ID to watch for changes in. + * @param {function} cb A function taking two arguments: the room ID the setting changed + * in and the new value for the setting at this level in the given room. + */ + watchSetting(settingName, roomId, cb) { + throw new Error("Invalid operation: watchSetting was not overridden"); + } + + /** + * Unwatches a previously watched setting. If the callback is not associated with + * a watcher, this is a no-op. + * @param {function} cb A callback function previously supplied to watchSetting + * which should no longer be used. + */ + unwatchSetting(cb) { + throw new Error("Invalid operation: unwatchSetting was not overridden"); + } } From b0cc69bca934865e93d759d33bba3850e9f53fda Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Feb 2019 16:57:41 -0700 Subject: [PATCH 09/63] Add an option to sort the room list by recents first Fixes https://github.com/vector-im/riot-web/issues/8892 --- .../settings/tabs/PreferencesSettingsTab.js | 7 ++++ src/i18n/strings/en_EN.json | 2 ++ src/settings/Settings.js | 7 +++- src/stores/RoomListStore.js | 34 ++++++++++++++++++- 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/tabs/PreferencesSettingsTab.js b/src/components/views/settings/tabs/PreferencesSettingsTab.js index d76dc8f3dd..d40e532789 100644 --- a/src/components/views/settings/tabs/PreferencesSettingsTab.js +++ b/src/components/views/settings/tabs/PreferencesSettingsTab.js @@ -44,6 +44,10 @@ export default class PreferencesSettingsTab extends React.Component { 'showDisplaynameChanges', ]; + static ROOM_LIST_SETTINGS = [ + 'RoomList.orderByImportance', + ]; + static ADVANCED_SETTINGS = [ 'alwaysShowEncryptionIcons', 'Pill.shouldShowPillAvatar', @@ -104,6 +108,9 @@ export default class PreferencesSettingsTab extends React.Component { {_t("Timeline")} {this._renderGroup(PreferencesSettingsTab.TIMELINE_SETTINGS)} + {_t("Room list")} + {this._renderGroup(PreferencesSettingsTab.ROOM_LIST_SETTINGS)} + {_t("Advanced")} {this._renderGroup(PreferencesSettingsTab.ADVANCED_SETTINGS)} {autoLaunchOption} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 84c9dacd07..d7c82e23e6 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -306,6 +306,7 @@ "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs", "Show developer tools": "Show developer tools", + "Order rooms in the room list by most important first instead of most recent": "Order rooms in the room list by most important first instead of most recent", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", @@ -554,6 +555,7 @@ "Preferences": "Preferences", "Composer": "Composer", "Timeline": "Timeline", + "Room list": "Room list", "Autocomplete delay (ms)": "Autocomplete delay (ms)", "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", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index cf68fed8ba..e4db12f5ba 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -1,6 +1,6 @@ /* Copyright 2017 Travis Ralston -Copyright 2018 New Vector Ltd +Copyright 2018, 2019 New Vector Ltd. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -340,4 +340,9 @@ export const SETTINGS = { displayName: _td('Show developer tools'), default: false, }, + "RoomList.orderByImportance": { + supportedLevels: LEVELS_ACCOUNT_SETTINGS, + displayName: _td('Order rooms in the room list by most important first instead of most recent'), + default: true, + }, }; diff --git a/src/stores/RoomListStore.js b/src/stores/RoomListStore.js index 0a11c2774a..aec57dedeb 100644 --- a/src/stores/RoomListStore.js +++ b/src/stores/RoomListStore.js @@ -59,6 +59,22 @@ class RoomListStore extends Store { this._recentsComparator = this._recentsComparator.bind(this); } + /** + * Alerts the RoomListStore to a potential change in how room list sorting should + * behave. + * @param {boolean} forceRegeneration True to force a change in the algorithm + */ + updateSortingAlgorithm(forceRegeneration=false) { + const byImportance = SettingsStore.getValue("RoomList.orderByImportance"); + if (byImportance !== this._state.orderRoomsByImportance || forceRegeneration) { + console.log("Updating room sorting algorithm: sortByImportance=" + byImportance); + this._setState({orderRoomsByImportance: byImportance}); + + // Trigger a resort of the entire list to reflect the change in algorithm + this._generateInitialRoomLists(); + } + } + _init() { // Initialise state const defaultLists = { @@ -77,7 +93,10 @@ class RoomListStore extends Store { presentationLists: defaultLists, // like `lists`, but with arrays of rooms instead ready: false, stickyRoomId: null, + orderRoomsByImportance: true, }; + + SettingsStore.monitorSetting('RoomList.orderByImportance', null); } _setState(newState) { @@ -99,6 +118,11 @@ class RoomListStore extends Store { __onDispatch(payload) { const logicallyReady = this._matrixClient && this._state.ready; switch (payload.action) { + case 'setting_updated': { + if (payload.settingName !== 'RoomList.orderByImportance') break; + this.updateSortingAlgorithm(); + } + break; // Initialise state after initial sync case 'MatrixActions.sync': { if (!(payload.prevState !== 'PREPARED' && payload.state === 'PREPARED')) { @@ -106,7 +130,7 @@ class RoomListStore extends Store { } this._matrixClient = payload.matrixClient; - this._generateInitialRoomLists(); + this.updateSortingAlgorithm(/*force=*/true); } break; case 'MatrixActions.Room.receipt': { @@ -517,6 +541,14 @@ class RoomListStore extends Store { } _calculateCategory(room) { + if (!this._state.orderRoomsByImportance) { + // Effectively disable the categorization of rooms if we're supposed to + // be sorting by more recent messages first. This triggers the timestamp + // comparison bit of _setRoomCategory and _recentsComparator instead of + // the category ordering. + return CATEGORY_IDLE; + } + const mentions = room.getUnreadNotificationCount("highlight") > 0; if (mentions) return CATEGORY_RED; From e31179224ed4d407e3f186af7c2259eea4fec7af Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 22 Feb 2019 20:47:06 -0700 Subject: [PATCH 10/63] Export the defaults for SdkConfig --- src/SdkConfig.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SdkConfig.js b/src/SdkConfig.js index 65982bd6f2..78dd050a1e 100644 --- a/src/SdkConfig.js +++ b/src/SdkConfig.js @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -const DEFAULTS = { +export const DEFAULTS = { // URL to a page we show in an iframe to configure integrations integrations_ui_url: "https://scalar.vector.im/", // Base URL to the REST interface of the integrations server From b02b37125014bc160767c1ac811538b922d46ee5 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 01:06:53 +0000 Subject: [PATCH 11/63] Allow configuration of whether closing window closes or minimizes to tray Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/BasePlatform.js | 25 +++++++++++++++++ .../settings/tabs/PreferencesSettingsTab.js | 28 +++++++++++++++++-- src/i18n/strings/en_EN.json | 1 + 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 79f0d69e2c..cac8c36267 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -113,4 +113,29 @@ export default class BasePlatform { reload() { throw new Error("reload not implemented!"); } + + supportsAutoLaunch() { + return false; + } + + // XXX: Surely this should be a setting like any other? + async getAutoLaunchEnabled() { + return false; + } + + async setAutoLaunchEnabled(enabled) { + throw new Error("Unimplemented"); + } + + supportsMinimizeToTray() { + return false; + } + + async getMinimizeToTrayEnabled() { + return false; + } + + async setMinimizeToTrayEnabled() { + throw new Error("Unimplemented"); + } } diff --git a/src/components/views/settings/tabs/PreferencesSettingsTab.js b/src/components/views/settings/tabs/PreferencesSettingsTab.js index d76dc8f3dd..b6273c5d47 100644 --- a/src/components/views/settings/tabs/PreferencesSettingsTab.js +++ b/src/components/views/settings/tabs/PreferencesSettingsTab.js @@ -59,24 +59,39 @@ export default class PreferencesSettingsTab extends React.Component { this.state = { autoLaunch: false, autoLaunchSupported: false, + minimizeToTray: true, + minimizeToTraySupported: false, }; } async componentWillMount(): void { - const autoLaunchSupported = await PlatformPeg.get().supportsAutoLaunch(); + const platform = PlatformPeg.get(); + + const autoLaunchSupported = await platform.supportsAutoLaunch(); let autoLaunch = false; 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) => { PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); }; + _onMinimizeToTrayChange = (checked) => { + PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({minimizeToTray: checked})); + }; + _onAutocompleteDelayChange = (e) => { SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value); }; @@ -93,6 +108,12 @@ export default class PreferencesSettingsTab extends React.Component { onChange={this._onAutoLaunchChange} label={_t('Start automatically after system login')} />; } + let minimizeToTrayOption = null; + if (this.state.minimizeToTraySupported) { + minimizeToTrayOption = ; + } return (
      @@ -106,6 +127,7 @@ export default class PreferencesSettingsTab extends React.Component { {_t("Advanced")} {this._renderGroup(PreferencesSettingsTab.ADVANCED_SETTINGS)} + {minimizeToTrayOption} {autoLaunchOption} Date: Sun, 24 Feb 2019 01:20:49 +0000 Subject: [PATCH 12/63] delint Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/BasePlatform.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index cac8c36267..c553c9fcd2 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -114,28 +114,28 @@ export default class BasePlatform { throw new Error("reload not implemented!"); } - supportsAutoLaunch() { + supportsAutoLaunch(): boolean { return false; } // XXX: Surely this should be a setting like any other? - async getAutoLaunchEnabled() { + async getAutoLaunchEnabled(): boolean { return false; } - async setAutoLaunchEnabled(enabled) { + async setAutoLaunchEnabled(enabled: boolean) { throw new Error("Unimplemented"); } - supportsMinimizeToTray() { + supportsMinimizeToTray(): boolean { return false; } - async getMinimizeToTrayEnabled() { + async getMinimizeToTrayEnabled(): boolean { return false; } - async setMinimizeToTrayEnabled() { + async setMinimizeToTrayEnabled(enabled: boolean) { throw new Error("Unimplemented"); } } From f16b9d76f4aa1d31e657125bda6feef48f7beb8e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 01:36:47 +0000 Subject: [PATCH 13/63] add roomnick SlashCommand Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/SlashCommands.js | 18 ++++++++++++++++++ src/i18n/strings/en_EN.json | 1 + 2 files changed, 19 insertions(+) diff --git a/src/SlashCommands.js b/src/SlashCommands.js index 115cf0e018..c558efffe9 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -110,6 +110,24 @@ export const CommandMap = { }, }), + roomnick: new Command({ + name: 'roomnick', + args: '', + description: _td('Changes your display nickname in the current room only'), + runFn: function(roomId, args) { + if (args) { + const cli = MatrixClientPeg.get(); + const ev = cli.getRoom(roomId).currentState.getStateEvents('m.room.member', cli.getUserId()); + const content = { + ...ev ? ev.getContent() : null, + displayname: args, + }; + return success(cli.sendStateEvent(roomId, 'm.room.member', content, cli.getUserId())); + } + return reject(this.getUsage()); + }, + }), + tint: new Command({ name: 'tint', args: ' []', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 84c9dacd07..9a4b1ebed8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -132,6 +132,7 @@ "To use it, just wait for autocomplete results to load and tab through them.": "To use it, just wait for autocomplete results to load and tab through them.", "Upgrades a room to a new version": "Upgrades a room to a new version", "Changes your display nickname": "Changes your display nickname", + "Changes your display nickname in the current room only": "Changes your display nickname in the current room only", "Changes colour scheme of current room": "Changes colour scheme of current room", "Gets or sets the room topic": "Gets or sets the room topic", "This room has no topic.": "This room has no topic.", From 4472c66b968ca7a1c75db974799d4ce701e3c836 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 01:38:31 +0000 Subject: [PATCH 14/63] delint s'more Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/BasePlatform.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index c553c9fcd2..54310d1849 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -123,7 +123,7 @@ export default class BasePlatform { return false; } - async setAutoLaunchEnabled(enabled: boolean) { + async setAutoLaunchEnabled(enabled: boolean): void { throw new Error("Unimplemented"); } @@ -135,7 +135,7 @@ export default class BasePlatform { return false; } - async setMinimizeToTrayEnabled(enabled: boolean) { + async setMinimizeToTrayEnabled(enabled: boolean): void { throw new Error("Unimplemented"); } } From f2624beca4ff09ae91acfa46da9083d4ca501757 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 02:03:20 +0000 Subject: [PATCH 15/63] Change Share Message to Share Permalink if !m.room.message||redacted Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/context_menus/MessageContextMenu.js | 22 +++++++++++-------- src/i18n/strings/en_EN.json | 1 + 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index ffa2a0bf5c..17de6f462a 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -211,7 +211,8 @@ module.exports = React.createClass({ }, render: function() { - const eventStatus = this.props.mxEvent.status; + const mxEvent = this.props.mxEvent; + const eventStatus = mxEvent.status; let resendButton; let redactButton; let cancelButton; @@ -251,8 +252,8 @@ module.exports = React.createClass({ ); } - if (isSent && this.props.mxEvent.getType() === 'm.room.message') { - const content = this.props.mxEvent.getContent(); + if (isSent && mxEvent.getType() === 'm.room.message') { + const content = mxEvent.getContent(); if (content.msgtype && content.msgtype !== 'm.bad.encrypted' && content.hasOwnProperty('body')) { forwardButton = (
      @@ -282,7 +283,7 @@ module.exports = React.createClass({
      ); - if (this.props.mxEvent.getType() !== this.props.mxEvent.getWireType()) { + if (mxEvent.getType() !== mxEvent.getWireType()) { viewClearSourceButton = (
      { _t('View Decrypted Source') } @@ -303,8 +304,11 @@ module.exports = React.createClass({ // XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID) const permalinkButton = ( ); @@ -318,12 +322,12 @@ module.exports = React.createClass({ // Bridges can provide a 'external_url' to link back to the source. if ( - typeof(this.props.mxEvent.event.content.external_url) === "string" && - isUrlPermitted(this.props.mxEvent.event.content.external_url) + typeof(mxEvent.event.content.external_url) === "string" && + isUrlPermitted(mxEvent.event.content.external_url) ) { externalURLButton = ( ); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 84c9dacd07..4c4679022d 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1202,6 +1202,7 @@ "View Decrypted Source": "View Decrypted Source", "Unhide Preview": "Unhide Preview", "Share Message": "Share Message", + "Share Permalink": "Share Permalink", "Quote": "Quote", "Source URL": "Source URL", "Collapse Reply Thread": "Collapse Reply Thread", From c99b4bda325b0d46336ab07d3010da8e3e35d21f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 02:30:45 +0000 Subject: [PATCH 16/63] view user on click typing tile Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/views/rooms/WhoIsTypingTile.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/rooms/WhoIsTypingTile.js b/src/components/views/rooms/WhoIsTypingTile.js index dba40f033a..9dd690f6e5 100644 --- a/src/components/views/rooms/WhoIsTypingTile.js +++ b/src/components/views/rooms/WhoIsTypingTile.js @@ -170,6 +170,7 @@ module.exports = React.createClass({ width={24} height={24} resizeMethod="crop" + viewUserOnClick={true} /> ); }); From 393fd26a42ac885443ae2485cfd0cdb3f6559813 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 02:42:41 +0000 Subject: [PATCH 17/63] Settings button in Room Context Menu Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../context_menus/RoomTileContextMenu.js | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/components/views/context_menus/RoomTileContextMenu.js b/src/components/views/context_menus/RoomTileContextMenu.js index 521282488e..23c5411e2d 100644 --- a/src/components/views/context_menus/RoomTileContextMenu.js +++ b/src/components/views/context_menus/RoomTileContextMenu.js @@ -271,6 +271,28 @@ module.exports = React.createClass({ ); }, + _onClickSettings: function() { + dis.dispatch({ + action: 'view_room', + room_id: this.props.room.roomId, + }, true); + dis.dispatch({ action: 'open_room_settings' }); + if (this.props.onFinished) { + this.props.onFinished(); + } + }, + + _renderSettingsMenu: function() { + return ( +
      +
      + + { _t('Settings') } +
      +
      + ); + }, + _renderLeaveMenu: function(membership) { if (!membership) { return null; @@ -350,13 +372,17 @@ module.exports = React.createClass({ // Can't set notif level or tags on non-join rooms if (myMembership !== 'join') { - return this._renderLeaveMenu(myMembership); + return
      + { this._renderSettingsMenu() } + { this._renderLeaveMenu(myMembership) } +
      ; } return (
      { this._renderNotifMenu() }
      + { this._renderSettingsMenu() } { this._renderLeaveMenu(myMembership) }
      { this._renderRoomTagMenu() } From ac17e225567d8c122defa2f88365998dd9c2a2d7 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 02:56:27 +0000 Subject: [PATCH 18/63] Toggle Search using Room Header button Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 8e32802d0a..46d634ba34 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1305,7 +1305,7 @@ module.exports = React.createClass({ }, onSearchClick: function() { - this.setState({ searching: true, showingPinned: false }); + this.setState({ searching: !this.state.searching, showingPinned: false }); }, onCancelSearchClick: function() { From e12e2c4a3d063785614fd5c351b7cfc227a42a3f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 02:57:13 +0000 Subject: [PATCH 19/63] tidy Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/RoomView.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 46d634ba34..c93337eb6e 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -1305,7 +1305,10 @@ module.exports = React.createClass({ }, onSearchClick: function() { - this.setState({ searching: !this.state.searching, showingPinned: false }); + this.setState({ + searching: !this.state.searching, + showingPinned: false, + }); }, onCancelSearchClick: function() { From fa5f1df194b8563d458bd1e9ebeb598b85316249 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 03:18:21 +0000 Subject: [PATCH 20/63] Fix z ordering of the overflow tile Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/views/rooms/_WhoIsTypingTile.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_WhoIsTypingTile.scss b/res/css/views/rooms/_WhoIsTypingTile.scss index eb51595858..ef20c24c84 100644 --- a/res/css/views/rooms/_WhoIsTypingTile.scss +++ b/res/css/views/rooms/_WhoIsTypingTile.scss @@ -40,6 +40,7 @@ limitations under the License. } .mx_WhoIsTypingTile_remainingAvatarPlaceholder { + position: relative; display: inline-block; color: #acacac; background-color: #ddd; From e6bf9fe9bf917e3224aa5e3eb0f41646b8ff4b4e Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 03:25:02 +0000 Subject: [PATCH 21/63] Fix share community for guests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/GroupView.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index 89fce9c718..b80f49d051 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -34,6 +34,7 @@ import GroupStore from '../../stores/GroupStore'; import FlairStore from '../../stores/FlairStore'; import { showGroupAddRoomDialog } from '../../GroupAddressPicker'; import {makeGroupPermalink, makeUserPermalink} from "../../matrix-to"; +import {Group} from "matrix-js-sdk"; const LONG_DESC_PLACEHOLDER = _td( `

      HTML for your community's page

      @@ -569,7 +570,7 @@ export default React.createClass({ _onShareClick: function() { const ShareDialog = sdk.getComponent("dialogs.ShareDialog"); Modal.createTrackedDialog('share community dialog', '', ShareDialog, { - target: this._matrixClient.getGroup(this.props.groupId), + target: this._matrixClient.getGroup(this.props.groupId) || new Group(this.props.groupId), }); }, From 7b88d5d21c82a339249362733a21115fac5d98bb Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 03:43:44 +0000 Subject: [PATCH 22/63] make ViewSource less awkward Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- res/css/_common.scss | 6 --- res/css/structures/_ViewSource.scss | 13 ++++++ src/components/structures/ViewSource.js | 40 ++++++++----------- .../views/context_menus/MessageContextMenu.js | 4 ++ 4 files changed, 34 insertions(+), 29 deletions(-) diff --git a/res/css/_common.scss b/res/css/_common.scss index fd93c8c967..4e327ab28d 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -249,12 +249,6 @@ textarea { box-shadow: none; } -/* View Source Dialog overide */ -.mx_Dialog_wrapper.mx_Dialog_viewsource .mx_Dialog { - padding-left: 10px; - padding-right: 10px; -} - .mx_Dialog { background-color: $primary-bg-color; color: $light-fg-color; diff --git a/res/css/structures/_ViewSource.scss b/res/css/structures/_ViewSource.scss index a4c7dcf58a..b908861c6f 100644 --- a/res/css/structures/_ViewSource.scss +++ b/res/css/structures/_ViewSource.scss @@ -14,6 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ +.mx_ViewSource_label_left { + float: left; +} + +.mx_ViewSource_label_right { + float: right; +} + +.mx_ViewSource_label_bottom { + clear: both; + border-bottom: 1px solid #e5e5e5; +} + .mx_ViewSource pre { text-align: left; font-size: 12px; diff --git a/src/components/structures/ViewSource.js b/src/components/structures/ViewSource.js index 4844149f59..fd35fdbeef 100644 --- a/src/components/structures/ViewSource.js +++ b/src/components/structures/ViewSource.js @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,11 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. */ -'use strict'; - import React from 'react'; import PropTypes from 'prop-types'; import SyntaxHighlight from '../views/elements/SyntaxHighlight'; +import {_t} from "../../languageHandler"; +import sdk from "../../index"; module.exports = React.createClass({ @@ -27,31 +28,24 @@ module.exports = React.createClass({ propTypes: { content: PropTypes.object.isRequired, onFinished: PropTypes.func.isRequired, - }, - - componentDidMount: function() { - document.addEventListener("keydown", this.onKeyDown); - }, - - componentWillUnmount: function() { - document.removeEventListener("keydown", this.onKeyDown); - }, - - onKeyDown: function(ev) { - if (ev.keyCode == 27) { // escape - ev.stopPropagation(); - ev.preventDefault(); - this.props.onFinished(); - } + roomId: PropTypes.string.isRequired, + eventId: PropTypes.string.isRequired, }, render: function() { + const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); return ( -
      - - { JSON.stringify(this.props.content, null, 2) } - -
      + +
      Room ID: { this.props.roomId }
      +
      Event ID: { this.props.eventId }
      +
      + +
      + + { JSON.stringify(this.props.content, null, 2) } + +
      + ); }, }); diff --git a/src/components/views/context_menus/MessageContextMenu.js b/src/components/views/context_menus/MessageContextMenu.js index ffa2a0bf5c..e948c1ad93 100644 --- a/src/components/views/context_menus/MessageContextMenu.js +++ b/src/components/views/context_menus/MessageContextMenu.js @@ -98,6 +98,8 @@ module.exports = React.createClass({ onViewSourceClick: function() { const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Event Source', '', ViewSource, { + roomId: this.props.mxEvent.getRoomId(), + eventId: this.props.mxEvent.getId(), content: this.props.mxEvent.event, }, 'mx_Dialog_viewsource'); this.closeMenu(); @@ -106,6 +108,8 @@ module.exports = React.createClass({ onViewClearSourceClick: function() { const ViewSource = sdk.getComponent('structures.ViewSource'); Modal.createTrackedDialog('View Clear Event Source', '', ViewSource, { + roomId: this.props.mxEvent.getRoomId(), + eventId: this.props.mxEvent.getId(), // FIXME: _clearEvent is private content: this.props.mxEvent._clearEvent, }, 'mx_Dialog_viewsource'); From dbf540074d61008607c26ccc40ca30d4c3253f2b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sun, 24 Feb 2019 04:28:42 +0000 Subject: [PATCH 23/63] replace text Inputs in Devtools with Field bcuz prettier Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- .../views/dialogs/DevtoolsDialog.js | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js index ea198461c5..ff118792fb 100644 --- a/src/components/views/dialogs/DevtoolsDialog.js +++ b/src/components/views/dialogs/DevtoolsDialog.js @@ -20,6 +20,7 @@ import sdk from '../../../index'; import SyntaxHighlight from '../elements/SyntaxHighlight'; import { _t } from '../../../languageHandler'; import MatrixClientPeg from '../../../MatrixClientPeg'; +import Field from "../elements/Field"; class DevtoolsComponent extends React.Component { static contextTypes = { @@ -56,14 +57,8 @@ class GenericEditor extends DevtoolsComponent { } textInput(id, label) { - return
      -
      - -
      -
      - -
      -
      ; + return ; } } @@ -138,12 +133,9 @@ class SendCustomEvent extends GenericEditor {
      -
      - -
      -
      -