From 633be5061c0a197b2c8dab1c6d7641d8a37dfe51 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 4 Dec 2018 23:34:57 -0700 Subject: [PATCH 01/21] Introduce a default_server_name for aesthetics and rework .well-known Fixes https://github.com/vector-im/riot-web/issues/7724 The `default_server_name` from the config gets displayed in the "Login with my [server] matrix ID" dropdown when the default server is being used. At this point, we also discourage the use of the `default_hs_url` and `default_is_url` options because we do an implicit .well-known lookup to configure the client based on the `default_server_name`. If the URLs are still present in the config, we'll honour them and won't do a .well-known lookup when the URLs are mixed with the new server_name option. Users will be warned if the `default_server_name` does not match the `default_hs_url` if both are supplied. Users are additionally prevented from logging in, registering, and resetting their password if the implicit .well-known check fails - this is to prevent people from doing actions against the wrong homeserver. This relies on https://github.com/matrix-org/matrix-js-sdk/pull/799 as we now do auto discovery in two places. Instead of bringing the .well-known out to its own utility class in the react-sdk, we might as well drag it out to the js-sdk. --- res/css/structures/login/_Login.scss | 7 + src/components/structures/MatrixChat.js | 46 ++++- .../structures/login/ForgotPassword.js | 23 +++ src/components/structures/login/Login.js | 157 ++++++------------ .../structures/login/Registration.js | 23 ++- src/components/views/login/PasswordLogin.js | 20 ++- src/i18n/strings/en_EN.json | 8 +- 7 files changed, 166 insertions(+), 118 deletions(-) diff --git a/res/css/structures/login/_Login.scss b/res/css/structures/login/_Login.scss index 1264d2a30f..9b19c24b14 100644 --- a/res/css/structures/login/_Login.scss +++ b/res/css/structures/login/_Login.scss @@ -180,6 +180,13 @@ limitations under the License. margin-bottom: 12px; } +.mx_Login_subtext { + display: block; + font-size: 0.8em; + text-align: center; + margin: 10px; +} + .mx_Login_type_container { display: flex; margin-bottom: 14px; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4d7c71e3ef..dc3872664b 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -48,6 +48,8 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; +const AutoDiscovery = Matrix.AutoDiscovery; + // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. Promise.config({warnings: false}); @@ -181,6 +183,12 @@ export default React.createClass({ register_is_url: null, register_id_sid: null, + // Parameters used for setting up the login/registration views + defaultServerName: this.props.config.default_server_name, + defaultHsUrl: this.props.config.default_hs_url, + defaultIsUrl: this.props.config.default_is_url, + defaultServerDiscoveryError: null, + // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: false, @@ -199,6 +207,10 @@ export default React.createClass({ }; }, + getDefaultServerName: function() { + return this.state.defaultServerName; + }, + getCurrentHsUrl: function() { if (this.state.register_hs_url) { return this.state.register_hs_url; @@ -211,8 +223,10 @@ export default React.createClass({ } }, - getDefaultHsUrl() { - return this.props.config.default_hs_url || "https://matrix.org"; + getDefaultHsUrl(defaultToMatrixDotOrg) { + defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg; + if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org"; + return this.state.defaultHsUrl; }, getFallbackHsUrl: function() { @@ -232,7 +246,7 @@ export default React.createClass({ }, getDefaultIsUrl() { - return this.props.config.default_is_url || "https://vector.im"; + return this.state.defaultIsUrl || "https://vector.im"; }, componentWillMount: function() { @@ -282,6 +296,11 @@ export default React.createClass({ console.info(`Team token set to ${this._teamToken}`); } + // Set up the default URLs (async) + if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { + this._tryDiscoverDefaultHomeserver(this.getDefaultServerName()); + } + // Set a default HS with query param `hs_url` const paramHs = this.props.startingFragmentQueryParams.hs_url; if (paramHs) { @@ -1732,6 +1751,21 @@ export default React.createClass({ this.setState(newState); }, + _tryDiscoverDefaultHomeserver: async function(serverName) { + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS) { + console.error("Failed to discover homeserver on startup:", discovery); + this.setState({defaultServerDiscoveryError: discovery["m.homeserver"].error}); + } else { + const hsUrl = discovery["m.homeserver"].base_url; + const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "https://vector.im"; + this.setState({defaultHsUrl: hsUrl, defaultIsUrl: isUrl}); + } + }, + _makeRegistrationUrl: function(params) { if (this.props.startingFragmentQueryParams.referrer) { params.referrer = this.props.startingFragmentQueryParams.referrer; @@ -1820,6 +1854,8 @@ export default React.createClass({ idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} referrer={this.props.startingFragmentQueryParams.referrer} + defaultServerName={this.getDefaultServerName()} + defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} defaultHsUrl={this.getDefaultHsUrl()} defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} @@ -1842,6 +1878,8 @@ export default React.createClass({ const ForgotPassword = sdk.getComponent('structures.login.ForgotPassword'); return ( { err }; + } + const LanguageSelector = sdk.getComponent('structures.login.LanguageSelector'); resetPasswordJsx = ( @@ -230,6 +252,7 @@ module.exports = React.createClass({ { serverConfigSection } + { errorText } { _t('Return to login screen') } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index bd18699dd1..08e94e413a 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -26,11 +26,17 @@ import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import SettingsStore from "../../../settings/SettingsStore"; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import request from 'browser-request'; +import { AutoDiscovery } from "matrix-js-sdk"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; +// These are used in several places, and come from the js-sdk's autodiscovery +// stuff. We define them here so that they'll be picked up by i18n. +_td("Invalid homeserver discovery response"); +_td("Invalid identity server discovery response"); +_td("General failure"); + /** * A wire component which glues together login UI components and Login logic */ @@ -51,6 +57,14 @@ module.exports = React.createClass({ // different home server without confusing users. fallbackHsUrl: PropTypes.string, + // The default server name to use when the user hasn't specified + // one. This is used when displaying the defaultHsUrl in the UI. + defaultServerName: PropTypes.string, + + // An error passed along from higher up explaining that something + // went wrong when finding the defaultHsUrl. + defaultServerDiscoveryError: PropTypes.string, + defaultDeviceDisplayName: PropTypes.string, // login shouldn't know or care how registration is done. @@ -113,7 +127,7 @@ module.exports = React.createClass({ onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { // Prevent people from submitting their password when homeserver // discovery went wrong - if (this.state.discoveryError) return; + if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return; this.setState({ busy: true, @@ -290,114 +304,43 @@ module.exports = React.createClass({ } try { - const wellknown = await this._getWellKnownObject(`https://${serverName}/.well-known/matrix/client`); - if (!wellknown["m.homeserver"]) { - console.error("No m.homeserver key in well-known response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) { + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: discovery["m.homeserver"].error, + }); + } else if (state === AutoDiscovery.PROMPT) { + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: "", + }); + } else if (state === AutoDiscovery.SUCCESS) { + this.setState({ + discoveredHsUrl: discovery["m.homeserver"].base_url, + discoveredIsUrl: + discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "", + discoveryError: "", + }); + } else { + console.warn("Unknown state for m.homeserver in discovery response: ", discovery); + this.setState({ + discoveredHsUrl: "", + discoveredIsUrl: "", + discoveryError: _t("Unknown failure discovering homeserver"), + }); } - - const hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]); - if (!hsUrl) { - console.error("Invalid base_url for m.homeserver"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - console.log("Verifying homeserver URL: " + hsUrl); - const hsVersions = await this._getWellKnownObject(`${hsUrl}/_matrix/client/versions`); - if (!hsVersions["versions"]) { - console.error("Invalid /versions response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - let isUrl = ""; - if (wellknown["m.identity_server"]) { - isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]); - if (!isUrl) { - console.error("Invalid base_url for m.identity_server"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - - console.log("Verifying identity server URL: " + isUrl); - const isResponse = await this._getWellKnownObject(`${isUrl}/_matrix/identity/api/v1`); - if (!isResponse) { - console.error("Invalid /api/v1 response"); - this.setState({discoveryError: _t("Invalid homeserver discovery response")}); - return; - } - } - - this.setState({discoveredHsUrl: hsUrl, discoveredIsUrl: isUrl, discoveryError: ""}); } catch (e) { console.error(e); - if (e.wkAction) { - if (e.wkAction === "FAIL_ERROR" || e.wkAction === "FAIL_PROMPT") { - // We treat FAIL_ERROR and FAIL_PROMPT the same to avoid having the user - // submit their details to the wrong homeserver. In practice, the custom - // server options will show up to try and guide the user into entering - // the required information. - this.setState({discoveryError: _t("Cannot find homeserver")}); - return; - } else if (e.wkAction === "IGNORE") { - // Nothing to discover - this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""}); - return; - } - } - throw e; } }, - _sanitizeWellKnownUrl: function(url) { - if (!url) return false; - - const parser = document.createElement('a'); - parser.href = url; - - if (parser.protocol !== "http:" && parser.protocol !== "https:") return false; - if (!parser.hostname) return false; - - const port = parser.port ? `:${parser.port}` : ""; - const path = parser.pathname ? parser.pathname : ""; - let saferUrl = `${parser.protocol}//${parser.hostname}${port}${path}`; - if (saferUrl.endsWith("/")) saferUrl = saferUrl.substring(0, saferUrl.length - 1); - return saferUrl; - }, - - _getWellKnownObject: function(url) { - return new Promise(function(resolve, reject) { - request( - { method: "GET", url: url }, - (err, response, body) => { - if (err || response.status < 200 || response.status >= 300) { - let action = "FAIL_ERROR"; - if (response.status === 404) { - // We could just resolve with an empty object, but that - // causes a different series of branches when the m.homeserver - // bit of the JSON is missing. - action = "IGNORE"; - } - reject({err: err, response: response, wkAction: action}); - return; - } - - try { - resolve(JSON.parse(body)); - } catch (e) { - console.error(e); - if (e.name === "SyntaxError") { - reject({wkAction: "FAIL_PROMPT", wkError: "Invalid JSON"}); - } else throw e; - } - }, - ); - }); - }, - _initLoginLogic: function(hsUrl, isUrl) { const self = this; hsUrl = hsUrl || this.state.enteredHomeserverUrl; @@ -527,6 +470,9 @@ module.exports = React.createClass({ _renderPasswordStep: function() { const PasswordLogin = sdk.getComponent('login.PasswordLogin'); + const hsName = this.state.enteredHomeserverUrl === this.props.defaultHsUrl + ? this.props.defaultServerName + : null; return ( ); }, @@ -559,7 +506,7 @@ module.exports = React.createClass({ const ServerConfig = sdk.getComponent("login.ServerConfig"); const loader = this.state.busy ?
: null; - const errorText = this.state.discoveryError || this.state.errorText; + const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; let loginAsGuestJsx; if (this.props.enableGuest) { @@ -576,7 +523,7 @@ module.exports = React.createClass({ serverConfig = { this.state.errorText }; + const err = this.state.errorText || this.props.defaultServerDiscoveryError; + if (theme === 'status' && err) { + header =
{ err }
; } else { header =

{ _t('Create an account') }

; - if (this.state.errorText) { - errorText =
{ this.state.errorText }
; + if (err) { + errorText =
{ err }
; } } diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 6a5577fb62..582ccf94dd 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -40,6 +40,7 @@ class PasswordLogin extends React.Component { initialPassword: "", loginIncorrect: false, hsDomain: "", + hsName: null, } constructor(props) { @@ -250,13 +251,24 @@ class PasswordLogin extends React.Component { ); } - let matrixIdText = ''; + let matrixIdText = _t('Matrix ID'); + let matrixIdSubtext = null; + if (this.props.hsName) { + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName}); + } if (this.props.hsUrl) { try { const parsedHsUrl = new URL(this.props.hsUrl); - matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); + if (!this.props.hsName) { + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); + } else if (parsedHsUrl.hostname !== this.props.hsName) { + matrixIdSubtext = _t('%(serverName)s is located at %(homeserverUrl)s', { + serverName: this.props.hsName, + homeserverUrl: this.props.hsUrl, + }); + } } catch (e) { - // pass + // ignore } } @@ -292,6 +304,7 @@ class PasswordLogin extends React.Component {
{ loginType } + { matrixIdSubtext } { loginField } {this._passwordField = e;}} type="password" name="password" @@ -325,6 +338,7 @@ PasswordLogin.propTypes = { onPhoneNumberChanged: PropTypes.func, onPasswordChanged: PropTypes.func, loginIncorrect: PropTypes.bool, + hsName: PropTypes.string, }; module.exports = PasswordLogin; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e4aad2c55d..22764c2e77 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -686,6 +686,7 @@ "Mobile phone number": "Mobile phone number", "Forgot your password?": "Forgot your password?", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", + "%(serverName)s is located at %(homeserverUrl)s": "%(serverName)s is located at %(homeserverUrl)s", "Sign in with": "Sign in with", "Email address": "Email address", "Sign in": "Sign in", @@ -831,7 +832,6 @@ "And %(count)s more...|other": "And %(count)s more...", "ex. @bob:example.com": "ex. @bob:example.com", "Add User": "Add User", - "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", "You have entered an invalid address.": "You have entered an invalid address.", @@ -1283,6 +1283,9 @@ "Confirm your new password": "Confirm your new password", "Send Reset Email": "Send Reset Email", "Create an account": "Create an account", + "Invalid homeserver discovery response": "Invalid homeserver discovery response", + "Invalid identity server discovery response": "Invalid identity server discovery response", + "General failure": "General failure", "This Home Server does not support login using email address.": "This Home Server does not support login using email address.", "Please contact your service administrator to continue using this service.": "Please contact your service administrator to continue using this service.", "Incorrect username and/or password.": "Incorrect username and/or password.", @@ -1290,8 +1293,7 @@ "Guest access is disabled on this Home Server.": "Guest access is disabled on this Home Server.", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "The phone number entered looks invalid": "The phone number entered looks invalid", - "Invalid homeserver discovery response": "Invalid homeserver discovery response", - "Cannot find homeserver": "Cannot find homeserver", + "Unknown failure discovering homeserver": "Unknown failure discovering homeserver", "This homeserver doesn't offer any login flows which are supported by this client.": "This homeserver doesn't offer any login flows which are supported by this client.", "Error: Problem communicating with the given homeserver.": "Error: Problem communicating with the given homeserver.", "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.": "Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. Either use HTTPS or enable unsafe scripts.", From 173669b375aa86fda3303a757db14f4407a5854a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 6 Dec 2018 16:18:02 -0700 Subject: [PATCH 02/21] Show the number of unread notifications above the bell on the right Fixes https://github.com/vector-im/riot-web/issues/3383 This achieves the result by counting up the number of highlights across all rooms and setting that as the badge above the icon. If there are no highlights, nothing is displayed. The red highlight on the bell is done by abusing how the Tinter works: because it has access to the properties of the SVG that we'd need to override it, we give it a collection of colors it should use instead of the theme/tint it is trying to apply. This results in the Tinter using our warning color instead of whatever it was going to apply. The RightPanel now listens for events to update the count too, otherwise when the user receives a ping they'd have to switch rooms to see the change. --- res/css/structures/_RightPanel.scss | 4 +++ src/Tinter.js | 13 +++++---- src/components/structures/RightPanel.js | 28 ++++++++++++++++++-- src/components/views/elements/TintableSvg.js | 3 ++- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/res/css/structures/_RightPanel.scss b/res/css/structures/_RightPanel.scss index b4dff612ed..554aabfcd1 100644 --- a/res/css/structures/_RightPanel.scss +++ b/res/css/structures/_RightPanel.scss @@ -55,6 +55,10 @@ limitations under the License. padding-bottom: 3px; } +.mx_RightPanel_headerButton_badgeHighlight .mx_RightPanel_headerButton_badge { + color: $warning-color; +} + .mx_RightPanel_headerButton_highlight { width: 25px; height: 5px; diff --git a/src/Tinter.js b/src/Tinter.js index d24a4c3e74..1b1ebbcccd 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -390,7 +390,7 @@ class Tinter { // XXX: we could just move this all into TintableSvg, but as it's so similar // to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg) // keeping it here for now. - calcSvgFixups(svgs) { + calcSvgFixups(svgs, forceColors) { // go through manually fixing up SVG colours. // we could do this by stylesheets, but keeping the stylesheets // updated would be a PITA, so just brute-force search for the @@ -418,13 +418,14 @@ class Tinter { const tag = tags[j]; for (let k = 0; k < this.svgAttrs.length; k++) { const attr = this.svgAttrs[k]; - for (let l = 0; l < this.keyHex.length; l++) { + for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please. if (tag.getAttribute(attr) && - tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) { + tag.getAttribute(attr).toUpperCase() === this.keyHex[m]) { fixups.push({ node: tag, attr: attr, - index: l, + index: m, + forceColors: forceColors, }); } } @@ -440,7 +441,9 @@ class Tinter { if (DEBUG) console.log("applySvgFixups start for " + fixups); for (let i = 0; i < fixups.length; i++) { const svgFixup = fixups[i]; - svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]); + const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null; + if (forcedColor) console.log(forcedColor); + svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]); } if (DEBUG) console.log("applySvgFixups end"); } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 9017447a34..c21c5f459f 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -30,6 +30,7 @@ import { showGroupInviteDialog, showGroupAddRoomDialog } from '../../GroupAddres import GroupStore from '../../stores/GroupStore'; import { formatCount } from '../../utils/FormattingUtils'; +import MatrixClientPeg from "../../MatrixClientPeg"; class HeaderButton extends React.Component { constructor() { @@ -49,17 +50,26 @@ class HeaderButton extends React.Component { const TintableSvg = sdk.getComponent("elements.TintableSvg"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); + // XXX: We really shouldn't be hardcoding colors here, but the way TintableSvg + // works kinda prevents us from using normal CSS tactics. We use $warning-color + // here. + // Note: This array gets passed along to the Tinter's forceColors eventually. + const tintableColors = this.props.badgeHighlight ? ["#ff0064"] : null; + + const classNames = ["mx_RightPanel_headerButton"]; + if (this.props.badgeHighlight) classNames.push("mx_RightPanel_headerButton_badgeHighlight"); + return
{ this.props.badge ? this.props.badge :   }
- + { this.props.isHighlighted ?
:
} ; @@ -76,6 +86,7 @@ HeaderButton.propTypes = { // The badge to display above the icon badge: PropTypes.node, + badgeHighlight: PropTypes.bool, // The parameters to track the click event analytics: PropTypes.arrayOf(PropTypes.string).isRequired, @@ -113,6 +124,7 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); const cli = this.context.matrixClient; cli.on("RoomState.members", this.onRoomStateMember); + cli.on("Room.notificationCounts", this.onRoomNotifications); this._initGroupStore(this.props.groupId); }, @@ -200,6 +212,10 @@ module.exports = React.createClass({ } }, + onRoomNotifications: function(room, type, count) { + if (type === "highlight") this.forceUpdate(); + }, + _delayedUpdate: new RateLimitedFunc(function() { this.forceUpdate(); // eslint-disable-line babel/no-invalid-this }, 500), @@ -308,6 +324,13 @@ module.exports = React.createClass({ let headerButtons = []; if (this.props.roomId) { + let notifCountBadge; + let notifCount = 0; + MatrixClientPeg.get().getRooms().forEach(r => notifCount += (r.getUnreadNotificationCount('highlight') || 0)); + if (notifCount > 0) { + notifCountBadge =
{ formatCount(notifCount) }
; + } + headerButtons = [ 0} analytics={['Right Panel', 'Notification List Button', 'click']} />, ]; diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index e04bf87793..af9e56377b 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -29,6 +29,7 @@ var TintableSvg = React.createClass({ width: PropTypes.string.isRequired, height: PropTypes.string.isRequired, className: PropTypes.string, + forceColors: PropTypes.arrayOf(PropTypes.string), }, statics: { @@ -58,7 +59,7 @@ var TintableSvg = React.createClass({ onLoad: function(event) { // console.log("TintableSvg.onLoad for " + this.props.src); - this.fixups = Tinter.calcSvgFixups([event.target]); + this.fixups = Tinter.calcSvgFixups([event.target], this.props.forceColors); Tinter.applySvgFixups(this.fixups); }, From 95d15b78632bdf770928729fb3f468295cd74925 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 6 Dec 2018 22:26:51 -0700 Subject: [PATCH 03/21] Fix tinting of notification icon and use a more reliable notification source The js-sdk's placement of the notification change was unreliable and could cause stuck notifications. The new location (piggybacking the Notifier) is a lot more reliable. The tinting has been changed fairly invasively in order to support the changing of the `fill` attribute. What was happening before was the `fill` property would happily get set to the forced color value, but when it came time to reset it it wouldn't be part of the colors array and fail the check, therefore never being changed back. By using a second field we can ensure we are checking the not-forced value where possible, falling back to the potentially forced value if needed. In addition to fixing which color the Tinter was checking against, something noticed during development is that `this.colors` might not always be a set of hex color codes. This is problematic when the attribute we're looking to replace is a rgb color code but we're only looking at `keyHex` - the value won't be reset. It appears as though this happens when people use custom tinting in places as `this.colors` often gets set to the rgb values throughout the file. To fix it, we just check against `keyHex` and `keyRgb`. --- src/Notifier.js | 5 +++++ src/Tinter.js | 13 ++++++++++--- src/components/structures/RightPanel.js | 10 ++++------ src/components/views/elements/TintableSvg.js | 16 ++++++++++++++-- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 80e8be1084..8550f3bf95 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -289,6 +289,11 @@ const Notifier = { const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); if (actions && actions.notify) { + dis.dispatch({ + action: "event_notification", + event: ev, + room: room, + }); if (this.isEnabled()) { this._displayPopupNotification(ev, room); } diff --git a/src/Tinter.js b/src/Tinter.js index 1b1ebbcccd..9c2afd4fab 100644 --- a/src/Tinter.js +++ b/src/Tinter.js @@ -419,11 +419,18 @@ class Tinter { for (let k = 0; k < this.svgAttrs.length; k++) { const attr = this.svgAttrs[k]; for (let m = 0; m < this.keyHex.length; m++) { // dev note: don't use L please. - if (tag.getAttribute(attr) && - tag.getAttribute(attr).toUpperCase() === this.keyHex[m]) { + // We use a different attribute from the one we're setting + // because we may also be using forceColors. If we were to + // check the keyHex against a forceColors value, it may not + // match and therefore not change when we need it to. + const valAttrName = "mx-val-" + attr; + let attribute = tag.getAttribute(valAttrName); + if (!attribute) attribute = tag.getAttribute(attr); // fall back to the original + if (attribute && (attribute.toUpperCase() === this.keyHex[m] || attribute.toLowerCase() === this.keyRgb[m])) { fixups.push({ node: tag, attr: attr, + refAttr: valAttrName, index: m, forceColors: forceColors, }); @@ -442,8 +449,8 @@ class Tinter { for (let i = 0; i < fixups.length; i++) { const svgFixup = fixups[i]; const forcedColor = svgFixup.forceColors ? svgFixup.forceColors[svgFixup.index] : null; - if (forcedColor) console.log(forcedColor); svgFixup.node.setAttribute(svgFixup.attr, forcedColor ? forcedColor : this.colors[svgFixup.index]); + svgFixup.node.setAttribute(svgFixup.refAttr, this.colors[svgFixup.index]); } if (DEBUG) console.log("applySvgFixups end"); } diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index c21c5f459f..0870f085a5 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -124,7 +124,6 @@ module.exports = React.createClass({ this.dispatcherRef = dis.register(this.onAction); const cli = this.context.matrixClient; cli.on("RoomState.members", this.onRoomStateMember); - cli.on("Room.notificationCounts", this.onRoomNotifications); this._initGroupStore(this.props.groupId); }, @@ -212,16 +211,15 @@ module.exports = React.createClass({ } }, - onRoomNotifications: function(room, type, count) { - if (type === "highlight") this.forceUpdate(); - }, - _delayedUpdate: new RateLimitedFunc(function() { this.forceUpdate(); // eslint-disable-line babel/no-invalid-this }, 500), onAction: function(payload) { - if (payload.action === "view_user") { + if (payload.action === "event_notification") { + // Try and re-caclulate any badge counts we might have + this.forceUpdate(); + } else if (payload.action === "view_user") { dis.dispatch({ action: 'show_right_panel', }); diff --git a/src/components/views/elements/TintableSvg.js b/src/components/views/elements/TintableSvg.js index af9e56377b..08628c8ca9 100644 --- a/src/components/views/elements/TintableSvg.js +++ b/src/components/views/elements/TintableSvg.js @@ -51,6 +51,12 @@ var TintableSvg = React.createClass({ delete TintableSvg.mounts[this.id]; }, + componentDidUpdate: function(prevProps, prevState) { + if (prevProps.forceColors !== this.props.forceColors) { + this.calcAndApplyFixups(this.refs.svgContainer); + } + }, + tint: function() { // TODO: only bother running this if the global tint settings have changed // since we loaded! @@ -58,8 +64,13 @@ var TintableSvg = React.createClass({ }, onLoad: function(event) { - // console.log("TintableSvg.onLoad for " + this.props.src); - this.fixups = Tinter.calcSvgFixups([event.target], this.props.forceColors); + this.calcAndApplyFixups(event.target); + }, + + calcAndApplyFixups: function(target) { + if (!target) return; + // console.log("TintableSvg.calcAndApplyFixups for " + this.props.src); + this.fixups = Tinter.calcSvgFixups([target], this.props.forceColors); Tinter.applySvgFixups(this.fixups); }, @@ -72,6 +83,7 @@ var TintableSvg = React.createClass({ height={this.props.height} onLoad={this.onLoad} tabIndex="-1" + ref="svgContainer" /> ); }, From d062e2c2f4dca6eb52650b34a520e6a6bf31199d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Dec 2018 15:03:58 -0700 Subject: [PATCH 04/21] Check to make sure email addresses look roughly valid before inviting them to room Fixes https://github.com/vector-im/riot-web/issues/6854 --- src/components/views/dialogs/AddressPickerDialog.js | 5 +++++ src/i18n/strings/en_EN.json | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index abc52f7b1d..cbe80763a6 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import Promise from 'bluebird'; import { addressTypes, getAddressType } from '../../../UserAddress.js'; import GroupStore from '../../../stores/GroupStore'; +import * as Email from "../../../email"; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -419,6 +420,10 @@ module.exports = React.createClass({ // a perfectly valid address if there are close matches. const addrType = getAddressType(query); if (this.props.validAddressTypes.includes(addrType)) { + if (addrType === 'email' && !Email.looksValid(query)) { + this.setState({searchError: _t("That doesn't look like a valid email address")}); + return; + } suggestedList.unshift({ addressType: addrType, address: query, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a4ce5143d7..ff403e9062 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -871,6 +871,7 @@ "Matrix ID": "Matrix ID", "Matrix Room ID": "Matrix Room ID", "email address": "email address", + "That doesn't look like a valid email address": "That doesn't look like a valid email address", "You have entered an invalid address.": "You have entered an invalid address.", "Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.", "Preparing to send logs": "Preparing to send logs", @@ -912,7 +913,6 @@ "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ": "You've previously used a newer version of Riot on %(host)s. To use this version again with end to end encryption, you will need to sign out and back in again. ", "Incompatible Database": "Incompatible Database", "Continue With Encryption Disabled": "Continue With Encryption Disabled", - "Failed to indicate account erasure": "Failed to indicate account erasure", "Unknown error": "Unknown error", "Incorrect password": "Incorrect password", "Deactivate Account": "Deactivate Account", From 6707186edcec64e314dfeebbe3900100686d484f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Dec 2018 15:36:49 -0700 Subject: [PATCH 05/21] Change how the default server name and HS URL interact They are now independent of each other. If both are specified in the config, the user will see an error and be prevented from logging in. The expected behaviour is that when a default server name is given, we do a .well-known lookup to find the default homeserver (and block the UI while we do this to prevent it from using matrix.org while we go out and find more information). If the config specifies just a default homeserver URL however, we don't do anything special. --- res/css/structures/login/_Login.scss | 7 ------- src/components/structures/MatrixChat.js | 22 ++++++++++++++++++--- src/components/structures/login/Login.js | 5 +---- src/components/views/login/PasswordLogin.js | 14 ++----------- src/i18n/strings/en_EN.json | 3 ++- 5 files changed, 24 insertions(+), 27 deletions(-) diff --git a/res/css/structures/login/_Login.scss b/res/css/structures/login/_Login.scss index 9b19c24b14..1264d2a30f 100644 --- a/res/css/structures/login/_Login.scss +++ b/res/css/structures/login/_Login.scss @@ -180,13 +180,6 @@ limitations under the License. margin-bottom: 12px; } -.mx_Login_subtext { - display: block; - font-size: 0.8em; - text-align: center; - margin: 10px; -} - .mx_Login_type_container { display: flex; margin-bottom: 14px; diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index dc3872664b..e93234c679 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -298,7 +298,16 @@ export default React.createClass({ // Set up the default URLs (async) if (this.getDefaultServerName() && !this.getDefaultHsUrl(false)) { + this.setState({loadingDefaultHomeserver: true}); this._tryDiscoverDefaultHomeserver(this.getDefaultServerName()); + } else if (this.getDefaultServerName() && this.getDefaultHsUrl(false)) { + // Ideally we would somehow only communicate this to the server admins, but + // given this is at login time we can't really do much besides hope that people + // will check their settings. + this.setState({ + defaultServerName: null, // To un-hide any secrets people might be keeping + defaultServerDiscoveryError: _t("Invalid configuration: Cannot supply a default homeserver URL and a default server name"), + }); } // Set a default HS with query param `hs_url` @@ -1756,13 +1765,20 @@ export default React.createClass({ const state = discovery["m.homeserver"].state; if (state !== AutoDiscovery.SUCCESS) { console.error("Failed to discover homeserver on startup:", discovery); - this.setState({defaultServerDiscoveryError: discovery["m.homeserver"].error}); + this.setState({ + defaultServerDiscoveryError: discovery["m.homeserver"].error, + loadingDefaultHomeserver: false, + }); } else { const hsUrl = discovery["m.homeserver"].base_url; const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS ? discovery["m.identity_server"].base_url : "https://vector.im"; - this.setState({defaultHsUrl: hsUrl, defaultIsUrl: isUrl}); + this.setState({ + defaultHsUrl: hsUrl, + defaultIsUrl: isUrl, + loadingDefaultHomeserver: false, + }); } }, @@ -1780,7 +1796,7 @@ export default React.createClass({ render: function() { // console.log(`Rendering MatrixChat with view ${this.state.view}`); - if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN) { + if (this.state.view === VIEWS.LOADING || this.state.view === VIEWS.LOGGING_IN || this.state.loadingDefaultHomeserver) { const Spinner = sdk.getComponent('elements.Spinner'); return (
diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 08e94e413a..6dcbfe7e47 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -470,9 +470,6 @@ module.exports = React.createClass({ _renderPasswordStep: function() { const PasswordLogin = sdk.getComponent('login.PasswordLogin'); - const hsName = this.state.enteredHomeserverUrl === this.props.defaultHsUrl - ? this.props.defaultServerName - : null; return ( ); }, diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 582ccf94dd..04aaae3630 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -252,21 +252,12 @@ class PasswordLogin extends React.Component { } let matrixIdText = _t('Matrix ID'); - let matrixIdSubtext = null; if (this.props.hsName) { matrixIdText = _t('%(serverName)s Matrix ID', {serverName: this.props.hsName}); - } - if (this.props.hsUrl) { + } else { try { const parsedHsUrl = new URL(this.props.hsUrl); - if (!this.props.hsName) { - matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); - } else if (parsedHsUrl.hostname !== this.props.hsName) { - matrixIdSubtext = _t('%(serverName)s is located at %(homeserverUrl)s', { - serverName: this.props.hsName, - homeserverUrl: this.props.hsUrl, - }); - } + matrixIdText = _t('%(serverName)s Matrix ID', {serverName: parsedHsUrl.hostname}); } catch (e) { // ignore } @@ -304,7 +295,6 @@ class PasswordLogin extends React.Component {
{ loginType } - { matrixIdSubtext } { loginField } {this._passwordField = e;}} type="password" name="password" diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 87bc05c81c..8c5f3f5351 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -721,8 +721,8 @@ "User name": "User name", "Mobile phone number": "Mobile phone number", "Forgot your password?": "Forgot your password?", + "Matrix ID": "Matrix ID", "%(serverName)s Matrix ID": "%(serverName)s Matrix ID", - "%(serverName)s is located at %(homeserverUrl)s": "%(serverName)s is located at %(homeserverUrl)s", "Sign in with": "Sign in with", "Email address": "Email address", "Sign in": "Sign in", @@ -1114,6 +1114,7 @@ "You are currently using Riot anonymously as a guest.": "You are currently using Riot anonymously as a guest.", "If you would like to create a Matrix account you can register now.": "If you would like to create a Matrix account you can register now.", "Login": "Login", + "Invalid configuration: Cannot supply a default homeserver URL and a default server name": "Invalid configuration: Cannot supply a default homeserver URL and a default server name", "Failed to reject invitation": "Failed to reject invitation", "This room is not public. You will not be able to rejoin without an invite.": "This room is not public. You will not be able to rejoin without an invite.", "Are you sure you want to leave the room '%(roomName)s'?": "Are you sure you want to leave the room '%(roomName)s'?", From a969237dc07a203baca4ef3903dc83d0f3479012 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 7 Dec 2018 15:37:20 -0700 Subject: [PATCH 06/21] Disable the submit button while .well-known is underway To give the user a little feedback about something happening. This definitely needs to be improved in the future though. --- src/components/structures/login/Login.js | 9 ++++++++- src/components/views/login/PasswordLogin.js | 6 +++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index 6dcbfe7e47..bfaab8fdb8 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -94,6 +94,7 @@ module.exports = React.createClass({ discoveredHsUrl: "", discoveredIsUrl: "", discoveryError: "", + findingHomeserver: false, }; }, @@ -299,10 +300,11 @@ module.exports = React.createClass({ _tryWellKnownDiscovery: async function(serverName) { if (!serverName.trim()) { // Nothing to discover - this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: ""}); + this.setState({discoveryError: "", discoveredHsUrl: "", discoveredIsUrl: "", findingHomeserver: false}); return; } + this.setState({findingHomeserver: true}); try { const discovery = await AutoDiscovery.findClientConfig(serverName); const state = discovery["m.homeserver"].state; @@ -311,12 +313,14 @@ module.exports = React.createClass({ discoveredHsUrl: "", discoveredIsUrl: "", discoveryError: discovery["m.homeserver"].error, + findingHomeserver: false, }); } else if (state === AutoDiscovery.PROMPT) { this.setState({ discoveredHsUrl: "", discoveredIsUrl: "", discoveryError: "", + findingHomeserver: false, }); } else if (state === AutoDiscovery.SUCCESS) { this.setState({ @@ -326,6 +330,7 @@ module.exports = React.createClass({ ? discovery["m.identity_server"].base_url : "", discoveryError: "", + findingHomeserver: false, }); } else { console.warn("Unknown state for m.homeserver in discovery response: ", discovery); @@ -333,6 +338,7 @@ module.exports = React.createClass({ discoveredHsUrl: "", discoveredIsUrl: "", discoveryError: _t("Unknown failure discovering homeserver"), + findingHomeserver: false, }); } } catch (e) { @@ -485,6 +491,7 @@ module.exports = React.createClass({ loginIncorrect={this.state.loginIncorrect} hsUrl={this.state.enteredHomeserverUrl} hsName={this.props.defaultServerName} + disableSubmit={this.state.findingHomeserver} /> ); }, diff --git a/src/components/views/login/PasswordLogin.js b/src/components/views/login/PasswordLogin.js index 04aaae3630..59d4db379c 100644 --- a/src/components/views/login/PasswordLogin.js +++ b/src/components/views/login/PasswordLogin.js @@ -41,6 +41,7 @@ class PasswordLogin extends React.Component { loginIncorrect: false, hsDomain: "", hsName: null, + disableSubmit: false, } constructor(props) { @@ -291,6 +292,8 @@ class PasswordLogin extends React.Component { ); } + const disableSubmit = this.props.disableSubmit || matrixIdText === ''; + return (
@@ -304,7 +307,7 @@ class PasswordLogin extends React.Component { />
{ forgotPasswordJsx } - +
); @@ -329,6 +332,7 @@ PasswordLogin.propTypes = { onPasswordChanged: PropTypes.func, loginIncorrect: PropTypes.bool, hsName: PropTypes.string, + disableSubmit: PropTypes.bool, }; module.exports = PasswordLogin; From 04c30181c63b5f17a775a580d2b7e5f37e256959 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 11 Dec 2018 19:01:27 +0000 Subject: [PATCH 07/21] Expose hidden notification rules in UI Adds UI control for 3 hidden notification rules: * Messages containing @room * Encrypted one-to-one messages * Encrypted group messages This should help to clarify some mysterious notification behavior, as it wasn't obvious that these rules existed. Fixes vector-im/riot-web#7833. Signed-off-by: J. Ryan Stinnett --- .../views/settings/Notifications.js | 6 +++ src/i18n/strings/en_EN.json | 3 ++ src/notifications/StandardActions.js | 1 + .../VectorPushRulesDefinitions.js | 54 ++++++++++++++++--- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/components/views/settings/Notifications.js b/src/components/views/settings/Notifications.js index 72ad2943aa..40c43e6b2e 100644 --- a/src/components/views/settings/Notifications.js +++ b/src/components/views/settings/Notifications.js @@ -483,8 +483,11 @@ module.exports = React.createClass({ // The default push rules displayed by Vector UI '.m.rule.contains_display_name': 'vector', '.m.rule.contains_user_name': 'vector', + '.m.rule.roomnotif': 'vector', '.m.rule.room_one_to_one': 'vector', + '.m.rule.encrypted_room_one_to_one': 'vector', '.m.rule.message': 'vector', + '.m.rule.encrypted': 'vector', '.m.rule.invite_for_me': 'vector', //'.m.rule.member_event': 'vector', '.m.rule.call': 'vector', @@ -534,9 +537,12 @@ module.exports = React.createClass({ const vectorRuleIds = [ '.m.rule.contains_display_name', '.m.rule.contains_user_name', + '.m.rule.roomnotif', '_keywords', '.m.rule.room_one_to_one', + '.m.rule.encrypted_room_one_to_one', '.m.rule.message', + '.m.rule.encrypted', '.m.rule.invite_for_me', //'im.vector.rule.member_event', '.m.rule.call', diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 0df81b8e2a..7165539347 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -295,8 +295,11 @@ "Waiting for response from server": "Waiting for response from server", "Messages containing my display name": "Messages containing my display name", "Messages containing my user name": "Messages containing my user name", + "Messages containing @room": "Messages containing @room", "Messages in one-to-one chats": "Messages in one-to-one chats", + "Encrypted messages in one-to-one chats": "Encrypted messages in one-to-one chats", "Messages in group chats": "Messages in group chats", + "Encrypted messages in group chats": "Encrypted messages in group chats", "When I'm invited to a room": "When I'm invited to a room", "Call invitation": "Call invitation", "Messages sent by bot": "Messages sent by bot", diff --git a/src/notifications/StandardActions.js b/src/notifications/StandardActions.js index 30d6ea5975..15f645d5f7 100644 --- a/src/notifications/StandardActions.js +++ b/src/notifications/StandardActions.js @@ -24,6 +24,7 @@ module.exports = { ACTION_NOTIFY: encodeActions({notify: true}), ACTION_NOTIFY_DEFAULT_SOUND: encodeActions({notify: true, sound: "default"}), ACTION_NOTIFY_RING_SOUND: encodeActions({notify: true, sound: "ring"}), + ACTION_HIGHLIGHT: encodeActions({notify: true, highlight: true}), ACTION_HIGHLIGHT_DEFAULT_SOUND: encodeActions({notify: true, sound: "default", highlight: true}), ACTION_DONT_NOTIFY: encodeActions({notify: false}), ACTION_DISABLED: null, diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index eeb193cb8a..d763da7e64 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -20,6 +20,7 @@ import { _td } from '../languageHandler'; const StandardActions = require('./StandardActions'); const PushRuleVectorState = require('./PushRuleVectorState'); +const { decodeActions } = require('./NotificationUtils'); class VectorPushRuleDefinition { constructor(opts) { @@ -31,13 +32,11 @@ class VectorPushRuleDefinition { // Translate the rule actions and its enabled value into vector state ruleToVectorState(rule) { let enabled = false; - let actions = null; if (rule) { enabled = rule.enabled; - actions = rule.actions; } - for (const stateKey in PushRuleVectorState.states) { + for (const stateKey in PushRuleVectorState.states) { // eslint-disable-line guard-for-in const state = PushRuleVectorState.states[stateKey]; const vectorStateToActions = this.vectorStateToActions[state]; @@ -47,15 +46,21 @@ class VectorPushRuleDefinition { return state; } } else { - // The actions must match to the ones expected by vector state - if (enabled && JSON.stringify(rule.actions) === JSON.stringify(vectorStateToActions)) { + // The actions must match to the ones expected by vector state. + // Use `decodeActions` on both sides to canonicalize things like + // value: true vs. unspecified for highlight (which defaults to + // true, making them equivalent. + if (enabled && + JSON.stringify(decodeActions(rule.actions)) === + JSON.stringify(decodeActions(vectorStateToActions))) { return state; } } } - console.error("Cannot translate rule actions into Vector rule state. Rule: " + - JSON.stringify(rule)); + console.error(`Cannot translate rule actions into Vector rule state. ` + + `Rule: ${JSON.stringify(rule)}, ` + + `Expected: ${JSON.stringify(this.vectorStateToActions)}`); return undefined; } } @@ -86,6 +91,17 @@ module.exports = { }, }), + // Messages containing @room + ".m.rule.roomnotif": new VectorPushRuleDefinition({ + kind: "override", + description: _td("Messages containing @room"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { // The actions for each vector state, or null to disable the rule. + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_HIGHLIGHT, + off: StandardActions.ACTION_DISABLED, + }, + }), + // Messages just sent to the user in a 1:1 room ".m.rule.room_one_to_one": new VectorPushRuleDefinition({ kind: "underride", @@ -97,6 +113,17 @@ module.exports = { }, }), + // Encrypted messages just sent to the user in a 1:1 room + ".m.rule.encrypted_room_one_to_one": new VectorPushRuleDefinition({ + kind: "underride", + description: _td("Encrypted messages in one-to-one chats"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, + }, + }), + // Messages just sent to a group chat room // 1:1 room messages are catched by the .m.rule.room_one_to_one rule if any defined // By opposition, all other room messages are from group chat rooms. @@ -110,6 +137,19 @@ module.exports = { }, }), + // Encrypted messages just sent to a group chat room + // Encrypted 1:1 room messages are catched by the .m.rule.encrypted_room_one_to_one rule if any defined + // By opposition, all other room messages are from group chat rooms. + ".m.rule.encrypted": new VectorPushRuleDefinition({ + kind: "underride", + description: _td("Encrypted messages in group chats"), // passed through _t() translation in src/components/views/settings/Notifications.js + vectorStateToActions: { + on: StandardActions.ACTION_NOTIFY, + loud: StandardActions.ACTION_NOTIFY_DEFAULT_SOUND, + off: StandardActions.ACTION_DONT_NOTIFY, + }, + }), + // Invitation for the user ".m.rule.invite_for_me": new VectorPushRuleDefinition({ kind: "underride", From 2ca747740616c7c0f8e1fc0b071fb5526b1b31de Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Wed, 12 Dec 2018 01:46:48 +0000 Subject: [PATCH 08/21] Fix typo in push rules comment Signed-off-by: J. Ryan Stinnett --- src/notifications/VectorPushRulesDefinitions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notifications/VectorPushRulesDefinitions.js b/src/notifications/VectorPushRulesDefinitions.js index d763da7e64..3df2e70774 100644 --- a/src/notifications/VectorPushRulesDefinitions.js +++ b/src/notifications/VectorPushRulesDefinitions.js @@ -49,7 +49,7 @@ class VectorPushRuleDefinition { // The actions must match to the ones expected by vector state. // Use `decodeActions` on both sides to canonicalize things like // value: true vs. unspecified for highlight (which defaults to - // true, making them equivalent. + // true, making them equivalent). if (enabled && JSON.stringify(decodeActions(rule.actions)) === JSON.stringify(decodeActions(vectorStateToActions))) { From 15366fbb0a9434ab06bac7a965cfe09375ef07ab Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 15:59:18 +0000 Subject: [PATCH 09/21] Change room recovery reminder button style Change the button to a transparent background so that it's less prominent and you focus on the primary button instead. Signed-off-by: J. Ryan Stinnett --- res/css/views/rooms/_RoomRecoveryReminder.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/rooms/_RoomRecoveryReminder.scss index 4bb42ff114..e4e2d19b42 100644 --- a/res/css/views/rooms/_RoomRecoveryReminder.scss +++ b/res/css/views/rooms/_RoomRecoveryReminder.scss @@ -40,4 +40,5 @@ limitations under the License. .mx_RoomRecoveryReminder_button.mx_RoomRecoveryReminder_secondary { @mixin mx_DialogButton_secondary; + background-color: transparent; } From 5f434cd31cda9d64523810777e4be48d3ce293d6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 14:45:08 -0700 Subject: [PATCH 10/21] Don't break the UI when something goes wrong --- src/components/structures/MatrixChat.js | 38 ++++++++++++++---------- src/components/structures/login/Login.js | 5 +++- src/i18n/strings/en_EN.json | 1 + 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index e93234c679..c8b2737cc9 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1761,22 +1761,30 @@ export default React.createClass({ }, _tryDiscoverDefaultHomeserver: async function(serverName) { - const discovery = await AutoDiscovery.findClientConfig(serverName); - const state = discovery["m.homeserver"].state; - if (state !== AutoDiscovery.SUCCESS) { - console.error("Failed to discover homeserver on startup:", discovery); + try { + const discovery = await AutoDiscovery.findClientConfig(serverName); + const state = discovery["m.homeserver"].state; + if (state !== AutoDiscovery.SUCCESS) { + console.error("Failed to discover homeserver on startup:", discovery); + this.setState({ + defaultServerDiscoveryError: discovery["m.homeserver"].error, + loadingDefaultHomeserver: false, + }); + } else { + const hsUrl = discovery["m.homeserver"].base_url; + const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS + ? discovery["m.identity_server"].base_url + : "https://vector.im"; + this.setState({ + defaultHsUrl: hsUrl, + defaultIsUrl: isUrl, + loadingDefaultHomeserver: false, + }); + } + } catch (e) { + console.error(e); this.setState({ - defaultServerDiscoveryError: discovery["m.homeserver"].error, - loadingDefaultHomeserver: false, - }); - } else { - const hsUrl = discovery["m.homeserver"].base_url; - const isUrl = discovery["m.identity_server"].state === AutoDiscovery.SUCCESS - ? discovery["m.identity_server"].base_url - : "https://vector.im"; - this.setState({ - defaultHsUrl: hsUrl, - defaultIsUrl: isUrl, + defaultServerDiscoveryError: _t("Unknown error discovering homeserver"), loadingDefaultHomeserver: false, }); } diff --git a/src/components/structures/login/Login.js b/src/components/structures/login/Login.js index bfaab8fdb8..b94a1759cf 100644 --- a/src/components/structures/login/Login.js +++ b/src/components/structures/login/Login.js @@ -343,7 +343,10 @@ module.exports = React.createClass({ } } catch (e) { console.error(e); - throw e; + this.setState({ + findingHomeserver: false, + discoveryError: _t("Unknown error discovering homeserver"), + }); } }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8c5f3f5351..56109fa8db 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1128,6 +1128,7 @@ "Review terms and conditions": "Review terms and conditions", "Old cryptography data detected": "Old cryptography data detected", "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of Riot has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", + "Unknown error discovering homeserver": "Unknown error discovering homeserver", "Logout": "Logout", "Your Communities": "Your Communities", "Did you know: you can use communities to filter your Riot.im experience!": "Did you know: you can use communities to filter your Riot.im experience!", From 49769a405d70ccfced28dd5f58af6fe0494dba4d Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 16:12:56 -0700 Subject: [PATCH 11/21] Fix translation error on notification icon Introduced by https://github.com/matrix-org/matrix-react-sdk/pull/2336 --- src/components/structures/RightPanel.js | 3 ++- src/i18n/strings/en_EN.json | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 0870f085a5..e8371697f2 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -326,7 +326,8 @@ module.exports = React.createClass({ let notifCount = 0; MatrixClientPeg.get().getRooms().forEach(r => notifCount += (r.getUnreadNotificationCount('highlight') || 0)); if (notifCount > 0) { - notifCountBadge =
{ formatCount(notifCount) }
; + const title = _t("%(count)s Notifications", {count: formatCount(notifCount)}); + notifCountBadge =
{ formatCount(notifCount) }
; } headerButtons = [ diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 92fb545b4e..a97182520a 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1144,6 +1144,8 @@ "%(count)s Members|other": "%(count)s Members", "%(count)s Members|one": "%(count)s Member", "Invite to this room": "Invite to this room", + "%(count)s Notifications|other": "%(count)s Notifications", + "%(count)s Notifications|one": "%(count)s Notification", "Files": "Files", "Notifications": "Notifications", "Hide panel": "Hide panel", From 8592e76e124c28d69903865e43180c7153bbaee0 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 16:05:34 -0700 Subject: [PATCH 12/21] Standardize errors about localpart structure Fixes https://github.com/vector-im/riot-web/issues/5833 This also includes changing some Jira references that aren't searchable anymore, and a thing to replace the spinner on the SetMxidDialog as per https://github.com/vector-im/riot-web/issues/5833#issuecomment-445323177 --- src/Registration.js | 4 ++++ src/components/structures/login/Registration.js | 2 +- src/components/views/dialogs/SetMxIdDialog.js | 15 ++++++--------- src/components/views/login/RegistrationForm.js | 7 +++---- .../views/room_settings/AliasSettings.js | 2 +- src/i18n/strings/en_EN.json | 3 ++- 6 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/Registration.js b/src/Registration.js index f86c9cc618..8364fc7cdb 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -26,6 +26,10 @@ import MatrixClientPeg from './MatrixClientPeg'; import Modal from './Modal'; import { _t } from './languageHandler'; +// Regex for what a "safe" or "Matrix-looking" localpart would be. +// TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514 +export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/g; + /** * Starts either the ILAG or full registration flow, depending * on what the HS supports diff --git a/src/components/structures/login/Registration.js b/src/components/structures/login/Registration.js index 98fd61370d..ad3ea5f19c 100644 --- a/src/components/structures/login/Registration.js +++ b/src/components/structures/login/Registration.js @@ -342,7 +342,7 @@ module.exports = React.createClass({ errMsg = _t('A phone number is required to register on this homeserver.'); break; case "RegistrationForm.ERR_USERNAME_INVALID": - errMsg = _t('User names may only contain letters, numbers, dots, hyphens and underscores.'); + errMsg = _t("Only use lower case letters, numbers and '=_-./'"); break; case "RegistrationForm.ERR_USERNAME_BLANK": errMsg = _t('You need to enter a user name.'); diff --git a/src/components/views/dialogs/SetMxIdDialog.js b/src/components/views/dialogs/SetMxIdDialog.js index fb892c4a0a..222a2c35fe 100644 --- a/src/components/views/dialogs/SetMxIdDialog.js +++ b/src/components/views/dialogs/SetMxIdDialog.js @@ -23,6 +23,7 @@ import MatrixClientPeg from '../../../MatrixClientPeg'; import classnames from 'classnames'; import { KeyCode } from '../../../Keyboard'; import { _t } from '../../../languageHandler'; +import { SAFE_LOCALPART_REGEX } from '../../../Registration'; // The amount of time to wait for further changes to the input username before // sending a request to the server @@ -110,12 +111,11 @@ export default React.createClass({ }, _doUsernameCheck: function() { - // XXX: SPEC-1 - // Check if username is valid - // Naive impl copied from https://github.com/matrix-org/matrix-react-sdk/blob/66c3a6d9ca695780eb6b662e242e88323053ff33/src/components/views/login/RegistrationForm.js#L190 - if (encodeURIComponent(this.state.username) !== this.state.username) { + // We do a quick check ahead of the username availability API to ensure the + // user ID roughly looks okay from a Matrix perspective. + if (!SAFE_LOCALPART_REGEX.test(this.state.username)) { this.setState({ - usernameError: _t('User names may only contain letters, numbers, dots, hyphens and underscores.'), + usernameError: _t("Only use lower case letters, numbers and '=_-./'"), }); return Promise.reject(); } @@ -210,7 +210,6 @@ export default React.createClass({ render: function() { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const InteractiveAuth = sdk.getComponent('structures.InteractiveAuth'); - const Spinner = sdk.getComponent('elements.Spinner'); let auth; if (this.state.doingUIAuth) { @@ -230,9 +229,8 @@ export default React.createClass({ }); let usernameIndicator = null; - let usernameBusyIndicator = null; if (this.state.usernameBusy) { - usernameBusyIndicator = ; + usernameIndicator =
{_t("Checking...")}
; } else { const usernameAvailable = this.state.username && this.state.usernameCheckSupport && !this.state.usernameError; @@ -270,7 +268,6 @@ export default React.createClass({ size="30" className={inputClasses} /> - { usernameBusyIndicator }
{ usernameIndicator }

diff --git a/src/components/views/login/RegistrationForm.js b/src/components/views/login/RegistrationForm.js index fe977025ae..137aeada91 100644 --- a/src/components/views/login/RegistrationForm.js +++ b/src/components/views/login/RegistrationForm.js @@ -25,7 +25,7 @@ import { looksValid as phoneNumberLooksValid } from '../../../phonenumber'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; -import SettingsStore from "../../../settings/SettingsStore"; +import { SAFE_LOCALPART_REGEX } from '../../../Registration'; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_COUNTRY = 'field_phone_country'; @@ -194,9 +194,8 @@ module.exports = React.createClass({ } else this.markFieldValid(field_id, phoneNumberValid, "RegistrationForm.ERR_PHONE_NUMBER_INVALID"); break; case FIELD_USERNAME: - // XXX: SPEC-1 - var username = this.refs.username.value.trim(); - if (encodeURIComponent(username) != username) { + const username = this.refs.username.value.trim(); + if (!SAFE_LOCALPART_REGEX.test(username)) { this.markFieldValid( field_id, false, diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js index de5d3db625..f68670b2f9 100644 --- a/src/components/views/room_settings/AliasSettings.js +++ b/src/components/views/room_settings/AliasSettings.js @@ -130,7 +130,7 @@ module.exports = React.createClass({ }, isAliasValid: function(alias) { - // XXX: FIXME SPEC-1 + // XXX: FIXME https://github.com/matrix-org/matrix-doc/issues/668 return (alias.match(/^#([^\/:,]+?):(.+)$/) && encodeURI(alias) === alias); }, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 92fb545b4e..c3282c8bf8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -985,10 +985,11 @@ "Unable to verify email address.": "Unable to verify email address.", "This will allow you to reset your password and receive notifications.": "This will allow you to reset your password and receive notifications.", "Skip": "Skip", - "User names may only contain letters, numbers, dots, hyphens and underscores.": "User names may only contain letters, numbers, dots, hyphens and underscores.", + "Only use lower case letters, numbers and '=_-./'": "Only use lower case letters, numbers and '=_-./'", "Username not available": "Username not available", "Username invalid: %(errMessage)s": "Username invalid: %(errMessage)s", "An error occurred: %(error_string)s": "An error occurred: %(error_string)s", + "Checking...": "Checking...", "Username available": "Username available", "To get started, please pick a username!": "To get started, please pick a username!", "This will be your account name on the homeserver, or you can pick a different server.": "This will be your account name on the homeserver, or you can pick a different server.", From 576bfedfb54e5625bcda2b976b1ca62aadfb0e11 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 13 Dec 2018 22:00:06 -0700 Subject: [PATCH 13/21] Remove global flag Regular expression objects are stateful, and the global flag interferes badly with the expression. A valid call can cause it to act like a flip flop instead of a stateless test. --- src/Registration.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Registration.js b/src/Registration.js index 8364fc7cdb..98aee3ac83 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -28,7 +28,7 @@ import { _t } from './languageHandler'; // Regex for what a "safe" or "Matrix-looking" localpart would be. // TODO: Update as needed for https://github.com/matrix-org/matrix-doc/issues/1514 -export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/g; +export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/; /** * Starts either the ILAG or full registration flow, depending From d304c35b38e49a3a6b40c5d1ece74c650b51b08f Mon Sep 17 00:00:00 2001 From: Willem Mulder Date: Fri, 14 Dec 2018 14:26:35 +0100 Subject: [PATCH 14/21] Allow widgets to autoplay media This is useful for e.g. webcam streams in widgets. Signed-off-by: Willem Mulder --- src/components/views/elements/AppTile.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 23b24adbb4..e36561cc15 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -544,7 +544,7 @@ export default class AppTile extends React.Component { // Additional iframe feature pemissions // (see - https://sites.google.com/a/chromium.org/dev/Home/chromium-security/deprecating-permissions-in-cross-origin-iframes and https://wicg.github.io/feature-policy/) - const iframeFeatures = "microphone; camera; encrypted-media;"; + const iframeFeatures = "microphone; camera; encrypted-media; autoplay;"; const appTileBodyClass = 'mx_AppTileBody' + (this.props.miniMode ? '_mini ' : ' '); From 1c4621c98e7b09b14bc7da6aa0cf4fc4e075f2d5 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 00:26:25 +0000 Subject: [PATCH 15/21] Link to CONTRIBUTING from JS SDK The JS SDK's CONTRIBUTING file is a bit simpler to read. The Synapse version previously used includes mentions of Python lint tools that don't apply here. Signed-off-by: J. Ryan Stinnett --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 99025f0e0a..f7c8c8b1c5 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,4 +1,4 @@ Contributing code to The React SDK ================================== -matrix-react-sdk follows the same pattern as https://github.com/matrix-org/synapse/blob/master/CONTRIBUTING.rst +matrix-react-sdk follows the same pattern as https://github.com/matrix-org/matrix-js-sdk/blob/master/CONTRIBUTING.rst From c6da61f1de8bd992715a82184eb45cbe80399104 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 17 Dec 2018 18:47:33 -0700 Subject: [PATCH 16/21] Make sure to grab the InlineSpinner object --- src/components/structures/GroupView.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index e2fd15aa89..56e6575793 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -1077,6 +1077,7 @@ export default React.createClass({ }, _getJoinableNode: function() { + const InlineSpinner = sdk.getComponent('elements.InlineSpinner'); return this.state.editing ?

{ _t('Who can join this community?') } From 2b14f2af5c3f54b61a743968c696d2ffc989ef73 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 12:10:08 +0000 Subject: [PATCH 17/21] Clean up when new key backup version fails to backup If creating a new key backup version succeeds but backing up to it fails, delete the version to avoid surprises. In addition, this converts the creation of a new key backup to async / await style. Signed-off-by: J. Ryan Stinnett --- .../keybackup/CreateKeyBackupDialog.js | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js index 6b115b890f..0db9d0699b 100644 --- a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js +++ b/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js @@ -92,25 +92,33 @@ export default React.createClass({ }); }, - _createBackup: function() { + _createBackup: async function() { this.setState({ phase: PHASE_BACKINGUP, error: null, }); - this._createBackupPromise = MatrixClientPeg.get().createKeyBackupVersion( - this._keyBackupInfo, - ).then((info) => { - return MatrixClientPeg.get().backupAllGroupSessions(info.version); - }).then(() => { + let info; + try { + info = await MatrixClientPeg.get().createKeyBackupVersion( + this._keyBackupInfo, + ); + await MatrixClientPeg.get().backupAllGroupSessions(info.version); this.setState({ phase: PHASE_DONE, }); - }).catch(e => { + } catch (e) { console.log("Error creating key backup", e); + // TODO: If creating a version succeeds, but backup fails, should we + // delete the version, disable backup, or do nothing? If we just + // disable without deleting, we'll enable on next app reload since + // it is trusted. + if (info) { + MatrixClientPeg.get().deleteKeyBackupVersion(info.version); + } this.setState({ error: e, }); - }); + } }, _onCancel: function() { From acc2e98355fcde7b1cfee2b7e49bea088c02af5d Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Thu, 13 Dec 2018 15:55:48 +0000 Subject: [PATCH 18/21] Add New Recovery Method dialog Adds a New Recovery Method dialog which is shown when key backup fails because of a version mismatch / version not found error. The set up button in the dialog currently only marks a device as verified (via a verification prompt) instead of the eventual restore and cross-sign flow, since those pieces don't exist yet. Signed-off-by: J. Ryan Stinnett --- res/css/_common.scss | 2 +- res/css/_components.scss | 1 + .../keybackup/_NewRecoveryMethodDialog.scss | 41 +++++++ res/img/e2e/lock-warning.svg | 1 + .../keybackup/NewRecoveryMethodDialog.js | 110 ++++++++++++++++++ src/components/structures/MatrixChat.js | 5 + src/components/views/dialogs/BaseDialog.js | 3 +- .../views/settings/KeyBackupPanel.js | 5 +- src/i18n/strings/en_EN.json | 8 +- 9 files changed, 170 insertions(+), 6 deletions(-) create mode 100644 res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss create mode 100644 res/img/e2e/lock-warning.svg create mode 100644 src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js diff --git a/res/css/_common.scss b/res/css/_common.scss index 11e04f5dc0..97ae5412e1 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -34,7 +34,7 @@ body { -webkit-font-smoothing: subpixel-antialiased; } -div.error, div.warning { +.error, .warning { color: $warning-color; } diff --git a/res/css/_components.scss b/res/css/_components.scss index 579856f880..48aa211fd8 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -47,6 +47,7 @@ @import "./views/dialogs/_ShareDialog.scss"; @import "./views/dialogs/_UnknownDeviceDialog.scss"; @import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss"; +@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss"; @import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss"; @import "./views/directory/_NetworkDropdown.scss"; @import "./views/elements/_AccessibleButton.scss"; diff --git a/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss new file mode 100644 index 0000000000..370f82d9ab --- /dev/null +++ b/res/css/views/dialogs/keybackup/_NewRecoveryMethodDialog.scss @@ -0,0 +1,41 @@ +/* +Copyright 2018 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. +*/ + +.mx_NewRecoveryMethodDialog .mx_Dialog_title { + margin-bottom: 32px; +} + +.mx_NewRecoveryMethodDialog_title { + position: relative; + padding-left: 45px; + padding-bottom: 10px; + + &:before { + mask: url("../../../img/e2e/lock-warning.svg"); + mask-repeat: no-repeat; + background-color: $primary-fg-color; + content: ""; + position: absolute; + top: -6px; + right: 0; + bottom: 0; + left: 0; + } +} + +.mx_NewRecoveryMethodDialog .mx_Dialog_buttons { + margin-top: 36px; +} diff --git a/res/img/e2e/lock-warning.svg b/res/img/e2e/lock-warning.svg new file mode 100644 index 0000000000..a984ed85a0 --- /dev/null +++ b/res/img/e2e/lock-warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js new file mode 100644 index 0000000000..e88e0444bc --- /dev/null +++ b/src/async-components/views/dialogs/keybackup/NewRecoveryMethodDialog.js @@ -0,0 +1,110 @@ +/* +Copyright 2018 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 React from "react"; +import PropTypes from "prop-types"; +import sdk from "../../../../index"; +import MatrixClientPeg from '../../../../MatrixClientPeg'; +import dis from "../../../../dispatcher"; +import { _t } from "../../../../languageHandler"; +import Modal from "../../../../Modal"; + +export default class NewRecoveryMethodDialog extends React.PureComponent { + static propTypes = { + onFinished: PropTypes.func.isRequired, + } + + onGoToSettingsClick = () => { + this.props.onFinished(); + dis.dispatch({ action: 'view_user_settings' }); + } + + onSetupClick = async() => { + // TODO: Should change to a restore key backup flow that checks the + // recovery passphrase while at the same time also cross-signing the + // device as well in a single flow. Since we don't have that yet, we'll + // look for an unverified device and verify it. Note that this means + // we won't restore keys yet; instead we'll only trust the backup for + // sending our own new keys to it. + let backupSigStatus; + try { + const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion(); + backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo); + } catch (e) { + console.log("Unable to fetch key backup status", e); + return; + } + + let unverifiedDevice; + for (const sig of backupSigStatus.sigs) { + if (!sig.device.isVerified()) { + unverifiedDevice = sig.device; + break; + } + } + if (!unverifiedDevice) { + console.log("Unable to find a device to verify."); + return; + } + + const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog'); + Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, { + userId: MatrixClientPeg.get().credentials.userId, + device: unverifiedDevice, + onFinished: this.props.onFinished, + }); + } + + render() { + const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog"); + const DialogButtons = sdk.getComponent("views.elements.DialogButtons"); + const title = + {_t("New Recovery Method")} + ; + + return ( + +
+

{_t( + "A new recovery passphrase and key for Secure " + + "Messages has been detected.", + )}

+

{_t( + "Setting up Secure Messages on this device " + + "will re-encrypt this device's message history with " + + "the new recovery method.", + )}

+

{_t( + "If you didn't set the new recovery method, an " + + "attacker may be trying to access your account. " + + "Change your account password and set a new recovery " + + "method immediately in Settings.", + )}

+ +
+
+ ); + } +} diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4517304453..fd95276445 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -1430,6 +1430,11 @@ export default React.createClass({ break; } }); + cli.on("crypto.keyBackupFailed", () => { + Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method', + import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'), + ); + }); // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user diff --git a/src/components/views/dialogs/BaseDialog.js b/src/components/views/dialogs/BaseDialog.js index 8ec417a59b..3e9052cc34 100644 --- a/src/components/views/dialogs/BaseDialog.js +++ b/src/components/views/dialogs/BaseDialog.js @@ -57,8 +57,7 @@ export default React.createClass({ className: PropTypes.string, // Title for the dialog. - // (could probably actually be something more complicated than a string if desired) - title: PropTypes.string.isRequired, + title: PropTypes.node.isRequired, // children should be the content of the dialog children: PropTypes.node, diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index b08f4d0e78..03b98d28a0 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -154,6 +154,7 @@ export default class KeyBackupPanel extends React.Component { } let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => { + const deviceName = sig.device.getDisplayName() || sig.device.deviceId; const sigStatusSubstitutions = { validity: sub => @@ -163,7 +164,7 @@ export default class KeyBackupPanel extends React.Component { {sub} , - device: sub => {sig.device.getDisplayName()}, + device: sub => {deviceName}, }; let sigStatus; if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) { @@ -174,7 +175,7 @@ export default class KeyBackupPanel extends React.Component { } else if (sig.valid && sig.device.isVerified()) { sigStatus = _t( "Backup has a valid signature from " + - "verified device x", + "verified device ", {}, sigStatusSubstitutions, ); } else if (sig.valid && !sig.device.isVerified()) { diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 00f781ea5b..b5a5762cc4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -351,7 +351,7 @@ "This device is uploading keys to this backup": "This device is uploading keys to this backup", "This device is not uploading keys to this backup": "This device is not uploading keys to this backup", "Backup has a valid signature from this device": "Backup has a valid signature from this device", - "Backup has a valid signature from verified device x": "Backup has a valid signature from verified device x", + "Backup has a valid signature from verified device ": "Backup has a valid signature from verified device ", "Backup has a valid signature from unverified device ": "Backup has a valid signature from unverified device ", "Backup has an invalid signature from verified device ": "Backup has an invalid signature from verified device ", "Backup has an invalid signature from unverified device ": "Backup has an invalid signature from unverified device ", @@ -1401,6 +1401,12 @@ "Retry": "Retry", "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.", "If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.", + "New Recovery Method": "New Recovery Method", + "A new recovery passphrase and key for Secure Messages has been detected.": "A new recovery passphrase and key for Secure Messages has been detected.", + "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.", + "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.", + "Set up Secure Messages": "Set up Secure Messages", + "Go to Settings": "Go to Settings", "Failed to set direct chat tag": "Failed to set direct chat tag", "Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room", "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" From b036e59021d95163c1816346f4a1014ae7667927 Mon Sep 17 00:00:00 2001 From: "J. Ryan Stinnett" Date: Tue, 18 Dec 2018 15:14:55 +0000 Subject: [PATCH 19/21] Enable ESLint rule to require defined components in JSX Signed-off-by: J. Ryan Stinnett --- .eslintrc.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.eslintrc.js b/.eslintrc.js index 62d24ea707..971809f851 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -47,6 +47,9 @@ module.exports = { }], "react/jsx-key": ["error"], + // Components in JSX should always be defined. + "react/jsx-no-undef": "error", + // Assert no spacing in JSX curly brackets // // From 37b3644fd91e49a8d39b779bd9c1ade4286ee450 Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Dec 2018 17:40:30 +0000 Subject: [PATCH 20/21] React-sdk changes to support sandboxed electron --- src/BasePlatform.js | 6 +-- src/components/structures/UserSettings.js | 45 ++++++++++++----------- src/components/views/elements/AppTile.js | 34 ----------------- 3 files changed, 25 insertions(+), 60 deletions(-) diff --git a/src/BasePlatform.js b/src/BasePlatform.js index abc9aa0bed..79f0d69e2c 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -3,6 +3,7 @@ /* Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd +Copyright 2018 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. @@ -105,11 +106,6 @@ export default class BasePlatform { return "Not implemented"; } - isElectron(): boolean { return false; } - - setupScreenSharingForIframe() { - } - /** * Restarts the application, without neccessarily reloading * any application code diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 6f932d71e1..b9dbe345c5 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -188,9 +188,11 @@ module.exports = React.createClass({ phase: "UserSettings.LOADING", // LOADING, DISPLAY email_add_pending: false, vectorVersion: undefined, + canSelfUpdate: null, rejectingInvites: false, mediaDevices: null, ignoredUsers: [], + autoLaunchEnabled: null, }; }, @@ -209,6 +211,13 @@ module.exports = React.createClass({ }, (e) => { console.log("Failed to fetch app version", e); }); + + PlatformPeg.get().canSelfUpdate().then((canUpdate) => { + if (this._unmounted) return; + this.setState({ + canSelfUpdate: canUpdate, + }); + }); } this._refreshMediaDevices(); @@ -227,11 +236,12 @@ module.exports = React.createClass({ }); this._refreshFromServer(); - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - - ipcRenderer.on('settings', this._electronSettings); - ipcRenderer.send('settings_get'); + if (PlatformPeg.get().supportsAutoLaunch()) { + PlatformPeg.get().getAutoLaunchEnabled().then(enabled => { + this.setState({ + autoLaunchEnabled: enabled, + }); + }); } this.setState({ @@ -262,11 +272,6 @@ module.exports = React.createClass({ if (cli) { cli.removeListener("RoomMember.membership", this._onInviteStateChange); } - - if (PlatformPeg.get().isElectron()) { - const {ipcRenderer} = require('electron'); - ipcRenderer.removeListener('settings', this._electronSettings); - } }, // `UserSettings` assumes that the client peg will not be null, so give it some @@ -285,10 +290,6 @@ module.exports = React.createClass({ }); }, - _electronSettings: function(ev, settings) { - this.setState({ electron_settings: settings }); - }, - _refreshMediaDevices: function(stream) { if (stream) { // kill stream so that we don't leave it lingering around with webcam enabled etc @@ -967,7 +968,7 @@ module.exports = React.createClass({ _renderCheckUpdate: function() { const platform = PlatformPeg.get(); - if ('canSelfUpdate' in platform && platform.canSelfUpdate() && 'startUpdateCheck' in platform) { + if (this.state.canSelfUpdate) { return

{ _t('Updates') }

@@ -1012,8 +1013,7 @@ module.exports = React.createClass({ }, _renderElectronSettings: function() { - const settings = this.state.electron_settings; - if (!settings) return; + if (!PlatformPeg.get().supportsAutoLaunch()) return; // TODO: This should probably be a granular setting, but it only applies to electron // and ends up being get/set outside of matrix anyways (local system setting). @@ -1023,7 +1023,7 @@ module.exports = React.createClass({
@@ -1033,8 +1033,11 @@ module.exports = React.createClass({ }, _onAutoLaunchChanged: function(e) { - const {ipcRenderer} = require('electron'); - ipcRenderer.send('settings_set', 'auto-launch', e.target.checked); + PlatformPeg.get().setAutoLaunchEnabled(e.target.checked).then(() => { + this.setState({ + autoLaunchEnabled: e.target.checked, + }); + }); }, _mapWebRtcDevicesToSpans: function(devices) { @@ -1393,7 +1396,7 @@ module.exports = React.createClass({ { this._renderBulkOptions() } { this._renderBugReport() } - { PlatformPeg.get().isElectron() && this._renderElectronSettings() } + { this._renderElectronSettings() } { this._renderAnalyticsControl() } diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 23b24adbb4..7eae17ace8 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -49,7 +49,6 @@ export default class AppTile extends React.Component { this.state = this._getNewState(props); this._onAction = this._onAction.bind(this); - this._onMessage = this._onMessage.bind(this); this._onLoaded = this._onLoaded.bind(this); this._onEditClick = this._onEditClick.bind(this); this._onDeleteClick = this._onDeleteClick.bind(this); @@ -143,10 +142,6 @@ export default class AppTile extends React.Component { } componentDidMount() { - // Legacy Jitsi widget messaging -- TODO replace this with standard widget - // postMessaging API - window.addEventListener('message', this._onMessage, false); - // Widget action listeners this.dispatcherRef = dis.register(this._onAction); } @@ -155,9 +150,6 @@ export default class AppTile extends React.Component { // Widget action listeners dis.unregister(this.dispatcherRef); - // Jitsi listener - window.removeEventListener('message', this._onMessage); - // if it's not remaining on screen, get rid of the PersistedElement container if (!ActiveWidgetStore.getWidgetPersistence(this.props.id)) { ActiveWidgetStore.destroyPersistentWidget(); @@ -233,32 +225,6 @@ export default class AppTile extends React.Component { } } - // Legacy Jitsi widget messaging - // TODO -- This should be replaced with the new widget postMessaging API - _onMessage(event) { - if (this.props.type !== 'jitsi') { - return; - } - if (!event.origin) { - event.origin = event.originalEvent.origin; - } - - const widgetUrlObj = url.parse(this.state.widgetUrl); - const eventOrigin = url.parse(event.origin); - if ( - eventOrigin.protocol !== widgetUrlObj.protocol || - eventOrigin.host !== widgetUrlObj.host - ) { - return; - } - - if (event.data.widgetAction === 'jitsi_iframe_loaded') { - const iframe = this.refs.appFrame.contentWindow - .document.querySelector('iframe[id^="jitsiConferenceFrame"]'); - PlatformPeg.get().setupScreenSharingForIframe(iframe); - } - } - _canUserModify() { // User widgets should always be modifiable by their creator if (this.props.userWidget && MatrixClientPeg.get().credentials.userId === this.props.creatorUserId) { From 3a8b9ab8501aed59808146f11dd802b72f7398de Mon Sep 17 00:00:00 2001 From: David Baker Date: Tue, 18 Dec 2018 17:57:23 +0000 Subject: [PATCH 21/21] unused import --- src/components/views/elements/AppTile.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/AppTile.js b/src/components/views/elements/AppTile.js index 7eae17ace8..1ce32c852c 100644 --- a/src/components/views/elements/AppTile.js +++ b/src/components/views/elements/AppTile.js @@ -22,7 +22,6 @@ import qs from 'querystring'; import React from 'react'; import PropTypes from 'prop-types'; import MatrixClientPeg from '../../../MatrixClientPeg'; -import PlatformPeg from '../../../PlatformPeg'; import ScalarAuthClient from '../../../ScalarAuthClient'; import WidgetMessaging from '../../../WidgetMessaging'; import TintableSvgButton from './TintableSvgButton';