From ce24165e19a1b61f1447306b68d35c569d72de2f Mon Sep 17 00:00:00 2001 From: Matthew Hodgson Date: Fri, 8 Feb 2019 16:44:03 +0000 Subject: [PATCH 0001/1310] port over low_bandwidth mode to develop --- src/Lifecycle.js | 5 ++++- src/MatrixClientPeg.js | 2 +- src/components/structures/TimelinePanel.js | 2 ++ src/components/structures/UserSettings.js | 9 ++++++++- src/components/views/avatars/BaseAvatar.js | 11 ++++++++--- src/components/views/rooms/MessageComposerInput.js | 1 + src/i18n/strings/en_EN.json | 1 + src/settings/Settings.js | 5 +++++ 8 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 54ac605c65..2ea8aa190a 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -31,6 +31,7 @@ import Modal from './Modal'; import sdk from './index'; import ActiveWidgetStore from './stores/ActiveWidgetStore'; import PlatformPeg from "./PlatformPeg"; +import SettingsStore from "./settings/SettingsStore"; import {sendLoginRequest} from "./Login"; /** @@ -440,7 +441,9 @@ async function startMatrixClient() { Notifier.start(); UserActivity.start(); - Presence.start(); + if (!SettingsStore.getValue("lowBandwidth")) { + Presence.start(); + } DMRoomMap.makeShared().start(); ActiveWidgetStore.start(); diff --git a/src/MatrixClientPeg.js b/src/MatrixClientPeg.js index 882a913452..12301e3716 100644 --- a/src/MatrixClientPeg.js +++ b/src/MatrixClientPeg.js @@ -115,7 +115,7 @@ class MatrixClientPeg { // try to initialise e2e on the new client try { // check that we have a version of the js-sdk which includes initCrypto - if (this.matrixClient.initCrypto) { + if (!SettingsStore.getValue("lowBandwidth") && this.matrixClient.initCrypto) { await this.matrixClient.initCrypto(); } } catch (e) { diff --git a/src/components/structures/TimelinePanel.js b/src/components/structures/TimelinePanel.js index 9fe83c2c2d..3bdcf542b1 100644 --- a/src/components/structures/TimelinePanel.js +++ b/src/components/structures/TimelinePanel.js @@ -584,6 +584,8 @@ var TimelinePanel = React.createClass({ }, sendReadReceipt: function() { + if (SettingsStore.getValue("lowBandwidth")) return; + if (!this.refs.messagePanel) return; if (!this.props.manageReadReceipts) return; // This happens on user_activity_end which is delayed, and it's diff --git a/src/components/structures/UserSettings.js b/src/components/structures/UserSettings.js index 809b06c3d6..0aa564cc85 100644 --- a/src/components/structures/UserSettings.js +++ b/src/components/structures/UserSettings.js @@ -84,6 +84,13 @@ const SIMPLE_SETTINGS = [ { id: "enableWidgetScreenshots" }, { id: "pinMentionedRooms" }, { id: "pinUnreadRooms" }, + { + id: "lowBandwidth", + fn: () => { + PlatformPeg.get().reload(); + }, + level: SettingLevel.DEVICE, + }, { id: "showDeveloperTools" }, { id: "promptBeforeInviteUnknownUsers" }, ]; @@ -644,7 +651,7 @@ module.exports = React.createClass({
); diff --git a/src/components/views/avatars/BaseAvatar.js b/src/components/views/avatars/BaseAvatar.js index 47de7c9dc4..7241e7cd4c 100644 --- a/src/components/views/avatars/BaseAvatar.js +++ b/src/components/views/avatars/BaseAvatar.js @@ -20,6 +20,7 @@ import PropTypes from 'prop-types'; import { MatrixClient } from 'matrix-js-sdk'; import AvatarLogic from '../../../Avatar'; import sdk from '../../../index'; +import SettingsStore from "../../../settings/SettingsStore"; import AccessibleButton from '../elements/AccessibleButton'; module.exports = React.createClass({ @@ -104,9 +105,13 @@ module.exports = React.createClass({ // work out the full set of urls to try to load. This is formed like so: // imageUrls: [ props.url, props.urls, default image ] - const urls = props.urls || []; - if (props.url) { - urls.unshift(props.url); // put in urls[0] + let urls = []; + if (!SettingsStore.getValue("lowBandwidth")) { + urls = props.urls || []; + + if (props.url) { + urls.unshift(props.url); // put in urls[0] + } } let defaultImageUrl = null; diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index ab89e402ae..0e9442edb9 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -487,6 +487,7 @@ export default class MessageComposerInput extends React.Component { sendTyping(isTyping) { if (!SettingsStore.getValue('sendTypingNotifications')) return; + if (SettingsStore.getValue('lowBandwidth')) return; MatrixClientPeg.get().sendTyping( this.props.room.roomId, this.isTyping, TYPING_SERVER_TIMEOUT, diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 623dd92613..e29be2da45 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -300,6 +300,7 @@ "Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets", "Prompt before sending invites to potentially invalid matrix IDs": "Prompt before sending invites to potentially invalid matrix IDs", "Show developer tools": "Show developer tools", + "Low Bandwidth Mode": "Low Bandwidth Mode", "Collecting app version information": "Collecting app version information", "Collecting logs": "Collecting logs", "Uploading report": "Uploading report", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index f21e94ea4a..177a90ef5d 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -362,4 +362,9 @@ export const SETTINGS = { displayName: _td('Show developer tools'), default: false, }, + "lowBandwidth": { + supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, + displayName: _td('Low Bandwidth Mode'), + default: false, + }, }; From f14d96e06994b757232f8a58342d2cb548a82180 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 19 Apr 2019 12:36:32 +0100 Subject: [PATCH 0002/1310] Look in room account data for custom notif sounds --- src/Notifier.js | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 6a4f9827f7..85807976eb 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -96,11 +96,46 @@ const Notifier = { } }, - _playAudioNotification: function(ev, room) { - const e = document.getElementById("messageAudio"); - if (e) { - e.play(); + _getSoundForRoom: async function(room) { + // We do no caching here because the SDK caches the event content + // and the browser will cache the sound. + const ev = await room.getAccountData("uk.half-shot.notification.sound"); + if (!ev) { + return null; } + let url = ev.getContent().url; + if (!url) { + console.warn(`${room.roomId} has custom notification sound event, but no url key`); + return null; + } + url = MatrixClientPeg.get().mxcUrlToHttp(url); + this.notifSoundsByRoom.set(room.roomId, url); + return url; + }, + + _playAudioNotification: function(ev, room) { + _getSoundForRoom(room).then((soundUrl) => { + console.log(`Got sound ${soundUrl || "default"} for ${room.roomId}`); + // XXX: How do we ensure this is a sound file and not + // going to be exploited? + const selector = document.querySelector(`audio source[src='${soundUrl}']`) || "#messageAudio"; + let audioElement = null; + if (!selector) { + if (!soundUrl) { + console.error("Tried to play alert sound but missing #messageAudio") + return + } + audioElement = new HTMLAudioElement(); + let sourceElement = new HTMLSourceElement(); + // XXX: type + sourceElement.src = soundUrl; + audioElement.appendChild(sourceElement); + document.appendChild(audioElement); + } else { + audioElement = selector.parentNode; + } + audioElement.play(); + }); }, start: function() { From b0bacdba1514c08e2ab0b4dd17a76ebcd189050b Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 19 Apr 2019 13:21:58 +0100 Subject: [PATCH 0003/1310] Simplyify --- src/Notifier.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 85807976eb..2caf611654 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -109,30 +109,23 @@ const Notifier = { return null; } url = MatrixClientPeg.get().mxcUrlToHttp(url); - this.notifSoundsByRoom.set(room.roomId, url); return url; }, _playAudioNotification: function(ev, room) { - _getSoundForRoom(room).then((soundUrl) => { + this._getSoundForRoom(room).then((soundUrl) => { console.log(`Got sound ${soundUrl || "default"} for ${room.roomId}`); // XXX: How do we ensure this is a sound file and not // going to be exploited? - const selector = document.querySelector(`audio source[src='${soundUrl}']`) || "#messageAudio"; - let audioElement = null; + const selector = document.querySelector(soundUrl ? `audio[src='${soundUrl}']` : "#messageAudio"); + let audioElement = selector; if (!selector) { if (!soundUrl) { console.error("Tried to play alert sound but missing #messageAudio") return } - audioElement = new HTMLAudioElement(); - let sourceElement = new HTMLSourceElement(); - // XXX: type - sourceElement.src = soundUrl; - audioElement.appendChild(sourceElement); - document.appendChild(audioElement); - } else { - audioElement = selector.parentNode; + audioElement = new Audio(soundUrl); + document.body.appendChild(audioElement); } audioElement.play(); }); From cd8647188fbd91815b21bf54031f2c16c4baa040 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 19 Apr 2019 14:10:10 +0100 Subject: [PATCH 0004/1310] Support account level custom sounds too --- src/Notifier.js | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 2caf611654..e8d0c27a31 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -99,36 +99,47 @@ const Notifier = { _getSoundForRoom: async function(room) { // We do no caching here because the SDK caches the event content // and the browser will cache the sound. - const ev = await room.getAccountData("uk.half-shot.notification.sound"); + let ev = await room.getAccountData("uk.half-shot.notification.sound"); if (!ev) { - return null; + // Check the account data. + ev = await MatrixClientPeg.get().getAccountData("uk.half-shot.notification.sound"); + if (!ev) { + return null; + } } - let url = ev.getContent().url; - if (!url) { + const content = ev.getContent(); + if (!content.url) { console.warn(`${room.roomId} has custom notification sound event, but no url key`); return null; } - url = MatrixClientPeg.get().mxcUrlToHttp(url); - return url; + return { + url: MatrixClientPeg.get().mxcUrlToHttp(content.url), + type: content.type, + }; }, _playAudioNotification: function(ev, room) { - this._getSoundForRoom(room).then((soundUrl) => { - console.log(`Got sound ${soundUrl || "default"} for ${room.roomId}`); + this._getSoundForRoom(room).then((sound) => { + console.log(`Got sound ${sound || "default"} for ${room.roomId}`); // XXX: How do we ensure this is a sound file and not // going to be exploited? - const selector = document.querySelector(soundUrl ? `audio[src='${soundUrl}']` : "#messageAudio"); + const selector = document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); let audioElement = selector; if (!selector) { - if (!soundUrl) { + if (!sound) { console.error("Tried to play alert sound but missing #messageAudio") - return + return; + } + audioElement = new Audio(sound.url); + if (sound.type) { + audioElement.type = sound.type; } - audioElement = new Audio(soundUrl); document.body.appendChild(audioElement); } audioElement.play(); - }); + }).catch((ex) => { + console.warn("Caught error when trying to fetch room notification sound:", ex); + }) }, start: function() { From 63ab7736ca4ab0ee4cfccc8f7b0957b2709e8d2f Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 19 Apr 2019 16:27:30 +0100 Subject: [PATCH 0005/1310] Add a fancy room tab and uploader --- src/Notifier.js | 19 ++- .../views/dialogs/RoomSettingsDialog.js | 8 +- .../tabs/room/NotificationSettingsTab.js | 139 ++++++++++++++++++ src/i18n/strings/en_EN.json | 6 +- 4 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 src/components/views/settings/tabs/room/NotificationSettingsTab.js diff --git a/src/Notifier.js b/src/Notifier.js index e8d0c27a31..041b91f4b2 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -96,11 +96,19 @@ const Notifier = { } }, - _getSoundForRoom: async function(room) { + setRoomSound: function(room, soundData) { + return MatrixClientPeg.get().setRoomAccountData(room.roomId, "uk.half-shot.notification.sound", soundData); + }, + + clearRoomSound: function(room) { + return room.setAccountData("uk.half-shot.notification.sound", null); + }, + + getSoundForRoom: async function(room) { // We do no caching here because the SDK caches the event content // and the browser will cache the sound. let ev = await room.getAccountData("uk.half-shot.notification.sound"); - if (!ev) { + if (!ev || !ev.getContent()) { // Check the account data. ev = await MatrixClientPeg.get().getAccountData("uk.half-shot.notification.sound"); if (!ev) { @@ -112,15 +120,18 @@ const Notifier = { console.warn(`${room.roomId} has custom notification sound event, but no url key`); return null; } + return { url: MatrixClientPeg.get().mxcUrlToHttp(content.url), + name: content.name, type: content.type, + size: content.size, }; }, _playAudioNotification: function(ev, room) { - this._getSoundForRoom(room).then((sound) => { - console.log(`Got sound ${sound || "default"} for ${room.roomId}`); + this.getSoundForRoom(room).then((sound) => { + console.log(`Got sound ${sound.name || "default"} for ${room.roomId}`); // XXX: How do we ensure this is a sound file and not // going to be exploited? const selector = document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 05ed262078..733c5002f5 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -22,7 +22,8 @@ import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsT import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab"; import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab"; import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab"; -import sdk from "../../../index"; +import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab"; +import sdk from "../../../index";RolesRoomSettingsTab import MatrixClientPeg from "../../../MatrixClientPeg"; export default class RoomSettingsDialog extends React.Component { @@ -49,6 +50,11 @@ export default class RoomSettingsDialog extends React.Component { "mx_RoomSettingsDialog_rolesIcon", , )); + tabs.push(new Tab( + _td("Notifications"), + "mx_RoomSettingsDialog_rolesIcon", + , + )) tabs.push(new Tab( _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js new file mode 100644 index 0000000000..35a223a1d8 --- /dev/null +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -0,0 +1,139 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import PropTypes from 'prop-types'; +import {_t} from "../../../../../languageHandler"; +import MatrixClientPeg from "../../../../../MatrixClientPeg"; +import sdk from "../../../../.."; +import AccessibleButton from "../../../elements/AccessibleButton"; +import Modal from "../../../../../Modal"; +import dis from "../../../../../dispatcher"; +import Notifier from "../../../../../Notifier"; + +export default class NotificationsSettingsTab extends React.Component { + static propTypes = { + roomId: PropTypes.string.isRequired, + closeSettingsFn: PropTypes.func.isRequired, + }; + + constructor() { + super(); + + this.state = { + currentSound: "default", + uploadedFile: null, + }; + } + + componentWillMount() { + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + Notifier.getSoundForRoom(room).then((soundData) => { + if (!soundData) { + return; + } + this.setState({currentSound: soundData.name || soundData.url}) + }) + } + + _onSoundUploadChanged(e) { + if (!e.target.files || !e.target.files.length) { + this.setState({ + uploadedFile: null, + }); + return; + } + + const file = e.target.files[0]; + this.setState({ + uploadedFile: file, + }); + } + + async _saveSound (e) { + e.stopPropagation(); + e.preventDefault(); + if (!this.state.uploadedFile) { + return; + } + let type = this.state.uploadedFile.type; + if (type === "video/ogg") { + // XXX: I've observed browsers allowing users to pick a audio/ogg files, + // and then calling it a video/ogg. This is a lame hack, but man browsers + // suck at detecting mimetypes. + type = "audio/ogg"; + } + const url = await MatrixClientPeg.get().uploadContent( + this.state.uploadedFile, { + type, + }, + ); + + const room = MatrixClientPeg.get().getRoom(this.props.roomId); + + await Notifier.setRoomSound(room, { + name: this.state.uploadedFile.name, + type: type, + size: this.state.uploadedFile.size, + url, + }); + + this.setState({ + uploadedFile: null, + uploadedFileUrl: null, + currentSound: this.state.uploadedFile.name, + }); + } + + _clearSound (e) { + e.stopPropagation(); + e.preventDefault(); + const room = client.getRoom(this.props.roomId); + Notifier.clearRoomSound(room); + + this.setState({ + currentSound: "default", + }); + } + + render() { + const client = MatrixClientPeg.get(); + + return ( +
+
{_t("Notifications")}
+
+ {_t("Sounds")} +
+ {_t("Notification sound")}: {this.state.currentSound} +
+
+

{_t("Set a new custom sound")}

+
+ + + {_t("Save")} + +
+ + {_t("Reset to default sound")} + +
+
+
+ ); + } +} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2b50fd9ad3..7f94bdc9cd 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1614,5 +1614,9 @@ "If you didn't remove the 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 remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in 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" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room", + "Sounds": "Sounds", + "Notification sound": "Notification sound", + "Set a new custom sound": "Set a new custom sound", + "Reset to default sound": "Reset to default sound" } From d33df45c5e0162a6f681c5007e8b6fc5dc60012f Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 19 Apr 2019 21:42:18 +0100 Subject: [PATCH 0006/1310] Linting --- src/Notifier.js | 6 +++--- .../views/dialogs/RoomSettingsDialog.js | 4 ++-- .../settings/tabs/room/NotificationSettingsTab.js | 15 +++++---------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 041b91f4b2..43d599ae0d 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -120,7 +120,7 @@ const Notifier = { console.warn(`${room.roomId} has custom notification sound event, but no url key`); return null; } - + return { url: MatrixClientPeg.get().mxcUrlToHttp(content.url), name: content.name, @@ -138,7 +138,7 @@ const Notifier = { let audioElement = selector; if (!selector) { if (!sound) { - console.error("Tried to play alert sound but missing #messageAudio") + console.error("Tried to play alert sound but missing #messageAudio"); return; } audioElement = new Audio(sound.url); @@ -150,7 +150,7 @@ const Notifier = { audioElement.play(); }).catch((ex) => { console.warn("Caught error when trying to fetch room notification sound:", ex); - }) + }); }, start: function() { diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index 733c5002f5..caed958003 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -23,7 +23,7 @@ import RolesRoomSettingsTab from "../settings/tabs/room/RolesRoomSettingsTab"; import GeneralRoomSettingsTab from "../settings/tabs/room/GeneralRoomSettingsTab"; import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsTab"; import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab"; -import sdk from "../../../index";RolesRoomSettingsTab +import sdk from "../../../index"; import MatrixClientPeg from "../../../MatrixClientPeg"; export default class RoomSettingsDialog extends React.Component { @@ -54,7 +54,7 @@ export default class RoomSettingsDialog extends React.Component { _td("Notifications"), "mx_RoomSettingsDialog_rolesIcon", , - )) + )); tabs.push(new Tab( _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index 35a223a1d8..a911ec3e4f 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -18,10 +18,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import {_t} from "../../../../../languageHandler"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; -import sdk from "../../../../.."; import AccessibleButton from "../../../elements/AccessibleButton"; -import Modal from "../../../../../Modal"; -import dis from "../../../../../dispatcher"; import Notifier from "../../../../../Notifier"; export default class NotificationsSettingsTab extends React.Component { @@ -45,8 +42,8 @@ export default class NotificationsSettingsTab extends React.Component { if (!soundData) { return; } - this.setState({currentSound: soundData.name || soundData.url}) - }) + this.setState({currentSound: soundData.name || soundData.url}); + }); } _onSoundUploadChanged(e) { @@ -63,7 +60,7 @@ export default class NotificationsSettingsTab extends React.Component { }); } - async _saveSound (e) { + async _saveSound(e) { e.stopPropagation(); e.preventDefault(); if (!this.state.uploadedFile) { @@ -98,10 +95,10 @@ export default class NotificationsSettingsTab extends React.Component { }); } - _clearSound (e) { + _clearSound(e) { e.stopPropagation(); e.preventDefault(); - const room = client.getRoom(this.props.roomId); + const room = MatrixClientPeg.get().getRoom(this.props.roomId); Notifier.clearRoomSound(room); this.setState({ @@ -110,8 +107,6 @@ export default class NotificationsSettingsTab extends React.Component { } render() { - const client = MatrixClientPeg.get(); - return (
{_t("Notifications")}
From 776210c135f4904116c0f57177a9bd9514b11b6a Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Fri, 19 Apr 2019 22:31:51 +0100 Subject: [PATCH 0007/1310] Use settings store --- src/Notifier.js | 25 ++++++------------ .../tabs/room/NotificationSettingsTab.js | 26 ++++++++++--------- src/settings/Settings.js | 5 ++++ 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index 43d599ae0d..d5810fe30d 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -96,28 +96,19 @@ const Notifier = { } }, - setRoomSound: function(room, soundData) { - return MatrixClientPeg.get().setRoomAccountData(room.roomId, "uk.half-shot.notification.sound", soundData); - }, - - clearRoomSound: function(room) { - return room.setAccountData("uk.half-shot.notification.sound", null); - }, - - getSoundForRoom: async function(room) { + getSoundForRoom: async function(roomId) { // We do no caching here because the SDK caches the event content // and the browser will cache the sound. - let ev = await room.getAccountData("uk.half-shot.notification.sound"); - if (!ev || !ev.getContent()) { - // Check the account data. - ev = await MatrixClientPeg.get().getAccountData("uk.half-shot.notification.sound"); - if (!ev) { + let content = SettingsStore.getValue("notificationSound", roomId); + if (!content) { + content = SettingsStore.getValue("notificationSound"); + if (!content) { return null; } } - const content = ev.getContent(); + if (!content.url) { - console.warn(`${room.roomId} has custom notification sound event, but no url key`); + console.warn(`${roomId} has custom notification sound event, but no url key`); return null; } @@ -130,7 +121,7 @@ const Notifier = { }, _playAudioNotification: function(ev, room) { - this.getSoundForRoom(room).then((sound) => { + this.getSoundForRoom(room.roomId).then((sound) => { console.log(`Got sound ${sound.name || "default"} for ${room.roomId}`); // XXX: How do we ensure this is a sound file and not // going to be exploited? diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index a911ec3e4f..062d54988a 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -20,6 +20,7 @@ import {_t} from "../../../../../languageHandler"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; import Notifier from "../../../../../Notifier"; +import SettingsStore from '../../../../../settings/SettingsStore'; export default class NotificationsSettingsTab extends React.Component { static propTypes = { @@ -37,8 +38,7 @@ export default class NotificationsSettingsTab extends React.Component { } componentWillMount() { - const room = MatrixClientPeg.get().getRoom(this.props.roomId); - Notifier.getSoundForRoom(room).then((soundData) => { + Notifier.getSoundForRoom(this.props.roomId).then((soundData) => { if (!soundData) { return; } @@ -79,14 +79,17 @@ export default class NotificationsSettingsTab extends React.Component { }, ); - const room = MatrixClientPeg.get().getRoom(this.props.roomId); - - await Notifier.setRoomSound(room, { - name: this.state.uploadedFile.name, - type: type, - size: this.state.uploadedFile.size, - url, - }); + await SettingsStore.setValue( + "notificationSound", + this.props.roomId, + "room-account", + { + name: this.state.uploadedFile.name, + type: type, + size: this.state.uploadedFile.size, + url, + }, + ); this.setState({ uploadedFile: null, @@ -98,8 +101,7 @@ export default class NotificationsSettingsTab extends React.Component { _clearSound(e) { e.stopPropagation(); e.preventDefault(); - const room = MatrixClientPeg.get().getRoom(this.props.roomId); - Notifier.clearRoomSound(room); + SettingsStore.setValue("notificationSound", this.props.roomId, "room-account", null); this.setState({ currentSound: "default", diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 35baa718b9..b40213e514 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -26,6 +26,7 @@ import ThemeController from './controllers/ThemeController'; // These are just a bunch of helper arrays to avoid copy/pasting a bunch of times const LEVELS_ROOM_SETTINGS = ['device', 'room-device', 'room-account', 'account', 'config']; +const LEVELS_ROOM_OR_ACCOUNT = ['room-account', 'account']; const LEVELS_ROOM_SETTINGS_WITH_ROOM = ['device', 'room-device', 'room-account', 'account', 'config', 'room']; const LEVELS_ACCOUNT_SETTINGS = ['device', 'account', 'config']; const LEVELS_FEATURE = ['device', 'config']; @@ -315,6 +316,10 @@ export const SETTINGS = { default: false, controller: new NotificationsEnabledController(), }, + "notificationSound": { + supportedLevels: LEVELS_ROOM_OR_ACCOUNT, + default: false, + }, "notificationBodyEnabled": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS, default: true, From b11050d32776c7995f9abd9953d7cebdcb899e7e Mon Sep 17 00:00:00 2001 From: Katie Wolfe Date: Sun, 21 Apr 2019 12:35:59 +0100 Subject: [PATCH 0008/1310] Check for sound before logging it Co-Authored-By: Half-Shot --- src/Notifier.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Notifier.js b/src/Notifier.js index d5810fe30d..c0c7fbdb94 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -122,7 +122,7 @@ const Notifier = { _playAudioNotification: function(ev, room) { this.getSoundForRoom(room.roomId).then((sound) => { - console.log(`Got sound ${sound.name || "default"} for ${room.roomId}`); + console.log(`Got sound ${sound && sound.name ? sound.name : "default"} for ${room.roomId}`); // XXX: How do we ensure this is a sound file and not // going to be exploited? const selector = document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); From 0f2cd6ea73c36015720293fcc20baf285a25d0be Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sun, 21 Apr 2019 18:01:26 +0100 Subject: [PATCH 0009/1310] Stick behind a feature flag --- src/Notifier.js | 14 +++++++------- .../views/dialogs/RoomSettingsDialog.js | 15 ++++++++++----- .../settings/tabs/room/NotificationSettingsTab.js | 2 +- src/i18n/strings/en_EN.json | 2 +- src/settings/Settings.js | 6 ++++++ 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index d5810fe30d..2aad99875c 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -120,11 +120,11 @@ const Notifier = { }; }, - _playAudioNotification: function(ev, room) { - this.getSoundForRoom(room.roomId).then((sound) => { - console.log(`Got sound ${sound.name || "default"} for ${room.roomId}`); - // XXX: How do we ensure this is a sound file and not - // going to be exploited? + _playAudioNotification: async function(ev, room) { + const sound = SettingsStore.isFeatureEnabled("feature_notification_sounds") ? await this.getSoundForRoom(room.roomId) : null; + console.log(`Got sound ${sound.name || "default"} for ${room.roomId}`); + // XXX: How do we ensure this is a sound file and not going to be exploited? + try { const selector = document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); let audioElement = selector; if (!selector) { @@ -139,9 +139,9 @@ const Notifier = { document.body.appendChild(audioElement); } audioElement.play(); - }).catch((ex) => { + } catch (ex) { console.warn("Caught error when trying to fetch room notification sound:", ex); - }); + } }, start: function() { diff --git a/src/components/views/dialogs/RoomSettingsDialog.js b/src/components/views/dialogs/RoomSettingsDialog.js index caed958003..180148aead 100644 --- a/src/components/views/dialogs/RoomSettingsDialog.js +++ b/src/components/views/dialogs/RoomSettingsDialog.js @@ -25,6 +25,7 @@ import SecurityRoomSettingsTab from "../settings/tabs/room/SecurityRoomSettingsT import NotificationSettingsTab from "../settings/tabs/room/NotificationSettingsTab"; import sdk from "../../../index"; import MatrixClientPeg from "../../../MatrixClientPeg"; +import SettingsStore from '../../../settings/SettingsStore'; export default class RoomSettingsDialog extends React.Component { static propTypes = { @@ -50,11 +51,15 @@ export default class RoomSettingsDialog extends React.Component { "mx_RoomSettingsDialog_rolesIcon", , )); - tabs.push(new Tab( - _td("Notifications"), - "mx_RoomSettingsDialog_rolesIcon", - , - )); + + if (SettingsStore.isFeatureEnabled("feature_notification_sounds")) { + tabs.push(new Tab( + _td("Notifications"), + "mx_RoomSettingsDialog_rolesIcon", + , + )); + } + tabs.push(new Tab( _td("Advanced"), "mx_RoomSettingsDialog_warningIcon", diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index 062d54988a..6199804cde 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -115,7 +115,7 @@ export default class NotificationsSettingsTab extends React.Component {
{_t("Sounds")}
- {_t("Notification sound")}: {this.state.currentSound} + {_t("Custom Notification Sounds")}: {this.state.currentSound}

{_t("Set a new custom sound")}

diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7f94bdc9cd..feeff019c0 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1616,7 +1616,7 @@ "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", "Sounds": "Sounds", - "Notification sound": "Notification sound", + "Custom Notification Sounds": "Notification sound", "Set a new custom sound": "Set a new custom sound", "Reset to default sound": "Reset to default sound" } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index b40213e514..b0014ae4b6 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -119,6 +119,12 @@ export const SETTINGS = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_notification_sounds": { + isFeature: true, + displayName: _td("Custom Notification Sounds"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "MessageComposerInput.suggestEmoji": { supportedLevels: LEVELS_ACCOUNT_SETTINGS, displayName: _td('Enable Emoji suggestions while typing'), From 8258e933375a4c02023c7ed599002eba83bd2b99 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sun, 21 Apr 2019 18:03:54 +0100 Subject: [PATCH 0010/1310] Use m.notification.sound --- src/settings/handlers/AccountSettingsHandler.js | 4 ++++ src/settings/handlers/RoomSettingsHandler.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index 71cef52c4e..979966ac79 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -73,6 +73,10 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa return !content['disable']; } + if (settingName === "notificationsEnabled") { + return this._getSettings("m.notification.sound"); + } + // Special case for breadcrumbs if (settingName === "breadcrumb_rooms") { const content = this._getSettings(BREADCRUMBS_EVENT_TYPE) || {}; diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 79626e2186..e929e81c88 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -67,6 +67,10 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl if (typeof(content['disable']) !== "boolean") return null; return !content['disable']; } + + if (settingName === "notificationsEnabled") { + return this._getSettings(roomId, "m.notification.sound"); + } const settings = this._getSettings(roomId) || {}; return settings[settingName]; From 9de8920869fc502621f9b5f723ffd3d517d52590 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Mon, 22 Apr 2019 23:04:03 +0100 Subject: [PATCH 0011/1310] Remove trailing space --- src/settings/handlers/RoomSettingsHandler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index e929e81c88..894190b23e 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -67,7 +67,7 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl if (typeof(content['disable']) !== "boolean") return null; return !content['disable']; } - + if (settingName === "notificationsEnabled") { return this._getSettings(roomId, "m.notification.sound"); } From 020d210cb00b1274c4f3d863b4b46661bbbf98b7 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 23 Apr 2019 16:23:58 +0100 Subject: [PATCH 0012/1310] Use uk.half-shot.* --- src/settings/handlers/AccountSettingsHandler.js | 4 ++-- src/settings/handlers/RoomSettingsHandler.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/settings/handlers/AccountSettingsHandler.js b/src/settings/handlers/AccountSettingsHandler.js index 979966ac79..e34f5b6aae 100644 --- a/src/settings/handlers/AccountSettingsHandler.js +++ b/src/settings/handlers/AccountSettingsHandler.js @@ -73,8 +73,8 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa return !content['disable']; } - if (settingName === "notificationsEnabled") { - return this._getSettings("m.notification.sound"); + if (settingName === "notificationSound") { + return this._getSettings("uk.half-shot.notification.sound"); } // Special case for breadcrumbs diff --git a/src/settings/handlers/RoomSettingsHandler.js b/src/settings/handlers/RoomSettingsHandler.js index 894190b23e..470641f9dd 100644 --- a/src/settings/handlers/RoomSettingsHandler.js +++ b/src/settings/handlers/RoomSettingsHandler.js @@ -68,8 +68,8 @@ export default class RoomSettingsHandler extends MatrixClientBackedSettingsHandl return !content['disable']; } - if (settingName === "notificationsEnabled") { - return this._getSettings(roomId, "m.notification.sound"); + if (settingName === "notificationSound") { + return this._getSettings(roomId, "uk.half-shot.notification.sound"); } const settings = this._getSettings(roomId) || {}; From 636cb8a5ccb00e91df624386af66c2c35f310c31 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 22:57:49 -0600 Subject: [PATCH 0013/1310] Have ServerConfig and co. do validation of the config in-house This also causes the components to produce a ValidatedServerConfig for use by other components. --- res/css/views/auth/_ServerConfig.scss | 5 + .../views/auth/ModularServerConfig.js | 92 ++++++++++----- src/components/views/auth/ServerConfig.js | 111 +++++++++++------- src/utils/AutoDiscoveryUtils.js | 104 ++++++++++++++++ 4 files changed, 238 insertions(+), 74 deletions(-) create mode 100644 src/utils/AutoDiscoveryUtils.js diff --git a/res/css/views/auth/_ServerConfig.scss b/res/css/views/auth/_ServerConfig.scss index 79ad9e8238..fe96da2019 100644 --- a/res/css/views/auth/_ServerConfig.scss +++ b/res/css/views/auth/_ServerConfig.scss @@ -35,3 +35,8 @@ limitations under the License. .mx_ServerConfig_help:link { opacity: 0.8; } + +.mx_ServerConfig_error { + display: block; + color: $warning-color; +} diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index 9c6c4b01bf..ea22577dbd 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -18,9 +18,15 @@ import React from 'react'; import PropTypes from 'prop-types'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import SdkConfig from "../../../SdkConfig"; +import AutoDiscoveryUtils from "../../../utils/AutoDiscoveryUtils"; +import * as ServerType from '../../views/auth/ServerTypeSelector'; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; +// TODO: TravisR - Can this extend ServerConfig for most things? + /* * Configure the Modular server name. * @@ -31,65 +37,87 @@ export default class ModularServerConfig extends React.PureComponent { static propTypes = { onServerConfigChange: PropTypes.func, - // default URLs are defined in config.json (or the hardcoded defaults) - // they are used if the user has not overridden them with a custom URL. - // In other words, if the custom URL is blank, the default is used. - defaultHsUrl: PropTypes.string, // e.g. https://matrix.org - - // This component always uses the default IS URL and doesn't allow it - // to be changed. We still receive it as a prop here to simplify - // consumers by still passing the IS URL via onServerConfigChange. - defaultIsUrl: PropTypes.string, // e.g. https://vector.im - - // custom URLs are explicitly provided by the user and override the - // default URLs. The user enters them via the component's input fields, - // which is reflected on these properties whenever on..UrlChanged fires. - // They are persisted in localStorage by MatrixClientPeg, and so can - // override the default URLs when the component initially loads. - customHsUrl: PropTypes.string, + // The current configuration that the user is expecting to change. + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged - } + }; static defaultProps = { onServerConfigChange: function() {}, customHsUrl: "", delayTimeMs: 0, - } + }; constructor(props) { super(props); this.state = { - hsUrl: props.customHsUrl, + busy: false, + errorText: "", + hsUrl: props.serverConfig.hsUrl, + isUrl: props.serverConfig.isUrl, }; } componentWillReceiveProps(newProps) { - if (newProps.customHsUrl === this.state.hsUrl) return; + if (newProps.serverConfig.hsUrl === this.state.hsUrl && + newProps.serverConfig.isUrl === this.state.isUrl) return; + + this.validateAndApplyServer(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); + } + + async validateAndApplyServer(hsUrl, isUrl) { + // Always try and use the defaults first + const defaultConfig: ValidatedServerConfig = SdkConfig.get()["validated_server_config"]; + if (defaultConfig.hsUrl === hsUrl && defaultConfig.isUrl === isUrl) { + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(defaultConfig); + return defaultConfig; + } this.setState({ - hsUrl: newProps.customHsUrl, - }); - this.props.onServerConfigChange({ - hsUrl: newProps.customHsUrl, - isUrl: this.props.defaultIsUrl, + hsUrl, + isUrl, + busy: true, + errorText: "", }); + + try { + const result = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl); + this.setState({busy: false, errorText: ""}); + this.props.onServerConfigChange(result); + return result; + } catch (e) { + console.error(e); + let message = _t("Unable to validate homeserver/identity server"); + if (e.translatedMessage) { + message = e.translatedMessage; + } + this.setState({ + busy: false, + errorText: message, + }); + } + } + + async validateServer() { + // TODO: Do we want to support .well-known lookups here? + // If for some reason someone enters "matrix.org" for a URL, we could do a lookup to + // find their homeserver without demanding they use "https://matrix.org" + return this.validateAndApplyServer(this.state.hsUrl, ServerType.TYPES.PREMIUM.identityServerUrl); } onHomeserverBlur = (ev) => { this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.props.defaultIsUrl, - }); + this.validateServer(); }); - } + }; onHomeserverChange = (ev) => { const hsUrl = ev.target.value; this.setState({ hsUrl }); - } + }; _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { @@ -116,7 +144,7 @@ export default class ModularServerConfig extends React.PureComponent {
{ this._hsTimeoutId = this._waitThenInvoke(this._hsTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.state.isUrl, - }); + this.validateServer(); }); - } + }; onHomeserverChange = (ev) => { const hsUrl = ev.target.value; this.setState({ hsUrl }); - } + }; onIdentityServerBlur = (ev) => { this._isTimeoutId = this._waitThenInvoke(this._isTimeoutId, () => { - this.props.onServerConfigChange({ - hsUrl: this.state.hsUrl, - isUrl: this.state.isUrl, - }); + this.validateServer(); }); - } + }; onIdentityServerChange = (ev) => { const isUrl = ev.target.value; this.setState({ isUrl }); - } + }; _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { @@ -114,11 +134,15 @@ export default class ServerConfig extends React.PureComponent { showHelpPopup = () => { const CustomServerDialog = sdk.getComponent('auth.CustomServerDialog'); Modal.createTrackedDialog('Custom Server Dialog', '', CustomServerDialog); - } + }; render() { const Field = sdk.getComponent('elements.Field'); + const errorText = this.state.errorText + ? {this.state.errorText} + : null; + return (

{_t("Other servers")}

@@ -127,20 +151,23 @@ export default class ServerConfig extends React.PureComponent { { sub } , })} + {errorText}
diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js new file mode 100644 index 0000000000..318c706136 --- /dev/null +++ b/src/utils/AutoDiscoveryUtils.js @@ -0,0 +1,104 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {AutoDiscovery} from "matrix-js-sdk"; +import {_td, newTranslatableError} from "../languageHandler"; +import {makeType} from "./TypeUtils"; +import SdkConfig from "../SdkConfig"; + +export class ValidatedServerConfig { + hsUrl: string; + hsName: string; + hsNameIsDifferent: string; + + isUrl: string; + identityEnabled: boolean; +} + +export default class AutoDiscoveryUtils { + static async validateServerConfigWithStaticUrls(homeserverUrl: string, identityUrl: string): ValidatedServerConfig { + if (!homeserverUrl) { + throw newTranslatableError(_td("No homeserver URL provided")); + } + + const wellknownConfig = { + "m.homeserver": { + base_url: homeserverUrl, + }, + "m.identity_server": { + base_url: identityUrl, + }, + }; + + const result = await AutoDiscovery.fromDiscoveryConfig(wellknownConfig); + + const url = new URL(homeserverUrl); + const serverName = url.hostname; + + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + } + + static async validateServerName(serverName: string): ValidatedServerConfig { + const result = await AutoDiscovery.findClientConfig(serverName); + return AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, result); + } + + static buildValidatedConfigFromDiscovery(serverName: string, discoveryResult): ValidatedServerConfig { + if (!discoveryResult || !discoveryResult["m.homeserver"]) { + // This shouldn't happen without major misconfiguration, so we'll log a bit of information + // in the log so we can find this bit of codee but otherwise tell teh user "it broke". + console.error("Ended up in a state of not knowing which homeserver to connect to."); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const hsResult = discoveryResult['m.homeserver']; + if (hsResult.state !== AutoDiscovery.SUCCESS) { + if (AutoDiscovery.ALL_ERRORS.indexOf(hsResult.error) !== -1) { + throw newTranslatableError(hsResult.error); + } + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const isResult = discoveryResult['m.identity_server']; + let preferredIdentityUrl = "https://vector.im"; + if (isResult && isResult.state === AutoDiscovery.SUCCESS) { + preferredIdentityUrl = isResult["base_url"]; + } else if (isResult && isResult.state !== AutoDiscovery.PROMPT) { + console.error("Error determining preferred identity server URL:", isResult); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + const preferredHomeserverUrl = hsResult["base_url"]; + let preferredHomeserverName = serverName ? serverName : hsResult["server_name"]; + + const url = new URL(preferredHomeserverUrl); + if (!preferredHomeserverName) preferredHomeserverName = url.hostname; + + // It should have been set by now, so check it + if (!preferredHomeserverName) { + console.error("Failed to parse homeserver name from homeserver URL"); + throw newTranslatableError(_td("Unexpected error resolving homeserver configuration")); + } + + return makeType(ValidatedServerConfig, { + hsUrl: preferredHomeserverUrl, + hsName: preferredHomeserverName, + hsNameIsDifferent: url.hostname !== preferredHomeserverName, + isUrl: preferredIdentityUrl, + identityEnabled: !SdkConfig.get()['disable_identity_server'], + }); + } +} From 6b45e6031454dc74929a76d6c071c0200d65925a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:02:01 -0600 Subject: [PATCH 0014/1310] Update ServerTypeSelector for registration to use a server config --- .../structures/auth/Registration.js | 10 +------ src/components/views/auth/RegistrationForm.js | 27 ++++++----------- .../views/auth/ServerTypeSelector.js | 21 +++++++++---- src/utils/TypeUtils.js | 30 +++++++++++++++++++ 4 files changed, 55 insertions(+), 33 deletions(-) create mode 100644 src/utils/TypeUtils.js diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index 708118bb22..c579b2082d 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -446,13 +446,6 @@ module.exports = React.createClass({ onEditServerDetailsClick = this.onEditServerDetailsClick; } - // If the current HS URL is the default HS URL, then we can label it - // with the default HS name (if it exists). - let hsName; - if (this.state.hsUrl === this.props.defaultHsUrl) { - hsName = this.props.defaultServerName; - } - return ; } }, diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 6e55581af0..f815ad081d 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { SAFE_LOCALPART_REGEX } from '../../../Registration'; import withValidation from '../elements/Validation'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; const FIELD_EMAIL = 'field_email'; const FIELD_PHONE_NUMBER = 'field_phone_number'; @@ -51,11 +52,7 @@ module.exports = React.createClass({ onRegisterClick: PropTypes.func.isRequired, // onRegisterClick(Object) => ?Promise onEditServerDetailsClick: PropTypes.func, flows: PropTypes.arrayOf(PropTypes.object).isRequired, - // This is optional and only set if we used a server name to determine - // the HS URL via `.well-known` discovery. The server name is used - // instead of the HS URL when talking about "your account". - hsName: PropTypes.string, - hsUrl: PropTypes.string, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, }, getDefaultProps: function() { @@ -499,20 +496,14 @@ module.exports = React.createClass({ }, render: function() { - let yourMatrixAccountText = _t('Create your Matrix account'); - if (this.props.hsName) { - yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { - serverName: this.props.hsName, + let yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + // TODO: TravisR - Use tooltip to underline + yourMatrixAccountText = _t('Create your Matrix account on ', {}, { + 'underlinedServerName': () => {this.props.serverConfig.hsName}, }); - } else { - try { - const parsedHsUrl = new URL(this.props.hsUrl); - yourMatrixAccountText = _t('Create your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - // ignore - } } let editLink = null; diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js index 71d13da421..602de72f3f 100644 --- a/src/components/views/auth/ServerTypeSelector.js +++ b/src/components/views/auth/ServerTypeSelector.js @@ -19,6 +19,8 @@ import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import classnames from 'classnames'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import {makeType} from "../../../utils/TypeUtils"; const MODULAR_URL = 'https://modular.im/?utm_source=riot-web&utm_medium=web&utm_campaign=riot-web-authentication'; @@ -32,8 +34,13 @@ export const TYPES = { label: () => _t('Free'), logo: () => , description: () => _t('Join millions for free on the largest public server'), - hsUrl: 'https://matrix.org', - isUrl: 'https://vector.im', + serverConfig: makeType(ValidatedServerConfig, { + hsUrl: "https://matrix.org", + hsName: "matrix.org", + hsNameIsDifferent: false, + isUrl: "https://vector.im", + identityEnabled: true, + }), }, PREMIUM: { id: PREMIUM, @@ -44,6 +51,7 @@ export const TYPES = { {sub} , }), + identityServerUrl: "https://vector.im", }, ADVANCED: { id: ADVANCED, @@ -56,10 +64,11 @@ export const TYPES = { }, }; -export function getTypeFromHsUrl(hsUrl) { +export function getTypeFromServerConfig(config) { + const {hsUrl} = config; if (!hsUrl) { return null; - } else if (hsUrl === TYPES.FREE.hsUrl) { + } else if (hsUrl === TYPES.FREE.serverConfig.hsUrl) { return FREE; } else if (new URL(hsUrl).hostname.endsWith('.modular.im')) { // This is an unlikely case to reach, as Modular defaults to hiding the @@ -76,7 +85,7 @@ export default class ServerTypeSelector extends React.PureComponent { selected: PropTypes.string, // Handler called when the selected type changes. onChange: PropTypes.func.isRequired, - } + }; constructor(props) { super(props); @@ -106,7 +115,7 @@ export default class ServerTypeSelector extends React.PureComponent { e.stopPropagation(); const type = e.currentTarget.dataset.id; this.updateSelectedType(type); - } + }; render() { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); diff --git a/src/utils/TypeUtils.js b/src/utils/TypeUtils.js new file mode 100644 index 0000000000..abdd0eb2a0 --- /dev/null +++ b/src/utils/TypeUtils.js @@ -0,0 +1,30 @@ +/* +Copyright 2019 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * Creates a class of a given type using the objects defined. This + * is a stopgap function while we don't have TypeScript interfaces. + * In future, we'd define the `type` as an interface and just cast + * it instead of cheating like we are here. + * @param {Type} Type The type of class to construct. + * @param {*} opts The options (properties) to set on the object. + * @returns {*} The created object. + */ +export function makeType(Type: any, opts: any) { + const c = new Type(); + Object.assign(c, opts); + return c; +} From 00ebb5e1fd1c5246e99d44400939960d23b6b70c Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:04:06 -0600 Subject: [PATCH 0015/1310] Make registration work with server configs The general idea is that we throw the object around between components so they can pull off the details they care about. --- .../structures/auth/Registration.js | 103 ++++++++---------- 1 file changed, 45 insertions(+), 58 deletions(-) diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index c579b2082d..faab8190bd 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -17,16 +17,15 @@ limitations under the License. */ import Matrix from 'matrix-js-sdk'; - import Promise from 'bluebird'; import React from 'react'; import PropTypes from 'prop-types'; - import sdk from '../../../index'; import { _t, _td } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; import * as ServerType from '../../views/auth/ServerTypeSelector'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -46,18 +45,7 @@ module.exports = React.createClass({ sessionId: PropTypes.string, makeRegistrationUrl: PropTypes.func.isRequired, idSid: PropTypes.string, - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about "your account". - defaultServerName: PropTypes.string, - // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, brand: PropTypes.string, email: PropTypes.string, // registration shouldn't know or care how login is done. @@ -66,7 +54,7 @@ module.exports = React.createClass({ }, getInitialState: function() { - const serverType = ServerType.getTypeFromHsUrl(this.props.customHsUrl); + const serverType = ServerType.getTypeFromServerConfig(this.props.serverConfig); return { busy: false, @@ -87,8 +75,6 @@ module.exports = React.createClass({ // straight back into UI auth doingUIAuth: Boolean(this.props.sessionId), serverType, - hsUrl: this.props.customHsUrl, - isUrl: this.props.customIsUrl, // Phase of the overall registration dialog. phase: PHASE_REGISTRATION, flows: null, @@ -100,18 +86,22 @@ module.exports = React.createClass({ this._replaceClient(); }, - onServerConfigChange: function(config) { - const newState = {}; - if (config.hsUrl !== undefined) { - newState.hsUrl = config.hsUrl; + componentWillReceiveProps(newProps) { + if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && + newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; + + this._replaceClient(newProps.serverConfig); + + // Handle cases where the user enters "https://matrix.org" for their server + // from the advanced option - we should default to FREE at that point. + const serverType = ServerType.getTypeFromServerConfig(newProps.serverConfig); + if (serverType !== this.state.serverType) { + // Reset the phase to default phase for the server type. + this.setState({ + serverType, + phase: this.getDefaultPhaseForServerType(serverType), + }); } - if (config.isUrl !== undefined) { - newState.isUrl = config.isUrl; - } - this.props.onServerConfigChange(config); - this.setState(newState, () => { - this._replaceClient(); - }); }, getDefaultPhaseForServerType(type) { @@ -136,19 +126,17 @@ module.exports = React.createClass({ // the new type. switch (type) { case ServerType.FREE: { - const { hsUrl, isUrl } = ServerType.TYPES.FREE; - this.onServerConfigChange({ - hsUrl, - isUrl, - }); + const { serverConfig } = ServerType.TYPES.FREE; + this.props.onServerConfigChange(serverConfig); break; } case ServerType.PREMIUM: + // We can accept whatever server config was the default here as this essentially + // acts as a slightly different "custom server"/ADVANCED option. + break; case ServerType.ADVANCED: - this.onServerConfigChange({ - hsUrl: this.props.defaultHsUrl, - isUrl: this.props.defaultIsUrl, - }); + // Use the default config from the config + this.props.onServerConfigChange(SdkConfig.get()["validated_server_config"]); break; } @@ -158,13 +146,15 @@ module.exports = React.createClass({ }); }, - _replaceClient: async function() { + _replaceClient: async function(serverConfig) { this.setState({ errorText: null, }); + if (!serverConfig) serverConfig = this.props.serverConfig; + const {hsUrl, isUrl} = serverConfig; this._matrixClient = Matrix.createClient({ - baseUrl: this.state.hsUrl, - idBaseUrl: this.state.isUrl, + baseUrl: hsUrl, + idBaseUrl: isUrl, }); try { await this._makeRegisterRequest({}); @@ -189,12 +179,6 @@ module.exports = React.createClass({ }, onFormSubmit: function(formVals) { - // Don't allow the user to register if there's a discovery error - // Without this, the user could end up registering on the wrong homeserver. - if (this.props.defaultServerDiscoveryError) { - this.setState({errorText: this.props.defaultServerDiscoveryError}); - return; - } this.setState({ errorText: "", busy: true, @@ -207,7 +191,7 @@ module.exports = React.createClass({ if (!success) { let msg = response.message || response.toString(); // can we give a better error message? - if (response.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { + if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( response.data.limit_type, response.data.admin_contact, { @@ -302,8 +286,13 @@ module.exports = React.createClass({ }); }, - onServerDetailsNextPhaseClick(ev) { + async onServerDetailsNextPhaseClick(ev) { ev.stopPropagation(); + // TODO: TravisR - Capture the user's input somehow else + if (this._serverConfigRef) { + // Just to make sure the user's input gets captured + await this._serverConfigRef.validateServer(); + } this.setState({ phase: PHASE_REGISTRATION, }); @@ -371,20 +360,17 @@ module.exports = React.createClass({ break; case ServerType.PREMIUM: serverDetails = this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} />; break; case ServerType.ADVANCED: serverDetails = this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} />; break; @@ -392,6 +378,7 @@ module.exports = React.createClass({ let nextButton = null; if (PHASES_ENABLED) { + // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? nextButton = @@ -466,7 +453,7 @@ module.exports = React.createClass({ const AuthPage = sdk.getComponent('auth.AuthPage'); let errorText; - const err = this.state.errorText || this.props.defaultServerDiscoveryError; + const err = this.state.errorText; if (err) { errorText =
{ err }
; } From b6e027f5cb101d5f70f05161852744eaf32bc401 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:05:59 -0600 Subject: [PATCH 0016/1310] Make password resets use server config objects Like registration, the idea is that the object is passed around between components so they can take details they need. --- .../structures/auth/ForgotPassword.js | 88 +++++-------------- 1 file changed, 23 insertions(+), 65 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 46071f0a9c..5316235fe0 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -21,8 +21,8 @@ import { _t } from '../../../languageHandler'; import sdk from '../../../index'; import Modal from "../../../Modal"; import SdkConfig from "../../../SdkConfig"; - import PasswordReset from "../../../PasswordReset"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // Phases // Show controls to configure server details @@ -40,28 +40,14 @@ module.exports = React.createClass({ displayName: 'ForgotPassword', propTypes: { - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about "your account". - defaultServerName: PropTypes.string, - // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, - - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + onServerConfigChange: PropTypes.func.isRequired, onLoginClick: PropTypes.func, onComplete: PropTypes.func.isRequired, }, getInitialState: function() { return { - enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl, - enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl, phase: PHASE_FORGOT, email: "", password: "", @@ -70,11 +56,11 @@ module.exports = React.createClass({ }; }, - submitPasswordReset: function(hsUrl, identityUrl, email, password) { + submitPasswordReset: function(email, password) { this.setState({ phase: PHASE_SENDING_EMAIL, }); - this.reset = new PasswordReset(hsUrl, identityUrl); + this.reset = new PasswordReset(this.props.serverConfig.hsUrl, this.props.serverConfig.isUrl); this.reset.resetPassword(email, password).done(() => { this.setState({ phase: PHASE_EMAIL_SENT, @@ -103,13 +89,6 @@ module.exports = React.createClass({ onSubmitForm: function(ev) { ev.preventDefault(); - // Don't allow the user to register if there's a discovery error - // Without this, the user could end up registering on the wrong homeserver. - if (this.props.defaultServerDiscoveryError) { - this.setState({errorText: this.props.defaultServerDiscoveryError}); - return; - } - if (!this.state.email) { this.showErrorDialog(_t('The email address linked to your account must be entered.')); } else if (!this.state.password || !this.state.password2) { @@ -132,10 +111,7 @@ module.exports = React.createClass({ button: _t('Continue'), onFinished: (confirmed) => { if (confirmed) { - this.submitPasswordReset( - this.state.enteredHsUrl, this.state.enteredIsUrl, - this.state.email, this.state.password, - ); + this.submitPasswordReset(this.state.email, this.state.password); } }, }); @@ -148,19 +124,13 @@ module.exports = React.createClass({ }); }, - onServerConfigChange: function(config) { - const newState = {}; - if (config.hsUrl !== undefined) { - newState.enteredHsUrl = config.hsUrl; - } - if (config.isUrl !== undefined) { - newState.enteredIsUrl = config.isUrl; - } - this.setState(newState); - }, - - onServerDetailsNextPhaseClick(ev) { + async onServerDetailsNextPhaseClick(ev) { ev.stopPropagation(); + // TODO: TravisR - Capture the user's input somehow else + if (this._serverConfigRef) { + // Just to make sure the user's input gets captured + await this._serverConfigRef.validateServer(); + } this.setState({ phase: PHASE_FORGOT, }); @@ -196,13 +166,12 @@ module.exports = React.createClass({ return null; } + // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? return
this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={0} /> { err }
; } - let yourMatrixAccountText = _t('Your Matrix account'); - if (this.state.enteredHsUrl === this.props.defaultHsUrl && this.props.defaultServerName) { - yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { - serverName: this.props.defaultServerName, + let yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + // TODO: TravisR - Use tooltip to underline + yourMatrixAccountText = _t('Your Matrix account on ', {}, { + 'underlinedServerName': () => {this.props.serverConfig.hsName}, }); - } else { - try { - const parsedHsUrl = new URL(this.state.enteredHsUrl); - yourMatrixAccountText = _t('Your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - errorText =
{_t( - "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please " + - "enter a valid URL including the protocol prefix.", - { - hsUrl: this.state.enteredHsUrl, - })}
; - } } // If custom URLs are allowed, wire up the server details edit link. From 0b1a0c77b7fe9a9eef7f134041cb438ac6ad47c6 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:07:40 -0600 Subject: [PATCH 0017/1310] Make login pass around server config objects Very similar to password resets and registration, the components pass around a server config for usage by other components. Login is a bit more complicated and needs a few more changes to pull the logic out to a more generic layer. --- src/components/structures/auth/Login.js | 189 ++++++--------------- src/components/views/auth/PasswordLogin.js | 77 ++++----- 2 files changed, 86 insertions(+), 180 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 2940346a4f..46bf0c2c76 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -25,7 +25,7 @@ import sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import { AutoDiscovery } from "matrix-js-sdk"; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -59,19 +59,14 @@ module.exports = React.createClass({ propTypes: { onLoggedIn: PropTypes.func.isRequired, - // The default server name to use when the user hasn't specified - // one. If set, `defaultHsUrl` and `defaultHsUrl` were derived for this - // via `.well-known` discovery. The server name is used instead of the - // HS URL when talking about where to "sign in to". - defaultServerName: PropTypes.string, // An error passed along from higher up explaining that something - // went wrong when finding the defaultHsUrl. - defaultServerDiscoveryError: PropTypes.string, + // went wrong. May be replaced with a different error within the + // Login component. + errorText: PropTypes.string, + + // If true, the component will consider itself busy. + busy: PropTypes.bool, - customHsUrl: PropTypes.string, - customIsUrl: PropTypes.string, - defaultHsUrl: PropTypes.string, - defaultIsUrl: PropTypes.string, // Secondary HS which we try to log into if the user is using // the default HS but login fails. Useful for migrating to a // different homeserver without confusing users. @@ -79,12 +74,13 @@ module.exports = React.createClass({ defaultDeviceDisplayName: PropTypes.string, - // login shouldn't know or care how registration is done. + // login shouldn't know or care how registration, password recovery, + // etc is done. onRegisterClick: PropTypes.func.isRequired, - - // login shouldn't care how password recovery is done. onForgotPasswordClick: PropTypes.func, onServerConfigChange: PropTypes.func.isRequired, + + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, }, getInitialState: function() { @@ -93,9 +89,6 @@ module.exports = React.createClass({ errorText: null, loginIncorrect: false, - enteredHsUrl: this.props.customHsUrl || this.props.defaultHsUrl, - enteredIsUrl: this.props.customIsUrl || this.props.defaultIsUrl, - // used for preserving form values when changing homeserver username: "", phoneCountry: null, @@ -105,10 +98,6 @@ module.exports = React.createClass({ phase: PHASE_LOGIN, // The current login flow, such as password, SSO, etc. currentFlow: "m.login.password", - - // .well-known discovery - discoveryError: "", - findingHomeserver: false, }; }, @@ -139,10 +128,17 @@ module.exports = React.createClass({ }); }, + isBusy: function() { + return this.state.busy || this.props.busy; + }, + + hasError: function() { + return this.state.errorText || this.props.errorText; + }, + onPasswordLogin: function(username, phoneCountry, phoneNumber, password) { - // Prevent people from submitting their password when homeserver - // discovery went wrong - if (this.state.discoveryError || this.props.defaultServerDiscoveryError) return; + // Prevent people from submitting their password when something isn't right. + if (this.isBusy() || this.hasError()) return; this.setState({ busy: true, @@ -164,7 +160,7 @@ module.exports = React.createClass({ const usingEmail = username.indexOf("@") > 0; if (error.httpStatus === 400 && usingEmail) { errorText = _t('This homeserver does not support login using email address.'); - } else if (error.errcode == 'M_RESOURCE_LIMIT_EXCEEDED') { + } else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { const errorTop = messageForResourceLimitError( error.data.limit_type, error.data.admin_contact, { @@ -194,11 +190,10 @@ module.exports = React.createClass({
{ _t('Incorrect username and/or password.') }
- { _t('Please note you are logging into the %(hs)s server, not matrix.org.', - { - hs: this.props.defaultHsUrl.replace(/^https?:\/\//, ''), - }) - } + {_t( + 'Please note you are logging into the %(hs)s server, not matrix.org.', + {hs: this.props.serverConfig.hsName}, + )}
); @@ -235,9 +230,9 @@ module.exports = React.createClass({ onUsernameBlur: function(username) { this.setState({ username: username, - discoveryError: null, + errorText: null, }); - if (username[0] === "@") { + if (username[0] === "@" && false) { // TODO: TravisR - Restore this const serverName = username.split(':').slice(1).join(':'); try { // we have to append 'https://' to make the URL constructor happy @@ -246,7 +241,7 @@ module.exports = React.createClass({ this._tryWellKnownDiscovery(url.hostname); } catch (e) { console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); - this.setState({discoveryError: _t("Failed to perform homeserver discovery")}); + this.setState({errorText: _t("Failed to perform homeserver discovery")}); } } }, @@ -274,32 +269,19 @@ module.exports = React.createClass({ } }, - onServerConfigChange: function(config) { - const self = this; - const newState = { - errorText: null, // reset err messages - }; - if (config.hsUrl !== undefined) { - newState.enteredHsUrl = config.hsUrl; - } - if (config.isUrl !== undefined) { - newState.enteredIsUrl = config.isUrl; - } - - this.props.onServerConfigChange(config); - this.setState(newState, function() { - self._initLoginLogic(config.hsUrl || null, config.isUrl); - }); - }, - onRegisterClick: function(ev) { ev.preventDefault(); ev.stopPropagation(); this.props.onRegisterClick(); }, - onServerDetailsNextPhaseClick(ev) { + async onServerDetailsNextPhaseClick(ev) { ev.stopPropagation(); + // TODO: TravisR - Capture the user's input somehow else + if (this._serverConfigRef) { + // Just to make sure the user's input gets captured + await this._serverConfigRef.validateServer(); + } this.setState({ phase: PHASE_LOGIN, }); @@ -313,64 +295,13 @@ module.exports = React.createClass({ }); }, - _tryWellKnownDiscovery: async function(serverName) { - if (!serverName.trim()) { - // Nothing to discover - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - return; - } - - this.setState({findingHomeserver: true}); - try { - const discovery = await AutoDiscovery.findClientConfig(serverName); - - const state = discovery["m.homeserver"].state; - if (state !== AutoDiscovery.SUCCESS && state !== AutoDiscovery.PROMPT) { - this.setState({ - discoveryError: discovery["m.homeserver"].error, - findingHomeserver: false, - }); - } else if (state === AutoDiscovery.PROMPT) { - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - } else if (state === AutoDiscovery.SUCCESS) { - this.setState({ - discoveryError: "", - findingHomeserver: false, - }); - this.onServerConfigChange({ - hsUrl: discovery["m.homeserver"].base_url, - isUrl: discovery["m.identity_server"].state === AutoDiscovery.SUCCESS - ? discovery["m.identity_server"].base_url - : "", - }); - } else { - console.warn("Unknown state for m.homeserver in discovery response: ", discovery); - this.setState({ - discoveryError: _t("Unknown failure discovering homeserver"), - findingHomeserver: false, - }); - } - } catch (e) { - console.error(e); - this.setState({ - findingHomeserver: false, - discoveryError: _t("Unknown error discovering homeserver"), - }); - } - }, - _initLoginLogic: function(hsUrl, isUrl) { const self = this; - hsUrl = hsUrl || this.state.enteredHsUrl; - isUrl = isUrl || this.state.enteredIsUrl; + hsUrl = hsUrl || this.props.serverConfig.hsUrl; + isUrl = isUrl || this.props.serverConfig.isUrl; - const fallbackHsUrl = hsUrl === this.props.defaultHsUrl ? this.props.fallbackHsUrl : null; + // TODO: TravisR - Only use this if the homeserver is the default homeserver + const fallbackHsUrl = this.props.fallbackHsUrl; const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, @@ -378,8 +309,6 @@ module.exports = React.createClass({ this._loginLogic = loginLogic; this.setState({ - enteredHsUrl: hsUrl, - enteredIsUrl: isUrl, busy: true, loginIncorrect: false, }); @@ -445,8 +374,8 @@ module.exports = React.createClass({ if (err.cors === 'rejected') { if (window.location.protocol === 'https:' && - (this.state.enteredHsUrl.startsWith("http:") || - !this.state.enteredHsUrl.startsWith("http")) + (this.props.serverConfig.hsUrl.startsWith("http:") || + !this.props.serverConfig.hsUrl.startsWith("http")) ) { errorText = { _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + @@ -469,9 +398,9 @@ module.exports = React.createClass({ "is not blocking requests.", {}, { 'a': (sub) => { - return { sub }; + return + { sub } + ; }, }, ) } @@ -495,19 +424,17 @@ module.exports = React.createClass({ } const serverDetails = this._serverConfigRef = r} + serverConfig={this.props.serverConfig} + onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} />; let nextButton = null; if (PHASES_ENABLED) { + // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? nextButton = + onClick={this.onServerDetailsNextPhaseClick}> {_t("Next")} ; } @@ -547,13 +474,6 @@ module.exports = React.createClass({ onEditServerDetailsClick = this.onEditServerDetailsClick; } - // If the current HS URL is the default HS URL, then we can label it - // with the default HS name (if it exists). - let hsName; - if (this.state.enteredHsUrl === this.props.defaultHsUrl) { - hsName = this.props.defaultServerName; - } - return ( + serverConfig={this.props.serverConfig} + disableSubmit={this.isBusy()} + /> ); }, @@ -595,9 +514,9 @@ module.exports = React.createClass({ const AuthPage = sdk.getComponent("auth.AuthPage"); const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthBody = sdk.getComponent("auth.AuthBody"); - const loader = this.state.busy ?
: null; + const loader = this.isBusy() ?
: null; - const errorText = this.props.defaultServerDiscoveryError || this.state.discoveryError || this.state.errorText; + const errorText = this.state.errorText || this.props.errorText; let errorTextSection; if (errorText) { diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index ed3afede2f..90c607442f 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2017 Vector Creations Ltd +Copyright 2017,2019 Vector Creations Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,11 +21,29 @@ import classNames from 'classnames'; import sdk from '../../../index'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; +import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; /** * A pure UI component which displays a username/password form. */ -class PasswordLogin extends React.Component { +export default class PasswordLogin extends React.Component { + static propTypes = { + onSubmit: PropTypes.func.isRequired, // fn(username, password) + onError: PropTypes.func, + onForgotPasswordClick: PropTypes.func, // fn() + initialUsername: PropTypes.string, + initialPhoneCountry: PropTypes.string, + initialPhoneNumber: PropTypes.string, + initialPassword: PropTypes.string, + onUsernameChanged: PropTypes.func, + onPhoneCountryChanged: PropTypes.func, + onPhoneNumberChanged: PropTypes.func, + onPasswordChanged: PropTypes.func, + loginIncorrect: PropTypes.bool, + disableSubmit: PropTypes.bool, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, + }; + static defaultProps = { onError: function() {}, onEditServerDetailsClick: null, @@ -40,13 +58,12 @@ class PasswordLogin extends React.Component { initialPhoneNumber: "", initialPassword: "", loginIncorrect: false, - // This is optional and only set if we used a server name to determine - // the HS URL via `.well-known` discovery. The server name is used - // instead of the HS URL when talking about where to "sign in to". - hsName: null, - hsUrl: "", disableSubmit: false, - } + }; + + static LOGIN_FIELD_EMAIL = "login_field_email"; + static LOGIN_FIELD_MXID = "login_field_mxid"; + static LOGIN_FIELD_PHONE = "login_field_phone"; constructor(props) { super(props); @@ -258,20 +275,14 @@ class PasswordLogin extends React.Component {
; } - let signInToText = _t('Sign in to your Matrix account'); - if (this.props.hsName) { - signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: this.props.hsName, + let signInToText = _t('Sign in to your Matrix account on %(serverName)s', { + serverName: this.props.serverConfig.hsName, + }); + if (this.props.serverConfig.hsNameIsDifferent) { + // TODO: TravisR - Use tooltip to underline + signInToText = _t('Sign in to your Matrix account on ', {}, { + 'underlinedServerName': () => {this.props.serverConfig.hsName}, }); - } else { - try { - const parsedHsUrl = new URL(this.props.hsUrl); - signInToText = _t('Sign in to your Matrix account on %(serverName)s', { - serverName: parsedHsUrl.hostname, - }); - } catch (e) { - // ignore - } } let editLink = null; @@ -353,27 +364,3 @@ class PasswordLogin extends React.Component { ); } } - -PasswordLogin.LOGIN_FIELD_EMAIL = "login_field_email"; -PasswordLogin.LOGIN_FIELD_MXID = "login_field_mxid"; -PasswordLogin.LOGIN_FIELD_PHONE = "login_field_phone"; - -PasswordLogin.propTypes = { - onSubmit: PropTypes.func.isRequired, // fn(username, password) - onError: PropTypes.func, - onForgotPasswordClick: PropTypes.func, // fn() - initialUsername: PropTypes.string, - initialPhoneCountry: PropTypes.string, - initialPhoneNumber: PropTypes.string, - initialPassword: PropTypes.string, - onUsernameChanged: PropTypes.func, - onPhoneCountryChanged: PropTypes.func, - onPhoneNumberChanged: PropTypes.func, - onPasswordChanged: PropTypes.func, - loginIncorrect: PropTypes.bool, - hsName: PropTypes.string, - hsUrl: PropTypes.string, - disableSubmit: PropTypes.bool, -}; - -module.exports = PasswordLogin; From 1f527e71b1296a5a5747160f1e0fbf7aea27bbfd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:09:07 -0600 Subject: [PATCH 0018/1310] Bring server config juggling into MatrixChat This way the server config is consistent across login, password reset, and registration. This also brings the code into a more generic place for all 3 duplicated efforts. --- src/components/structures/MatrixChat.js | 155 +++--------------------- 1 file changed, 17 insertions(+), 138 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 277985ba1d..ca9ddec749 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -50,8 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; - -const AutoDiscovery = Matrix.AutoDiscovery; +import {ValidatedDiscoveryConfig} from "../../utils/AutoDiscoveryUtils"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -181,16 +180,8 @@ export default React.createClass({ // Parameters used in the registration dance with the IS register_client_secret: null, register_session_id: null, - register_hs_url: null, - register_is_url: null, register_id_sid: null, - // Parameters used for setting up the authentication 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, @@ -211,42 +202,15 @@ export default React.createClass({ }; }, - getDefaultServerName: function() { - return this.state.defaultServerName; - }, - - getCurrentHsUrl: function() { - if (this.state.register_hs_url) { - return this.state.register_hs_url; - } else if (MatrixClientPeg.get()) { - return MatrixClientPeg.get().getHomeserverUrl(); - } else { - return this.getDefaultHsUrl(); - } - }, - - getDefaultHsUrl(defaultToMatrixDotOrg) { - defaultToMatrixDotOrg = typeof(defaultToMatrixDotOrg) !== 'boolean' ? true : defaultToMatrixDotOrg; - if (!this.state.defaultHsUrl && defaultToMatrixDotOrg) return "https://matrix.org"; - return this.state.defaultHsUrl; - }, - + // TODO: TravisR - Remove this or put it somewhere else getFallbackHsUrl: function() { return this.props.config.fallback_hs_url; }, - getCurrentIsUrl: function() { - if (this.state.register_is_url) { - return this.state.register_is_url; - } else if (MatrixClientPeg.get()) { - return MatrixClientPeg.get().getIdentityServerUrl(); - } else { - return this.getDefaultIsUrl(); - } - }, - - getDefaultIsUrl() { - return this.state.defaultIsUrl || "https://vector.im"; + getServerProperties() { + let props = this.state.serverConfig; + if (!props) props = SdkConfig.get()["validated_server_config"]; + return {serverConfig: props}; }, componentWillMount: function() { @@ -260,40 +224,6 @@ export default React.createClass({ MatrixClientPeg.opts.initialSyncLimit = this.props.config.sync_timeline_limit; } - // 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` - const paramHs = this.props.startingFragmentQueryParams.hs_url; - if (paramHs) { - console.log('Setting register_hs_url ', paramHs); - this.setState({ - register_hs_url: paramHs, - }); - } - // Set a default IS with query param `is_url` - const paramIs = this.props.startingFragmentQueryParams.is_url; - if (paramIs) { - console.log('Setting register_is_url ', paramIs); - this.setState({ - register_is_url: paramIs, - }); - } - // a thing to call showScreen with once login completes. this is kept // outside this.state because updating it should never trigger a // rerender. @@ -374,8 +304,8 @@ export default React.createClass({ return Lifecycle.loadSession({ fragmentQueryParams: this.props.startingFragmentQueryParams, enableGuest: this.props.enableGuest, - guestHsUrl: this.getCurrentHsUrl(), - guestIsUrl: this.getCurrentIsUrl(), + guestHsUrl: this.getServerProperties().serverConfig.hsUrl, + guestIsUrl: this.getServerProperties().serverConfig.isUrl, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, }); }).then((loadedSession) => { @@ -1823,44 +1753,7 @@ export default React.createClass({ }, onServerConfigChange(config) { - const newState = {}; - if (config.hsUrl) { - newState.register_hs_url = config.hsUrl; - } - if (config.isUrl) { - newState.register_is_url = config.isUrl; - } - this.setState(newState); - }, - - _tryDiscoverDefaultHomeserver: async function(serverName) { - 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: _t("Unknown error discovering homeserver"), - loadingDefaultHomeserver: false, - }); - } + this.setState({serverConfig: config}); }, _makeRegistrationUrl: function(params) { @@ -1879,8 +1772,7 @@ export default React.createClass({ if ( this.state.view === VIEWS.LOADING || - this.state.view === VIEWS.LOGGING_IN || - this.state.loadingDefaultHomeserver + this.state.view === VIEWS.LOGGING_IN ) { const Spinner = sdk.getComponent('elements.Spinner'); return ( @@ -1958,18 +1850,13 @@ export default React.createClass({ sessionId={this.state.register_session_id} idSid={this.state.register_id_sid} email={this.props.startingFragmentQueryParams.email} - defaultServerName={this.getDefaultServerName()} - defaultServerDiscoveryError={this.state.defaultServerDiscoveryError} - defaultHsUrl={this.getDefaultHsUrl()} - defaultIsUrl={this.getDefaultIsUrl()} brand={this.props.config.brand} - customHsUrl={this.getCurrentHsUrl()} - customIsUrl={this.getCurrentIsUrl()} makeRegistrationUrl={this._makeRegistrationUrl} onLoggedIn={this.onRegistered} onLoginClick={this.onLoginClick} onServerConfigChange={this.onServerConfigChange} - /> + {...this.getServerProperties()} + /> ); } @@ -1978,14 +1865,11 @@ export default React.createClass({ const ForgotPassword = sdk.getComponent('structures.auth.ForgotPassword'); return ( + onLoginClick={this.onLoginClick} + onServerConfigChange={this.onServerConfigChange} + {...this.getServerProperties()} + /> ); } @@ -1995,16 +1879,11 @@ export default React.createClass({ ); } From bb6ee10d8c4ac46c8547ae3263a352ef77e6886b Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:09:31 -0600 Subject: [PATCH 0019/1310] Add language features to support server config changes --- src/i18n/strings/en_EN.json | 13 ++++++------- src/languageHandler.js | 12 ++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index eaea057b36..35593708c8 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -246,6 +246,8 @@ "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", + "No homeserver URL provided": "No homeserver URL provided", + "Unexpected error resolving homeserver configuration": "Unexpected error resolving homeserver configuration", "This homeserver has hit its Monthly Active User limit.": "This homeserver has hit its Monthly Active User limit.", "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", @@ -1306,6 +1308,7 @@ "Code": "Code", "Submit": "Submit", "Start authentication": "Start authentication", + "Unable to validate homeserver/identity server": "Unable to validate homeserver/identity server", "Your Modular server": "Your Modular server", "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.": "Enter the location of your Modular homeserver. It may use your own domain name or be a subdomain of modular.im.", "Server Name": "Server Name", @@ -1318,8 +1321,8 @@ "Username": "Username", "Phone": "Phone", "Not sure of your password? Set a new one": "Not sure of your password? Set a new one", - "Sign in to your Matrix account": "Sign in to your Matrix account", "Sign in to your Matrix account on %(serverName)s": "Sign in to your Matrix account on %(serverName)s", + "Sign in to your Matrix account on ": "Sign in to your Matrix account on ", "Change": "Change", "Sign in with": "Sign in with", "If you don't specify an email address, you won't be able to reset your password. Are you sure?": "If you don't specify an email address, you won't be able to reset your password. Are you sure?", @@ -1339,8 +1342,8 @@ "Email (optional)": "Email (optional)", "Confirm": "Confirm", "Phone (optional)": "Phone (optional)", - "Create your Matrix account": "Create your Matrix account", "Create your Matrix account on %(serverName)s": "Create your Matrix account on %(serverName)s", + "Create your Matrix account on ": "Create your Matrix account on ", "Use an email address to recover your account.": "Use an email address to recover your account.", "Other users can invite you to rooms using your contact details.": "Other users can invite you to rooms using your contact details.", "Other servers": "Other servers", @@ -1407,7 +1410,6 @@ "This homeserver does not support communities": "This homeserver does not support communities", "Failed to load %(groupId)s": "Failed to load %(groupId)s", "Filter room names": "Filter room names", - "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'?", @@ -1423,7 +1425,6 @@ "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.", "You are logged in to another account": "You are logged in to another account", "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.": "Thank you for verifying your email! The account you're logged into here (%(sessionUserId)s) appears to be different from the account you've verified an email for (%(verifiedUserId)s). If you would like to log in to %(verifiedUserId2)s, please log out first.", - "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!", @@ -1491,9 +1492,8 @@ "A new password must be entered.": "A new password must be entered.", "New passwords must match each other.": "New passwords must match each other.", "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.": "Changing your password will reset any end-to-end encryption keys on all of your devices, making encrypted chat history unreadable. Set up Key Backup or export your room keys from another device before resetting your password.", - "Your Matrix account": "Your Matrix account", "Your Matrix account on %(serverName)s": "Your Matrix account on %(serverName)s", - "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.": "The homeserver URL %(hsUrl)s doesn't seem to be valid URL. Please enter a valid URL including the protocol prefix.", + "Your Matrix account on ": "Your Matrix account on ", "A verification email will be sent to your inbox to confirm setting your new password.": "A verification email will be sent to your inbox to confirm setting your new password.", "Send Reset Email": "Send Reset Email", "Sign in instead": "Sign in instead", @@ -1517,7 +1517,6 @@ "Please note you are logging into the %(hs)s server, not matrix.org.": "Please note you are logging into the %(hs)s server, not matrix.org.", "Failed to perform homeserver discovery": "Failed to perform homeserver discovery", "The phone number entered looks invalid": "The phone number entered looks invalid", - "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.", diff --git a/src/languageHandler.js b/src/languageHandler.js index 854ac079bc..bd3a8df721 100644 --- a/src/languageHandler.js +++ b/src/languageHandler.js @@ -32,6 +32,18 @@ counterpart.setSeparator('|'); // Fall back to English counterpart.setFallbackLocale('en'); +/** + * Helper function to create an error which has an English message + * with a translatedMessage property for use by the consumer. + * @param {string} message Message to translate. + * @returns {Error} The constructed error. + */ +export function newTranslatableError(message) { + const error = new Error(message); + error.translatedMessage = _t(message); + return error; +} + // Function which only purpose is to mark that a string is translatable // Does not actually do anything. It's helpful for automatic extraction of translatable strings export function _td(s) { From a4b64649029b1f51a2da2d9143ccade636039ccd Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:39:15 -0600 Subject: [PATCH 0020/1310] Appease the linter --- src/components/structures/MatrixChat.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index ca9ddec749..089c843e6f 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -50,7 +50,6 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; -import {ValidatedDiscoveryConfig} from "../../utils/AutoDiscoveryUtils"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. From ae63df95ea09ec4be2f9a50bf4988ed4d98a16ef Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:46:43 -0600 Subject: [PATCH 0021/1310] Fix tests to use new serverConfig prop --- test/components/structures/auth/Login-test.js | 4 ++-- test/components/structures/auth/Registration-test.js | 4 ++-- test/test-utils.js | 12 ++++++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/test/components/structures/auth/Login-test.js b/test/components/structures/auth/Login-test.js index ec95243a56..74451b922f 100644 --- a/test/components/structures/auth/Login-test.js +++ b/test/components/structures/auth/Login-test.js @@ -22,6 +22,7 @@ import ReactTestUtils from 'react-dom/test-utils'; import sdk from 'matrix-react-sdk'; import SdkConfig from '../../../../src/SdkConfig'; import * as TestUtils from '../../../test-utils'; +import {mkServerConfig} from "../../../test-utils"; const Login = sdk.getComponent( 'structures.auth.Login', @@ -44,8 +45,7 @@ describe('Login', function() { function render() { return ReactDOM.render( {}} onRegisterClick={() => {}} onServerConfigChange={() => {}} diff --git a/test/components/structures/auth/Registration-test.js b/test/components/structures/auth/Registration-test.js index a10201d465..6914ed71d7 100644 --- a/test/components/structures/auth/Registration-test.js +++ b/test/components/structures/auth/Registration-test.js @@ -22,6 +22,7 @@ import ReactTestUtils from 'react-dom/test-utils'; import sdk from 'matrix-react-sdk'; import SdkConfig from '../../../../src/SdkConfig'; import * as TestUtils from '../../../test-utils'; +import {mkServerConfig} from "../../../test-utils"; const Registration = sdk.getComponent( 'structures.auth.Registration', @@ -44,8 +45,7 @@ describe('Registration', function() { function render() { return ReactDOM.render( {}} onLoggedIn={() => {}} onLoginClick={() => {}} diff --git a/test/test-utils.js b/test/test-utils.js index f4f00effbb..54705434e2 100644 --- a/test/test-utils.js +++ b/test/test-utils.js @@ -7,6 +7,8 @@ import PropTypes from 'prop-types'; import peg from '../src/MatrixClientPeg'; import dis from '../src/dispatcher'; import jssdk from 'matrix-js-sdk'; +import {makeType} from "../src/utils/TypeUtils"; +import {ValidatedServerConfig} from "../src/utils/AutoDiscoveryUtils"; const MatrixEvent = jssdk.MatrixEvent; /** @@ -260,6 +262,16 @@ export function mkStubRoom(roomId = null) { }; } +export function mkServerConfig(hsUrl, isUrl) { + return makeType(ValidatedServerConfig, { + hsUrl, + hsName: "TEST_ENVIRONMENT", + hsNameIsDifferent: false, // yes, we lie + isUrl, + identityEnabled: true, + }); +} + export function getDispatchForStore(store) { // Mock the dispatcher by gut-wrenching. Stores can only __emitChange whilst a // dispatcher `_isDispatching` is true. From 4ada66d3195052a0617f5c49d4512d764da961cc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 May 2019 23:59:54 -0600 Subject: [PATCH 0022/1310] Fix rogue instance of old hsUrl property --- src/components/views/auth/PasswordLogin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 90c607442f..f5b2aec210 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -212,7 +212,7 @@ export default class PasswordLogin extends React.Component { type="text" label={SdkConfig.get().disable_custom_urls ? _t("Username on %(hs)s", { - hs: this.props.hsUrl.replace(/^https?:\/\//, ''), + hs: this.props.serverConfig.hsName, }) : _t("Username")} value={this.state.username} onChange={this.onUsernameChanged} From 58b9eb4cb22220c9d90f21d966843199cad55535 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Fri, 3 May 2019 16:25:54 -0600 Subject: [PATCH 0023/1310] Add a serverConfig property to MatrixChat for unit tests --- src/components/structures/MatrixChat.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 089c843e6f..545f847718 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -50,6 +50,7 @@ import SettingsStore, {SettingLevel} from "../../settings/SettingsStore"; import { startAnyRegistrationFlow } from "../../Registration.js"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; +import {ValidatedServerConfig} from "../../utils/AutoDiscoveryUtils"; // Disable warnings for now: we use deprecated bluebird functions // and need to migrate, but they spam the console with warnings. @@ -107,6 +108,7 @@ export default React.createClass({ propTypes: { config: PropTypes.object, + serverConfig: PropTypes.instanceOf(ValidatedServerConfig), ConferenceHandler: PropTypes.any, onNewScreen: PropTypes.func, registrationUrl: PropTypes.string, @@ -208,6 +210,7 @@ export default React.createClass({ getServerProperties() { let props = this.state.serverConfig; + if (!props) props = this.props.serverConfig; // for unit tests if (!props) props = SdkConfig.get()["validated_server_config"]; return {serverConfig: props}; }, From eab209a26b5bc6eb9cf13bc082fd506dea82e10a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 6 May 2019 12:00:48 -0600 Subject: [PATCH 0024/1310] Log in to the right homeserver when changing the homeserver --- src/components/structures/auth/Login.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 46bf0c2c76..af9370f2db 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -121,6 +121,14 @@ module.exports = React.createClass({ this._unmounted = true; }, + componentWillReceiveProps(newProps) { + if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && + newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; + + // Ensure that we end up actually logging in to the right place + this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); + }, + onPasswordLoginError: function(errorText) { this.setState({ errorText, From 64a384477e567fb7d737c5389edca582e47d8d47 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Sun, 12 May 2019 17:14:21 +0100 Subject: [PATCH 0025/1310] Resolve issues --- src/Notifier.js | 16 +++++---- .../tabs/room/NotificationSettingsTab.js | 34 ++++++++++++------- src/i18n/strings/en_EN.json | 10 +++--- src/settings/Settings.js | 1 + 4 files changed, 38 insertions(+), 23 deletions(-) diff --git a/src/Notifier.js b/src/Notifier.js index ca0a24d593..8c62a9e822 100644 --- a/src/Notifier.js +++ b/src/Notifier.js @@ -97,20 +97,24 @@ const Notifier = { }, getSoundForRoom: async function(roomId) { - // We do no caching here because the SDK caches the event content + // We do no caching here because the SDK caches setting // and the browser will cache the sound. let content = SettingsStore.getValue("notificationSound", roomId); if (!content) { - content = SettingsStore.getValue("notificationSound"); - if (!content) { - return null; - } + return null; } if (!content.url) { console.warn(`${roomId} has custom notification sound event, but no url key`); return null; } + + if (!content.url.startsWith("mxc://")) { + console.warn(`${roomId} has custom notification sound event, but url is not a mxc url`); + return null; + } + + // Ideally in here we could use MSC1310 to detect the type of file, and reject it. return { url: MatrixClientPeg.get().mxcUrlToHttp(content.url), @@ -123,7 +127,7 @@ const Notifier = { _playAudioNotification: async function(ev, room) { const sound = SettingsStore.isFeatureEnabled("feature_notification_sounds") ? await this.getSoundForRoom(room.roomId) : null; console.log(`Got sound ${sound && sound.name || "default"} for ${room.roomId}`); - // XXX: How do we ensure this is a sound file and not going to be exploited? + try { const selector = document.querySelector(sound ? `audio[src='${sound.url}']` : "#messageAudio"); let audioElement = selector; diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index 6199804cde..e7ed14b491 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -20,12 +20,12 @@ import {_t} from "../../../../../languageHandler"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; import Notifier from "../../../../../Notifier"; -import SettingsStore from '../../../../../settings/SettingsStore'; +import SettingsStore from '../../../../../settings/SettingsStore'; +import { SettingLevel } from '../../../../../settings/SettingsStore'; export default class NotificationsSettingsTab extends React.Component { static propTypes = { roomId: PropTypes.string.isRequired, - closeSettingsFn: PropTypes.func.isRequired, }; constructor() { @@ -46,7 +46,7 @@ export default class NotificationsSettingsTab extends React.Component { }); } - _onSoundUploadChanged(e) { + async _onSoundUploadChanged(e) { if (!e.target.files || !e.target.files.length) { this.setState({ uploadedFile: null, @@ -58,11 +58,18 @@ export default class NotificationsSettingsTab extends React.Component { this.setState({ uploadedFile: file, }); + + try { + await this._saveSound(); + } catch (ex) { + console.error( + `Unable to save notification sound for ${this.props.roomId}`, + ex, + ); + } } - async _saveSound(e) { - e.stopPropagation(); - e.preventDefault(); + async _saveSound() { if (!this.state.uploadedFile) { return; } @@ -82,7 +89,8 @@ export default class NotificationsSettingsTab extends React.Component { await SettingsStore.setValue( "notificationSound", this.props.roomId, - "room-account", + SettingsStore. + SettingLevel.ROOM_ACCOUNT, { name: this.state.uploadedFile.name, type: type, @@ -101,7 +109,12 @@ export default class NotificationsSettingsTab extends React.Component { _clearSound(e) { e.stopPropagation(); e.preventDefault(); - SettingsStore.setValue("notificationSound", this.props.roomId, "room-account", null); + SettingsStore.setValue( + "notificationSound", + this.props.roomId, + SettingLevel.ROOM_ACCOUNT, + null, + ); this.setState({ currentSound: "default", @@ -119,11 +132,8 @@ export default class NotificationsSettingsTab extends React.Component {

{_t("Set a new custom sound")}

-
+ - - {_t("Save")} -
{_t("Reset to default sound")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d396c70e4c..d841d5a34b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -297,6 +297,7 @@ "Show recent room avatars above the room list": "Show recent room avatars above the room list", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", + "Custom Notification Sounds": "Custom Notification Sounds", "React to messages with emoji": "React to messages with emoji", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", @@ -613,6 +614,9 @@ "Room Addresses": "Room Addresses", "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", "URL Previews": "URL Previews", + "Sounds": "Sounds", + "Set a new custom sound": "Set a new custom sound", + "Reset to default sound": "Reset to default sound", "Change room avatar": "Change room avatar", "Change room name": "Change room name", "Change main address for the room": "Change main address for the room", @@ -1619,9 +1623,5 @@ "If you didn't remove the 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 remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in 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", - "Sounds": "Sounds", - "Custom Notification Sounds": "Notification sound", - "Set a new custom sound": "Set a new custom sound", - "Reset to default sound": "Reset to default sound" + "Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room" } diff --git a/src/settings/Settings.js b/src/settings/Settings.js index 3ef2e8dd86..e5ae504b53 100644 --- a/src/settings/Settings.js +++ b/src/settings/Settings.js @@ -122,6 +122,7 @@ export const SETTINGS = { "feature_notification_sounds": { isFeature: true, displayName: _td("Custom Notification Sounds"), + }, "feature_reactions": { isFeature: true, displayName: _td("React to messages with emoji"), From ee33a4e9ba6349136c68a5165c14e914b0d02135 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 13 May 2019 16:30:34 -0600 Subject: [PATCH 0026/1310] Refactor "Next" button into ServerConfig components TODO still remains about making ModularServerConfig extend ServerConfig instead of duplicating everything. See https://github.com/vector-im/riot-web/issues/9290 --- .../structures/auth/ForgotPassword.js | 30 +++----- src/components/structures/auth/Login.js | 38 ++++------ .../structures/auth/Registration.js | 31 +++----- .../views/auth/ModularServerConfig.js | 51 ++++++++++--- src/components/views/auth/ServerConfig.js | 71 ++++++++++++++----- 5 files changed, 125 insertions(+), 96 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 5316235fe0..a772e72c5a 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -124,13 +124,7 @@ module.exports = React.createClass({ }); }, - async onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); - // TODO: TravisR - Capture the user's input somehow else - if (this._serverConfigRef) { - // Just to make sure the user's input gets captured - await this._serverConfigRef.validateServer(); - } + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_FORGOT, }); @@ -160,25 +154,19 @@ module.exports = React.createClass({ renderServerDetails() { const ServerConfig = sdk.getComponent("auth.ServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; } - // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? - return
- this._serverConfigRef = r} - serverConfig={this.props.serverConfig} - onServerConfigChange={this.props.onServerConfigChange} - delayTimeMs={0} /> - - {_t("Next")} - -
; + return ; }, renderForgot() { diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index af9370f2db..68b440d064 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -20,11 +20,11 @@ limitations under the License. import React from 'react'; import PropTypes from 'prop-types'; -import { _t, _td } from '../../../languageHandler'; +import {_t, _td} from '../../../languageHandler'; import sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; -import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; +import {messageForResourceLimitError} from '../../../utils/ErrorUtils'; import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // For validating phone numbers without country codes @@ -283,13 +283,7 @@ module.exports = React.createClass({ this.props.onRegisterClick(); }, - async onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); - // TODO: TravisR - Capture the user's input somehow else - if (this._serverConfigRef) { - // Just to make sure the user's input gets captured - await this._serverConfigRef.validateServer(); - } + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_LOGIN, }); @@ -421,7 +415,6 @@ module.exports = React.createClass({ renderServerComponent() { const ServerConfig = sdk.getComponent("auth.ServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; @@ -431,26 +424,19 @@ module.exports = React.createClass({ return null; } - const serverDetails = this._serverConfigRef = r} + const serverDetailsProps = {}; + if (PHASES_ENABLED) { + serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; + serverDetailsProps.submitText = _t("Next"); + serverDetailsProps.submitClass = "mx_Login_submit"; + } + + return ; - - let nextButton = null; - if (PHASES_ENABLED) { - // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? - nextButton = - {_t("Next")} - ; - } - - return
- {serverDetails} - {nextButton} -
; }, renderLoginComponentForStep() { diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js index faab8190bd..f516816033 100644 --- a/src/components/structures/auth/Registration.js +++ b/src/components/structures/auth/Registration.js @@ -286,13 +286,7 @@ module.exports = React.createClass({ }); }, - async onServerDetailsNextPhaseClick(ev) { - ev.stopPropagation(); - // TODO: TravisR - Capture the user's input somehow else - if (this._serverConfigRef) { - // Just to make sure the user's input gets captured - await this._serverConfigRef.validateServer(); - } + async onServerDetailsNextPhaseClick() { this.setState({ phase: PHASE_REGISTRATION, }); @@ -337,7 +331,6 @@ module.exports = React.createClass({ const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); const ServerConfig = sdk.getComponent("auth.ServerConfig"); const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); if (SdkConfig.get()['disable_custom_urls']) { return null; @@ -354,45 +347,41 @@ module.exports = React.createClass({
; } + const serverDetailsProps = {}; + if (PHASES_ENABLED) { + serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; + serverDetailsProps.submitText = _t("Next"); + serverDetailsProps.submitClass = "mx_Login_submit"; + } + let serverDetails = null; switch (this.state.serverType) { case ServerType.FREE: break; case ServerType.PREMIUM: serverDetails = this._serverConfigRef = r} serverConfig={this.props.serverConfig} onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} + {...serverDetailsProps} />; break; case ServerType.ADVANCED: serverDetails = this._serverConfigRef = r} serverConfig={this.props.serverConfig} onServerConfigChange={this.props.onServerConfigChange} delayTimeMs={250} + {...serverDetailsProps} />; break; } - let nextButton = null; - if (PHASES_ENABLED) { - // TODO: TravisR - Pull out server discovery from ServerConfig to disable the next button? - nextButton = - {_t("Next")} - ; - } - return
{serverDetails} - {nextButton}
; }, diff --git a/src/components/views/auth/ModularServerConfig.js b/src/components/views/auth/ModularServerConfig.js index ea22577dbd..5a3bc23596 100644 --- a/src/components/views/auth/ModularServerConfig.js +++ b/src/components/views/auth/ModularServerConfig.js @@ -41,6 +41,16 @@ export default class ModularServerConfig extends React.PureComponent { serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged + + // Called after the component calls onServerConfigChange + onAfterSubmit: PropTypes.func, + + // Optional text for the submit button. If falsey, no button will be shown. + submitText: PropTypes.string, + + // Optional class for the submit button. Only applies if the submit button + // is to be rendered. + submitClass: PropTypes.string, }; static defaultProps = { @@ -119,6 +129,16 @@ export default class ModularServerConfig extends React.PureComponent { this.setState({ hsUrl }); }; + onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + await this.validateServer(); + + if (this.props.onAfterSubmit) { + this.props.onAfterSubmit(); + } + }; + _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { clearTimeout(existingTimeoutId); @@ -128,6 +148,16 @@ export default class ModularServerConfig extends React.PureComponent { render() { const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); + + const submitButton = this.props.submitText + ? {this.props.submitText} + : null; return (
@@ -141,15 +171,18 @@ export default class ModularServerConfig extends React.PureComponent { , }, )} -
- -
+
+
+ +
+ {submitButton} +
); } diff --git a/src/components/views/auth/ServerConfig.js b/src/components/views/auth/ServerConfig.js index 096e461efe..3967f49f18 100644 --- a/src/components/views/auth/ServerConfig.js +++ b/src/components/views/auth/ServerConfig.js @@ -30,12 +30,22 @@ import SdkConfig from "../../../SdkConfig"; export default class ServerConfig extends React.PureComponent { static propTypes = { - onServerConfigChange: PropTypes.func, + onServerConfigChange: PropTypes.func.isRequired, // The current configuration that the user is expecting to change. serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, delayTimeMs: PropTypes.number, // time to wait before invoking onChanged + + // Called after the component calls onServerConfigChange + onAfterSubmit: PropTypes.func, + + // Optional text for the submit button. If falsey, no button will be shown. + submitText: PropTypes.string, + + // Optional class for the submit button. Only applies if the submit button + // is to be rendered. + submitClass: PropTypes.string, }; static defaultProps = { @@ -124,6 +134,16 @@ export default class ServerConfig extends React.PureComponent { this.setState({ isUrl }); }; + onSubmit = async (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + await this.validateServer(); + + if (this.props.onAfterSubmit) { + this.props.onAfterSubmit(); + } + }; + _waitThenInvoke(existingTimeoutId, fn) { if (existingTimeoutId) { clearTimeout(existingTimeoutId); @@ -138,11 +158,21 @@ export default class ServerConfig extends React.PureComponent { render() { const Field = sdk.getComponent('elements.Field'); + const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); const errorText = this.state.errorText ? {this.state.errorText} : null; + const submitButton = this.props.submitText + ? {this.props.submitText} + : null; + return (

{_t("Other servers")}

@@ -152,24 +182,27 @@ export default class ServerConfig extends React.PureComponent { , })} {errorText} -
- - -
+
+
+ + +
+ {submitButton} +
); } From e4576dac28cad4e5e18b04d4f5a1dbd433dfa59f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 13 May 2019 17:16:40 -0600 Subject: [PATCH 0027/1310] Render underlines and tooltips on custom server names in auth pages See https://github.com/vector-im/riot-web/issues/9290 --- res/css/_components.scss | 1 + res/css/structures/auth/_Login.scss | 4 ++ res/css/views/elements/_TextWithTooltip.scss | 19 +++++++ .../structures/auth/ForgotPassword.js | 11 +++- src/components/views/auth/PasswordLogin.js | 11 +++- src/components/views/auth/RegistrationForm.js | 11 +++- .../views/elements/TextWithTooltip.js | 56 +++++++++++++++++++ src/components/views/elements/Tooltip.js | 4 ++ 8 files changed, 111 insertions(+), 6 deletions(-) create mode 100644 res/css/views/elements/_TextWithTooltip.scss create mode 100644 src/components/views/elements/TextWithTooltip.js diff --git a/res/css/_components.scss b/res/css/_components.scss index 6e681894e3..ff22ad9eab 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -97,6 +97,7 @@ @import "./views/elements/_RoleButton.scss"; @import "./views/elements/_Spinner.scss"; @import "./views/elements/_SyntaxHighlight.scss"; +@import "./views/elements/_TextWithTooltip.scss"; @import "./views/elements/_ToggleSwitch.scss"; @import "./views/elements/_ToolTipButton.scss"; @import "./views/elements/_Tooltip.scss"; diff --git a/res/css/structures/auth/_Login.scss b/res/css/structures/auth/_Login.scss index 2cf6276557..4eff5c33e4 100644 --- a/res/css/structures/auth/_Login.scss +++ b/res/css/structures/auth/_Login.scss @@ -79,3 +79,7 @@ limitations under the License. .mx_Login_type_dropdown { min-width: 200px; } + +.mx_Login_underlinedServerName { + border-bottom: 1px dashed $accent-color; +} diff --git a/res/css/views/elements/_TextWithTooltip.scss b/res/css/views/elements/_TextWithTooltip.scss new file mode 100644 index 0000000000..5b34e7a3be --- /dev/null +++ b/res/css/views/elements/_TextWithTooltip.scss @@ -0,0 +1,19 @@ +/* +Copyright 2019 New Vector Ltd. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_TextWithTooltip_tooltip { + display: none; +} \ No newline at end of file diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 5316235fe0..91ed1aa3ae 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -194,9 +194,16 @@ module.exports = React.createClass({ serverName: this.props.serverConfig.hsName, }); if (this.props.serverConfig.hsNameIsDifferent) { - // TODO: TravisR - Use tooltip to underline + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + yourMatrixAccountText = _t('Your Matrix account on ', {}, { - 'underlinedServerName': () => {this.props.serverConfig.hsName}, + 'underlinedServerName': () => { + return ; + }, }); } diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index f5b2aec210..80716b766c 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -279,9 +279,16 @@ export default class PasswordLogin extends React.Component { serverName: this.props.serverConfig.hsName, }); if (this.props.serverConfig.hsNameIsDifferent) { - // TODO: TravisR - Use tooltip to underline + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + signInToText = _t('Sign in to your Matrix account on ', {}, { - 'underlinedServerName': () => {this.props.serverConfig.hsName}, + 'underlinedServerName': () => { + return ; + }, }); } diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index 0eecc6b826..be0142e6c6 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -516,9 +516,16 @@ module.exports = React.createClass({ serverName: this.props.serverConfig.hsName, }); if (this.props.serverConfig.hsNameIsDifferent) { - // TODO: TravisR - Use tooltip to underline + const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip"); + yourMatrixAccountText = _t('Create your Matrix account on ', {}, { - 'underlinedServerName': () => {this.props.serverConfig.hsName}, + 'underlinedServerName': () => { + return ; + }, }); } diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js new file mode 100644 index 0000000000..757bcc9891 --- /dev/null +++ b/src/components/views/elements/TextWithTooltip.js @@ -0,0 +1,56 @@ +/* + Copyright 2019 New Vector Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import React from 'react'; +import PropTypes from 'prop-types'; +import sdk from '../../../index'; + +export default class TextWithTooltip extends React.Component { + static propTypes = { + class: PropTypes.string, + tooltip: PropTypes.string.isRequired, + }; + + constructor() { + super(); + + this.state = { + hover: false, + }; + } + + onMouseOver = () => { + this.setState({hover: true}); + }; + + onMouseOut = () => { + this.setState({hover: false}); + }; + + render() { + const Tooltip = sdk.getComponent("elements.Tooltip"); + + return ( + + {this.props.children} + + + ); + } +} \ No newline at end of file diff --git a/src/components/views/elements/Tooltip.js b/src/components/views/elements/Tooltip.js index 1cc82978ed..1d6b54f413 100644 --- a/src/components/views/elements/Tooltip.js +++ b/src/components/views/elements/Tooltip.js @@ -79,6 +79,10 @@ module.exports = React.createClass({ let offset = 0; if (parentBox.height > MIN_TOOLTIP_HEIGHT) { offset = Math.floor((parentBox.height - MIN_TOOLTIP_HEIGHT) / 2); + } else { + // The tooltip is larger than the parent height: figure out what offset + // we need so that we're still centered. + offset = Math.floor(parentBox.height - MIN_TOOLTIP_HEIGHT); } style.top = (parentBox.top - 2) + window.pageYOffset + offset; style.left = 6 + parentBox.right + window.pageXOffset; From 25e3f7888e5453cb25415e2c27ecf09f3c0c4b04 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 13 May 2019 18:31:43 -0600 Subject: [PATCH 0028/1310] newline for the linter --- src/components/views/elements/TextWithTooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/TextWithTooltip.js b/src/components/views/elements/TextWithTooltip.js index 757bcc9891..61c3a2125a 100644 --- a/src/components/views/elements/TextWithTooltip.js +++ b/src/components/views/elements/TextWithTooltip.js @@ -53,4 +53,4 @@ export default class TextWithTooltip extends React.Component { ); } -} \ No newline at end of file +} From a62f68bd399341ce9810b5d8ff4da3904613dc6c Mon Sep 17 00:00:00 2001 From: Pierre Boyer Date: Tue, 14 May 2019 13:44:01 +0200 Subject: [PATCH 0029/1310] Hide autocomplete on Enter key press instead of sending message --- src/components/views/rooms/MessageComposerInput.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/rooms/MessageComposerInput.js b/src/components/views/rooms/MessageComposerInput.js index e54ddd6787..d8ad9930c8 100644 --- a/src/components/views/rooms/MessageComposerInput.js +++ b/src/components/views/rooms/MessageComposerInput.js @@ -1046,6 +1046,12 @@ export default class MessageComposerInput extends React.Component { return change.insertText('\n'); } + if (this.autocomplete.countCompletions() > 0) { + this.autocomplete.hide(); + ev.preventDefault(); + return true; + } + const editorState = this.state.editorState; const lastBlock = editorState.blocks.last(); From bb163576360e0defc3265f12fc08c5949f97ee69 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 May 2019 13:06:56 -0600 Subject: [PATCH 0030/1310] Flag all generated configs as non-default by default The app is expected to flag a particular config themselves as default. This is primarily intended so that other parts of the app can determine what to do based on whether or not the config is a default config. See https://github.com/vector-im/riot-web/issues/9290 --- src/utils/AutoDiscoveryUtils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/utils/AutoDiscoveryUtils.js b/src/utils/AutoDiscoveryUtils.js index 318c706136..0850039344 100644 --- a/src/utils/AutoDiscoveryUtils.js +++ b/src/utils/AutoDiscoveryUtils.js @@ -26,6 +26,8 @@ export class ValidatedServerConfig { isUrl: string; identityEnabled: boolean; + + isDefault: boolean; } export default class AutoDiscoveryUtils { @@ -99,6 +101,7 @@ export default class AutoDiscoveryUtils { hsNameIsDifferent: url.hostname !== preferredHomeserverName, isUrl: preferredIdentityUrl, identityEnabled: !SdkConfig.get()['disable_identity_server'], + isDefault: false, }); } } From 34719b9a2e8f4734d9a9367823142ffdb7a32655 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 May 2019 13:10:02 -0600 Subject: [PATCH 0031/1310] Only expose the fallback_hs_url if the homeserver is the default HS See https://github.com/vector-im/riot-web/issues/9290 --- src/components/structures/MatrixChat.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 4198980a17..38f597f673 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -203,9 +203,12 @@ export default React.createClass({ }; }, - // TODO: TravisR - Remove this or put it somewhere else getFallbackHsUrl: function() { - return this.props.config.fallback_hs_url; + if (this.props.serverConfig.isDefault) { + return this.props.config.fallback_hs_url; + } else { + return null; + } }, getServerProperties() { From 626cb46915d3ff535156dd1114114fadb4942d2d Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 14 May 2019 21:05:22 +0100 Subject: [PATCH 0032/1310] Cleanup interface buttons --- res/css/views/settings/_Notifications.scss | 8 ++++++++ .../tabs/room/NotificationSettingsTab.js | 18 +++++++++++++++--- src/i18n/strings/en_EN.json | 3 ++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss index d6c0b5dbeb..b21e7a5113 100644 --- a/res/css/views/settings/_Notifications.scss +++ b/res/css/views/settings/_Notifications.scss @@ -71,3 +71,11 @@ limitations under the License. .mx_UserNotifSettings_notifTable .mx_Spinner { position: absolute; } + +.mx_NotificationSound_soundUpload { + display: none; +} + +.mx_NotificationSound_resetSound { + margin-left: 5px; +} \ No newline at end of file diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index e7ed14b491..9d0848baf4 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -46,6 +46,13 @@ export default class NotificationsSettingsTab extends React.Component { }); } + async _triggerUploader(e) { + e.stopPropagation(); + e.preventDefault(); + + this.refs.soundUpload.click(); + } + async _onSoundUploadChanged(e) { if (!e.target.files || !e.target.files.length) { this.setState({ @@ -133,10 +140,15 @@ export default class NotificationsSettingsTab extends React.Component {

{_t("Set a new custom sound")}

- +
- - {_t("Reset to default sound")} + + + {_t("Browse")} + + + + {_t("Reset")}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index d841d5a34b..5070bb7d30 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -616,7 +616,8 @@ "URL Previews": "URL Previews", "Sounds": "Sounds", "Set a new custom sound": "Set a new custom sound", - "Reset to default sound": "Reset to default sound", + "Browse": "Browse", + "Reset": "Reset", "Change room avatar": "Change room avatar", "Change room name": "Change room name", "Change main address for the room": "Change main address for the room", From 079bdd44a4cfc5d45df54932db08a9bdf0f4e5c9 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 14 May 2019 21:07:29 +0100 Subject: [PATCH 0033/1310] Update il8n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 6747e67833..70128c4f8b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -301,7 +301,6 @@ "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", "Custom Notification Sounds": "Custom Notification Sounds", - "React to messages with emoji": "React to messages with emoji", "React to messages with emoji (refresh to apply changes)": "React to messages with emoji (refresh to apply changes)", "Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing", "Use compact timeline layout": "Use compact timeline layout", From 9b5830bb080a25781cac66e1807f16a87a9e1ed5 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 14 May 2019 15:01:22 -0600 Subject: [PATCH 0034/1310] Restore use of full mxid login See https://github.com/vector-im/riot-web/issues/9290 --- src/components/structures/auth/Login.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index af9370f2db..3fc7aad50d 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -25,7 +25,7 @@ import sdk from '../../../index'; import Login from '../../../Login'; import SdkConfig from '../../../SdkConfig'; import { messageForResourceLimitError } from '../../../utils/ErrorUtils'; -import {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; +import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; // For validating phone numbers without country codes const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; @@ -235,21 +235,25 @@ module.exports = React.createClass({ this.setState({ username: username }); }, - onUsernameBlur: function(username) { + onUsernameBlur: async function(username) { this.setState({ username: username, + busy: true, // unset later by the result of onServerConfigChange errorText: null, }); - if (username[0] === "@" && false) { // TODO: TravisR - Restore this + if (username[0] === "@") { const serverName = username.split(':').slice(1).join(':'); try { - // we have to append 'https://' to make the URL constructor happy - // otherwise we get things like 'protocol: matrix.org, pathname: 8448' - const url = new URL("https://" + serverName); - this._tryWellKnownDiscovery(url.hostname); + const result = await AutoDiscoveryUtils.validateServerName(serverName); + this.props.onServerConfigChange(result); } catch (e) { console.error("Problem parsing URL or unhandled error doing .well-known discovery:", e); - this.setState({errorText: _t("Failed to perform homeserver discovery")}); + + let message = _t("Failed to perform homeserver discovery"); + if (e.translatedMessage) { + message = e.translatedMessage; + } + this.setState({errorText: message, busy: false}); } } }, From d752de09728042a0d3dd7c1c42cf5c59c91259fa Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Wed, 15 May 2019 15:52:42 +0100 Subject: [PATCH 0035/1310] Improve UX --- res/css/views/settings/_Notifications.scss | 17 ++++++++- .../tabs/room/NotificationSettingsTab.js | 36 ++++++++++++++----- src/i18n/strings/en_EN.json | 4 ++- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/res/css/views/settings/_Notifications.scss b/res/css/views/settings/_Notifications.scss index b21e7a5113..773ea055df 100644 --- a/res/css/views/settings/_Notifications.scss +++ b/res/css/views/settings/_Notifications.scss @@ -76,6 +76,21 @@ limitations under the License. display: none; } -.mx_NotificationSound_resetSound { +.mx_NotificationSound_browse { + color: $accent-color; + border: 1px solid $accent-color; + background-color: transparent; +} + +.mx_NotificationSound_save { margin-left: 5px; + color: white; + background-color: $accent-color; +} + +.mx_NotificationSound_resetSound { + margin-top: 5px; + color: white; + border: $warning-color; + background-color: $warning-color; } \ No newline at end of file diff --git a/src/components/views/settings/tabs/room/NotificationSettingsTab.js b/src/components/views/settings/tabs/room/NotificationSettingsTab.js index 9d0848baf4..6df5b2e469 100644 --- a/src/components/views/settings/tabs/room/NotificationSettingsTab.js +++ b/src/components/views/settings/tabs/room/NotificationSettingsTab.js @@ -20,7 +20,7 @@ import {_t} from "../../../../../languageHandler"; import MatrixClientPeg from "../../../../../MatrixClientPeg"; import AccessibleButton from "../../../elements/AccessibleButton"; import Notifier from "../../../../../Notifier"; -import SettingsStore from '../../../../../settings/SettingsStore'; +import SettingsStore from '../../../../../settings/SettingsStore'; import { SettingLevel } from '../../../../../settings/SettingsStore'; export default class NotificationsSettingsTab extends React.Component { @@ -65,14 +65,19 @@ export default class NotificationsSettingsTab extends React.Component { this.setState({ uploadedFile: file, }); + } + + async _onClickSaveSound(e) { + e.stopPropagation(); + e.preventDefault(); try { await this._saveSound(); } catch (ex) { console.error( `Unable to save notification sound for ${this.props.roomId}`, - ex, ); + console.error(ex); } } @@ -80,6 +85,7 @@ export default class NotificationsSettingsTab extends React.Component { if (!this.state.uploadedFile) { return; } + let type = this.state.uploadedFile.type; if (type === "video/ogg") { // XXX: I've observed browsers allowing users to pick a audio/ogg files, @@ -87,6 +93,7 @@ export default class NotificationsSettingsTab extends React.Component { // suck at detecting mimetypes. type = "audio/ogg"; } + const url = await MatrixClientPeg.get().uploadContent( this.state.uploadedFile, { type, @@ -96,7 +103,6 @@ export default class NotificationsSettingsTab extends React.Component { await SettingsStore.setValue( "notificationSound", this.props.roomId, - SettingsStore. SettingLevel.ROOM_ACCOUNT, { name: this.state.uploadedFile.name, @@ -108,7 +114,6 @@ export default class NotificationsSettingsTab extends React.Component { this.setState({ uploadedFile: null, - uploadedFileUrl: null, currentSound: this.state.uploadedFile.name, }); } @@ -129,13 +134,25 @@ export default class NotificationsSettingsTab extends React.Component { } render() { + let currentUploadedFile = null; + if (this.state.uploadedFile) { + currentUploadedFile = ( +
+ {_t("Uploaded sound")}: {this.state.uploadedFile.name} +
+ ); + } + return (
{_t("Notifications")}
{_t("Sounds")}
- {_t("Custom Notification Sounds")}: {this.state.currentSound} + {_t("Notification sound")}: {this.state.currentSound}
+ + {_t("Reset")} +

{_t("Set a new custom sound")}

@@ -143,13 +160,16 @@ export default class NotificationsSettingsTab extends React.Component { - + {currentUploadedFile} + + {_t("Browse")} - - {_t("Reset")} + + {_t("Save")} +
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 70128c4f8b..91829a80b4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -617,10 +617,12 @@ "Room Addresses": "Room Addresses", "Publish this room to the public in %(domain)s's room directory?": "Publish this room to the public in %(domain)s's room directory?", "URL Previews": "URL Previews", + "Uploaded sound": "Uploaded sound", "Sounds": "Sounds", + "Notification sound": "Notification sound", + "Reset": "Reset", "Set a new custom sound": "Set a new custom sound", "Browse": "Browse", - "Reset": "Reset", "Change room avatar": "Change room avatar", "Change room name": "Change room name", "Change main address for the room": "Change main address for the room", From 6e3b06f3646b83d4e271e51ca894c718474d8c27 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 15 May 2019 13:55:50 -0600 Subject: [PATCH 0036/1310] Human de-linting --- src/components/structures/auth/ForgotPassword.js | 3 ++- src/components/views/auth/PasswordLogin.js | 5 +++-- src/components/views/auth/RegistrationForm.js | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/structures/auth/ForgotPassword.js b/src/components/structures/auth/ForgotPassword.js index 91ed1aa3ae..42ca23256c 100644 --- a/src/components/structures/auth/ForgotPassword.js +++ b/src/components/structures/auth/ForgotPassword.js @@ -200,7 +200,8 @@ module.exports = React.createClass({ 'underlinedServerName': () => { return ; }, diff --git a/src/components/views/auth/PasswordLogin.js b/src/components/views/auth/PasswordLogin.js index 80716b766c..825bffdc84 100644 --- a/src/components/views/auth/PasswordLogin.js +++ b/src/components/views/auth/PasswordLogin.js @@ -284,8 +284,9 @@ export default class PasswordLogin extends React.Component { signInToText = _t('Sign in to your Matrix account on ', {}, { 'underlinedServerName': () => { return ; }, diff --git a/src/components/views/auth/RegistrationForm.js b/src/components/views/auth/RegistrationForm.js index be0142e6c6..b1af6ea42c 100644 --- a/src/components/views/auth/RegistrationForm.js +++ b/src/components/views/auth/RegistrationForm.js @@ -522,7 +522,8 @@ module.exports = React.createClass({ 'underlinedServerName': () => { return ; }, From 595b490fd7130c0a53e61902723627358e60ce58 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 May 2019 20:00:11 -0600 Subject: [PATCH 0037/1310] Don't act busy on the login page for moving your cursor If you were in the username field and simply tabbed out without entering anything, the form would become "busy" and not let you submit. We should only be doing this if we have work to do, like .well-known discovery of the homeserver. --- src/components/structures/auth/Login.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/structures/auth/Login.js b/src/components/structures/auth/Login.js index 393c640604..aed8e9fca0 100644 --- a/src/components/structures/auth/Login.js +++ b/src/components/structures/auth/Login.js @@ -236,12 +236,13 @@ module.exports = React.createClass({ }, onUsernameBlur: async function(username) { + const doWellknownLookup = username[0] === "@"; this.setState({ username: username, - busy: true, // unset later by the result of onServerConfigChange + busy: doWellknownLookup, // unset later by the result of onServerConfigChange errorText: null, }); - if (username[0] === "@") { + if (doWellknownLookup) { const serverName = username.split(':').slice(1).join(':'); try { const result = await AutoDiscoveryUtils.validateServerName(serverName); From 7ecab350628f4454058d25406dc2b5c414ca52ca Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 20 May 2019 20:02:02 -0600 Subject: [PATCH 0038/1310] Add a null guard for serverConfig This is often null while the component is on its first render, and is called during that render. It is eventually populated by React, and the function re-called - we just have to be patient. --- src/components/structures/MatrixChat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 38f597f673..ac328f8387 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -204,7 +204,7 @@ export default React.createClass({ }, getFallbackHsUrl: function() { - if (this.props.serverConfig.isDefault) { + if (this.props.serverConfig && this.props.serverConfig.isDefault) { return this.props.config.fallback_hs_url; } else { return null; From a551ef1a7256d2aec2169d83fc3ac7ed2ffa8f29 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 21 May 2019 20:00:15 -0600 Subject: [PATCH 0039/1310] Label message edit field as such for screen readers See https://github.com/vector-im/riot-web/issues/9747 --- src/components/views/elements/MessageEditor.js | 1 + src/i18n/strings/en_EN.json | 1 + 2 files changed, 2 insertions(+) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index 1f3440d740..24b22b4ca2 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -187,6 +187,7 @@ export default class MessageEditor extends React.Component { onInput={this._onInput} onKeyDown={this._onKeyDown} ref={ref => this._editorRef = ref} + aria-label={_t("Edit message")} >
{_t("Cancel")} diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index c2e633d566..8e10d276d3 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1052,6 +1052,7 @@ "%(oneUser)schanged their avatar %(count)s times|one": "%(oneUser)schanged their avatar", "collapse": "collapse", "expand": "expand", + "Edit message": "Edit message", "Power level": "Power level", "Custom level": "Custom level", "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.": "Unable to load event that was replied to, it either does not exist or you do not have permission to view it.", From 5373007301986335e89bc1262bea86e86bf36671 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 21 May 2019 17:34:18 +0200 Subject: [PATCH 0040/1310] initial attempt at converting html back to markdown --- src/editor/deserialize.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index d440f9d336..71fe71d68a 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -40,11 +40,25 @@ function parseHtmlMessage(html, room) { switch (prefix) { case "@": return new UserPillPart(resourceId, n.textContent, room.getMember(resourceId)); case "#": return new RoomPillPart(resourceId); - default: return new PlainPart(n.textContent); + default: { + if (href === n.textContent) { + return new PlainPart(n.textContent); + } else { + return new PlainPart(`[${n.textContent}](${href})`); + } + } } } case "BR": return new NewlinePart("\n"); + case "EM": + return new PlainPart(`*${n.textContent}*`); + case "STRONG": + return new PlainPart(`**${n.textContent}**`); + case "PRE": + return new PlainPart(`\`\`\`${n.textContent}\`\`\``); + case "CODE": + return new PlainPart(`\`${n.textContent}\``); default: return new PlainPart(n.textContent); } From 53b6586986ccbbe8e8390091f0c3c54061ab76fd Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 21 May 2019 17:59:54 +0200 Subject: [PATCH 0041/1310] re-apply markdown when saving a message --- src/components/views/elements/MessageEditor.js | 7 ++++--- src/editor/serialize.js | 16 +++++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/MessageEditor.js b/src/components/views/elements/MessageEditor.js index b07eca22b6..cb5767b192 100644 --- a/src/components/views/elements/MessageEditor.js +++ b/src/components/views/elements/MessageEditor.js @@ -22,7 +22,7 @@ import dis from '../../../dispatcher'; import EditorModel from '../../../editor/model'; import {setCaretPosition} from '../../../editor/caret'; import {getCaretOffsetAndText} from '../../../editor/dom'; -import {htmlSerialize, textSerialize, requiresHtml} from '../../../editor/serialize'; +import {htmlSerializeIfNeeded, textSerialize} from '../../../editor/serialize'; import {parseEvent} from '../../../editor/deserialize'; import Autocomplete from '../rooms/Autocomplete'; import {PartCreator} from '../../../editor/parts'; @@ -128,9 +128,10 @@ export default class MessageEditor extends React.Component { msgtype: newContent.msgtype, body: ` * ${newContent.body}`, }; - if (requiresHtml(this.model)) { + const formattedBody = htmlSerializeIfNeeded(this.model); + if (formattedBody) { newContent.format = "org.matrix.custom.html"; - newContent.formatted_body = htmlSerialize(this.model); + newContent.formatted_body = formattedBody; contentBody.format = newContent.format; contentBody.formatted_body = ` * ${newContent.formatted_body}`; } diff --git a/src/editor/serialize.js b/src/editor/serialize.js index 1724e4a2b7..73fbbe5d01 100644 --- a/src/editor/serialize.js +++ b/src/editor/serialize.js @@ -15,21 +15,31 @@ See the License for the specific language governing permissions and limitations under the License. */ -export function htmlSerialize(model) { +import Markdown from '../Markdown'; + +export function mdSerialize(model) { return model.parts.reduce((html, part) => { switch (part.type) { case "newline": - return html + "
"; + return html + "\n"; case "plain": case "pill-candidate": return html + part.text; case "room-pill": case "user-pill": - return html + `${part.text}`; + return html + `[${part.text}](https://matrix.to/#/${part.resourceId})`; } }, ""); } +export function htmlSerializeIfNeeded(model) { + const md = mdSerialize(model); + const parser = new Markdown(md); + if (!parser.isPlainText()) { + return parser.toHTML(); + } +} + export function textSerialize(model) { return model.parts.reduce((text, part) => { switch (part.type) { From 5f5a2f71402c1c1bcf2e4772e2c56b1d3a2d1432 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 21 May 2019 19:45:12 +0200 Subject: [PATCH 0042/1310] put code block on new line --- src/editor/deserialize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 71fe71d68a..8a410ea105 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -56,7 +56,7 @@ function parseHtmlMessage(html, room) { case "STRONG": return new PlainPart(`**${n.textContent}**`); case "PRE": - return new PlainPart(`\`\`\`${n.textContent}\`\`\``); + return new PlainPart(`\`\`\`\n${n.textContent}\`\`\``); case "CODE": return new PlainPart(`\`${n.textContent}\``); default: From 723086e4d768aa2d17f69cde06bede41c1128862 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Wed, 22 May 2019 13:00:39 +0200 Subject: [PATCH 0043/1310] Decend into P & DIV elements while parsing a message. Also split on newline so all newlines are represented by a newlinepart --- src/editor/deserialize.js | 119 ++++++++++++++++++++++++++++---------- 1 file changed, 88 insertions(+), 31 deletions(-) diff --git a/src/editor/deserialize.js b/src/editor/deserialize.js index 8a410ea105..b3f4fe5b80 100644 --- a/src/editor/deserialize.js +++ b/src/editor/deserialize.js @@ -18,54 +18,111 @@ limitations under the License. import { MATRIXTO_URL_PATTERN } from '../linkify-matrix'; import { PlainPart, UserPillPart, RoomPillPart, NewlinePart } from "./parts"; +const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); + +function parseLink(a, parts, room) { + const {href} = a; + const pillMatch = REGEX_MATRIXTO.exec(href) || []; + const resourceId = pillMatch[1]; // The room/user ID + const prefix = pillMatch[2]; // The first character of prefix + switch (prefix) { + case "@": + parts.push(new UserPillPart( + resourceId, + a.textContent, + room.getMember(resourceId), + )); + break; + case "#": + parts.push(new RoomPillPart(resourceId)); + break; + default: { + if (href === a.textContent) { + parts.push(new PlainPart(a.textContent)); + } else { + parts.push(new PlainPart(`[${a.textContent}](${href})`)); + } + break; + } + } +} + function parseHtmlMessage(html, room) { - const REGEX_MATRIXTO = new RegExp(MATRIXTO_URL_PATTERN); // no nodes from parsing here should be inserted in the document, // as scripts in event handlers, etc would be executed then. // we're only taking text, so that is fine - const nodes = Array.from(new DOMParser().parseFromString(html, "text/html").body.childNodes); - const parts = nodes.map(n => { + const root = new DOMParser().parseFromString(html, "text/html").body; + let n = root.firstChild; + const parts = []; + let isFirstNode = true; + while (n && n !== root) { switch (n.nodeType) { case Node.TEXT_NODE: - return new PlainPart(n.nodeValue); + // the plainpart doesn't accept \n and will cause + // a newlinepart to be created. + if (n.nodeValue !== "\n") { + parts.push(new PlainPart(n.nodeValue)); + } + break; case Node.ELEMENT_NODE: switch (n.nodeName) { - case "MX-REPLY": - return null; - case "A": { - const {href} = n; - const pillMatch = REGEX_MATRIXTO.exec(href) || []; - const resourceId = pillMatch[1]; // The room/user ID - const prefix = pillMatch[2]; // The first character of prefix - switch (prefix) { - case "@": return new UserPillPart(resourceId, n.textContent, room.getMember(resourceId)); - case "#": return new RoomPillPart(resourceId); - default: { - if (href === n.textContent) { - return new PlainPart(n.textContent); - } else { - return new PlainPart(`[${n.textContent}](${href})`); - } - } + case "DIV": + case "P": { + // block element should cause line break if not first + if (!isFirstNode) { + parts.push(new NewlinePart("\n")); + } + // decend into paragraph or div + if (n.firstChild) { + n = n.firstChild; + continue; + } else { + break; } } + case "A": { + parseLink(n, parts, room); + break; + } case "BR": - return new NewlinePart("\n"); + parts.push(new NewlinePart("\n")); + break; case "EM": - return new PlainPart(`*${n.textContent}*`); + parts.push(new PlainPart(`*${n.textContent}*`)); + break; case "STRONG": - return new PlainPart(`**${n.textContent}**`); - case "PRE": - return new PlainPart(`\`\`\`\n${n.textContent}\`\`\``); + parts.push(new PlainPart(`**${n.textContent}**`)); + break; + case "PRE": { + // block element should cause line break if not first + if (!isFirstNode) { + parts.push(new NewlinePart("\n")); + } + const preLines = `\`\`\`\n${n.textContent}\`\`\``.split("\n"); + preLines.forEach((l, i) => { + parts.push(new PlainPart(l)); + if (i < preLines.length - 1) { + parts.push(new NewlinePart("\n")); + } + }); + break; + } case "CODE": - return new PlainPart(`\`${n.textContent}\``); + parts.push(new PlainPart(`\`${n.textContent}\``)); + break; default: - return new PlainPart(n.textContent); + parts.push(new PlainPart(n.textContent)); + break; } - default: - return null; + break; } - }).filter(p => !!p); + // go up if we can't go next + if (!n.nextSibling) { + n = n.parentElement; + } + n = n.nextSibling; + isFirstNode = false; + } return parts; } From d0deffac3dad3feef48bdbb6f42dce3a376d1cb0 Mon Sep 17 00:00:00 2001 From: Andrew Morgan Date: Wed, 22 May 2019 15:19:42 +0100 Subject: [PATCH 0044/1310] Move checkmark to the front of key backup message --- src/components/views/settings/KeyBackupPanel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js index d8ed959dae..ec1e52a90c 100644 --- a/src/components/views/settings/KeyBackupPanel.js +++ b/src/components/views/settings/KeyBackupPanel.js @@ -180,7 +180,7 @@ export default class KeyBackupPanel extends React.PureComponent { if (MatrixClientPeg.get().getKeyBackupEnabled()) { clientBackupStatus =

{encryptedMessageAreEncrypted}

-

{_t("This device is backing up your keys. ")}✅

+

✅ {_t("This device is backing up your keys. ")}

; } else { clientBackupStatus =
From 85a024175b78229d949966aff7b9c7c9471c0818 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 21 May 2019 18:52:27 -0600 Subject: [PATCH 0045/1310] Hide flair from screen readers To have less noise when they run over the sender profile. See https://github.com/vector-im/riot-web/issues/9747 --- src/components/views/elements/Flair.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/Flair.js b/src/components/views/elements/Flair.js index aa629794ba..7d3d298804 100644 --- a/src/components/views/elements/Flair.js +++ b/src/components/views/elements/Flair.js @@ -45,12 +45,18 @@ class FlairAvatar extends React.Component { const tooltip = this.props.groupProfile.name ? `${this.props.groupProfile.name} (${this.props.groupProfile.groupId})`: this.props.groupProfile.groupId; + + // Note: we hide flair from screen readers but ideally we'd support + // reading something out on hover. There's no easy way to do this though, + // so instead we just hide it completely. return ; + title={tooltip} + aria-hidden={true} + />; } } From 6edf7609437eace7b134fe88a91623842e178ea1 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 21 May 2019 18:54:40 -0600 Subject: [PATCH 0046/1310] Mute avatars and read receipts on event tiles This reduces overall noise from the screen reader. It was reading the alt attribute from the sender avatar, which was just a mxid. The read receipts were just nonsensical noise. Fixes https://github.com/vector-im/riot-web/issues/2716 Fixes https://github.com/vector-im/riot-web/issues/5697 See https://github.com/vector-im/riot-web/issues/9747 --- src/components/views/elements/AccessibleButton.js | 4 ++-- src/components/views/rooms/EventTile.js | 12 +++++++++--- src/components/views/rooms/ReadReceiptMarker.js | 4 +++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/components/views/elements/AccessibleButton.js b/src/components/views/elements/AccessibleButton.js index 06c440c54e..19150682f0 100644 --- a/src/components/views/elements/AccessibleButton.js +++ b/src/components/views/elements/AccessibleButton.js @@ -67,8 +67,8 @@ export default function AccessibleButton(props) { restProps.ref = restProps.inputRef; delete restProps.inputRef; - restProps.tabIndex = restProps.tabIndex || "0"; - restProps.role = "button"; + restProps.tabIndex = restProps.tabIndex === undefined ? "0" : restProps.tabIndex; + restProps.role = restProps.role === undefined ? "button" : restProps.role; restProps.className = (restProps.className ? restProps.className + " " : "") + "mx_AccessibleButton"; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 6f0d555d4f..8d01042ed1 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -545,6 +545,8 @@ module.exports = withMatrixClient(React.createClass({ const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); + const muteScreenReader = isSending || !this.props.eventSendStatus; + const classes = classNames({ mx_EventTile: true, mx_EventTile_isEditing: this.props.isEditing, @@ -601,9 +603,13 @@ module.exports = withMatrixClient(React.createClass({ if (this.props.mxEvent.sender && avatarSize) { avatar = (
-
); @@ -773,7 +779,7 @@ module.exports = withMatrixClient(React.createClass({ 'replyThread', ); return ( -
+
{ readAvatars }
@@ -797,7 +803,7 @@ module.exports = withMatrixClient(React.createClass({ { actionBar }
{ - // The avatar goes after the event tile as it's absolutly positioned to be over the + // The avatar goes after the event tile as it's absolutely positioned to be over the // event tile line, so needs to be later in the DOM so it appears on top (this avoids // the need for further z-indexing chaos) } diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.js index 2f7a599d95..4025a36831 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.js @@ -211,11 +211,13 @@ module.exports = React.createClass({