diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6fa9cc29f9..e4a7ddc407 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,121 @@
+Changes in [3.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0) (2020-09-28)
+===================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0-rc.1...v3.5.0)
+
+ * Upgrade JS SDK to 8.4.1
+
+Changes in [3.5.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0-rc.1) (2020-09-23)
+=============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.1...v3.5.0-rc.1)
+
+ * Upgrade JS SDK to 8.4.0-rc.1
+ * Update from Weblate
+ [\#5246](https://github.com/matrix-org/matrix-react-sdk/pull/5246)
+ * Upgrade sanitize-html, set nesting limit
+ [\#5245](https://github.com/matrix-org/matrix-react-sdk/pull/5245)
+ * Add a note to use the desktop builds when seshat isn't available
+ [\#5225](https://github.com/matrix-org/matrix-react-sdk/pull/5225)
+ * Add some permission checks to the communities v2 prototype
+ [\#5240](https://github.com/matrix-org/matrix-react-sdk/pull/5240)
+ * Support HS-preferred Secure Backup setup methods
+ [\#5242](https://github.com/matrix-org/matrix-react-sdk/pull/5242)
+ * Only show User Info verify button if the other user has e2ee devices
+ [\#5234](https://github.com/matrix-org/matrix-react-sdk/pull/5234)
+ * Fix New Room List arrow key management
+ [\#5237](https://github.com/matrix-org/matrix-react-sdk/pull/5237)
+ * Fix Room Directory View & Preview actions for federated joins
+ [\#5235](https://github.com/matrix-org/matrix-react-sdk/pull/5235)
+ * Add a UI feature to disable advanced encryption options
+ [\#5238](https://github.com/matrix-org/matrix-react-sdk/pull/5238)
+ * UI Feature Flag: Communities
+ [\#5216](https://github.com/matrix-org/matrix-react-sdk/pull/5216)
+ * Rename apps back to widgets
+ [\#5236](https://github.com/matrix-org/matrix-react-sdk/pull/5236)
+ * Adjust layout and formatting of notifications / files cards
+ [\#5229](https://github.com/matrix-org/matrix-react-sdk/pull/5229)
+ * Fix Search Results Tile undefined variable access regression
+ [\#5232](https://github.com/matrix-org/matrix-react-sdk/pull/5232)
+ * Fix Cmd/Ctrl+Shift+U for File Upload
+ [\#5233](https://github.com/matrix-org/matrix-react-sdk/pull/5233)
+ * Disable the e2ee toggle when creating a room on a server with forced e2e
+ [\#5231](https://github.com/matrix-org/matrix-react-sdk/pull/5231)
+ * UI Feature Flag: Disable advanced options and tidy up some copy
+ [\#5215](https://github.com/matrix-org/matrix-react-sdk/pull/5215)
+ * UI Feature Flag: 3PIDs
+ [\#5228](https://github.com/matrix-org/matrix-react-sdk/pull/5228)
+ * Defer encryption setup until first E2EE room
+ [\#5219](https://github.com/matrix-org/matrix-react-sdk/pull/5219)
+ * Tidy devDeps, all the webpack stuff lives in the layer above
+ [\#5179](https://github.com/matrix-org/matrix-react-sdk/pull/5179)
+ * UI Feature Flag: Hide flair
+ [\#5214](https://github.com/matrix-org/matrix-react-sdk/pull/5214)
+ * UI Feature Flag: Identity server
+ [\#5218](https://github.com/matrix-org/matrix-react-sdk/pull/5218)
+ * UI Feature Flag: Share dialog QR code and social icons
+ [\#5221](https://github.com/matrix-org/matrix-react-sdk/pull/5221)
+ * UI Feature Flag: Registration, Password Reset, Deactivate
+ [\#5227](https://github.com/matrix-org/matrix-react-sdk/pull/5227)
+ * Retry joinRoom up to 5 times in the case of a 504 GATEWAY TIMEOUT
+ [\#5204](https://github.com/matrix-org/matrix-react-sdk/pull/5204)
+ * UI Feature Flag: Disable VoIP
+ [\#5217](https://github.com/matrix-org/matrix-react-sdk/pull/5217)
+ * Fix setState() usage in the constructor of RoomDirectory
+ [\#5224](https://github.com/matrix-org/matrix-react-sdk/pull/5224)
+ * Hide Analytics sections if piwik config is not provided
+ [\#5211](https://github.com/matrix-org/matrix-react-sdk/pull/5211)
+ * UI Feature Flag: Disable feedback button
+ [\#5213](https://github.com/matrix-org/matrix-react-sdk/pull/5213)
+ * Clean up UserInfo to not show a blank Power Selector for users not in room
+ [\#5220](https://github.com/matrix-org/matrix-react-sdk/pull/5220)
+ * Also hide bug reporting prompts from the Error Boundaries
+ [\#5212](https://github.com/matrix-org/matrix-react-sdk/pull/5212)
+ * Tactical improvements to 3PID invites
+ [\#5201](https://github.com/matrix-org/matrix-react-sdk/pull/5201)
+ * If no bug_report_endpoint_url, hide rageshaking from the App
+ [\#5210](https://github.com/matrix-org/matrix-react-sdk/pull/5210)
+ * Introduce a concept of UI features, using it for URL previews at first
+ [\#5208](https://github.com/matrix-org/matrix-react-sdk/pull/5208)
+ * Remove defunct "always show encryption icons" setting
+ [\#5207](https://github.com/matrix-org/matrix-react-sdk/pull/5207)
+ * Don't show Notifications Prompt Toast if user has master rule enabled
+ [\#5203](https://github.com/matrix-org/matrix-react-sdk/pull/5203)
+ * Fix Bridges tab crashing when the room does not have bridges
+ [\#5206](https://github.com/matrix-org/matrix-react-sdk/pull/5206)
+ * Don't count widgets which no longer exist towards pinned count
+ [\#5202](https://github.com/matrix-org/matrix-react-sdk/pull/5202)
+ * Fix crashes with cannot read isResizing of undefined
+ [\#5205](https://github.com/matrix-org/matrix-react-sdk/pull/5205)
+ * Prompt to remove the jitsi widget when pressing the call button
+ [\#5193](https://github.com/matrix-org/matrix-react-sdk/pull/5193)
+ * Show verification status in the room summary card
+ [\#5195](https://github.com/matrix-org/matrix-react-sdk/pull/5195)
+ * Fix user info scrolling in new card view
+ [\#5198](https://github.com/matrix-org/matrix-react-sdk/pull/5198)
+ * Fix sticker picker height
+ [\#5197](https://github.com/matrix-org/matrix-react-sdk/pull/5197)
+ * Call jitsi widgets 'group calls'
+ [\#5191](https://github.com/matrix-org/matrix-react-sdk/pull/5191)
+ * Don't show 'unpin' for persistent widgets
+ [\#5194](https://github.com/matrix-org/matrix-react-sdk/pull/5194)
+ * Split up cross-signing and secure backup settings
+ [\#5182](https://github.com/matrix-org/matrix-react-sdk/pull/5182)
+ * Fix onNewScreen to use replace when going from roomId->roomAlias
+ [\#5185](https://github.com/matrix-org/matrix-react-sdk/pull/5185)
+ * bring back 1.2M style badge counts rather than 99+
+ [\#5192](https://github.com/matrix-org/matrix-react-sdk/pull/5192)
+ * Run the rageshake command through the bug report dialog
+ [\#5189](https://github.com/matrix-org/matrix-react-sdk/pull/5189)
+ * Account for via in pill matching regex
+ [\#5188](https://github.com/matrix-org/matrix-react-sdk/pull/5188)
+ * Remove now-unused create-react-class from lockfile
+ [\#5187](https://github.com/matrix-org/matrix-react-sdk/pull/5187)
+ * Fixed 1px jump upwards
+ [\#5163](https://github.com/matrix-org/matrix-react-sdk/pull/5163)
+ * Always allow widgets when using the local version
+ [\#5184](https://github.com/matrix-org/matrix-react-sdk/pull/5184)
+ * Migrate RoomView and RoomContext to Typescript
+ [\#5175](https://github.com/matrix-org/matrix-react-sdk/pull/5175)
+
Changes in [3.4.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.1) (2020-09-14)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.0...v3.4.1)
diff --git a/__mocks__/browser-request.js b/__mocks__/browser-request.js
index 7d231fb9db..4c59e8a43a 100644
--- a/__mocks__/browser-request.js
+++ b/__mocks__/browser-request.js
@@ -1,5 +1,10 @@
const en = require("../src/i18n/strings/en_EN");
+const de = require("../src/i18n/strings/de_DE");
+// Mock the browser-request for the languageHandler tests to return
+// Fake languages.json containing references to en_EN and de_DE
+// en_EN.json
+// de_DE.json
module.exports = jest.fn((opts, cb) => {
const url = opts.url || opts.uri;
if (url && url.endsWith("languages.json")) {
@@ -8,9 +13,15 @@ module.exports = jest.fn((opts, cb) => {
"fileName": "en_EN.json",
"label": "English",
},
+ "de": {
+ "fileName": "de_DE.json",
+ "label": "German",
+ },
}));
} else if (url && url.endsWith("en_EN.json")) {
cb(undefined, {status: 200}, JSON.stringify(en));
+ } else if (url && url.endsWith("de_DE.json")) {
+ cb(undefined, {status: 200}, JSON.stringify(de));
} else {
cb(true, {status: 404}, "");
}
diff --git a/package.json b/package.json
index 22df9c37c2..3ab523ee9a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.4.1",
+ "version": "3.5.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -95,7 +95,7 @@
"react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.1",
"rfc4648": "^1.4.0",
- "sanitize-html": "^1.27.1",
+ "sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db",
"tar-js": "^0.3.0",
"text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0",
@@ -149,7 +149,6 @@
"eslint-plugin-flowtype": "^2.50.3",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^2.5.1",
- "file-loader": "^3.0.1",
"glob": "^5.0.15",
"jest": "^24.9.0",
"jest-canvas-mock": "^2.2.0",
@@ -158,7 +157,6 @@
"matrix-react-test-utils": "^0.2.2",
"react-test-renderer": "^16.13.1",
"rimraf": "^2.7.1",
- "source-map-loader": "^0.2.4",
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.3.0",
"stylelint-scss": "^3.18.0",
diff --git a/res/css/_common.scss b/res/css/_common.scss
index a22d77f3d3..aafd6e5297 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -18,6 +18,8 @@ limitations under the License.
@import "./_font-sizes.scss";
+$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
+
:root {
font-size: 10px;
}
diff --git a/res/css/_components.scss b/res/css/_components.scss
index 26ad802955..261b35690e 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -91,15 +91,17 @@
@import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
-@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
-@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
-@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
-@import "./views/dialogs/secretstorage/_AccessSecretStorageDialog.scss";
-@import "./views/dialogs/secretstorage/_CreateSecretStorageDialog.scss";
+@import "./views/dialogs/security/_AccessSecretStorageDialog.scss";
+@import "./views/dialogs/security/_CreateCrossSigningDialog.scss";
+@import "./views/dialogs/security/_CreateKeyBackupDialog.scss";
+@import "./views/dialogs/security/_CreateSecretStorageDialog.scss";
+@import "./views/dialogs/security/_KeyBackupFailedDialog.scss";
+@import "./views/dialogs/security/_RestoreKeyBackupDialog.scss";
@import "./views/directory/_NetworkDropdown.scss";
@import "./views/elements/_AccessibleButton.scss";
@import "./views/elements/_AddressSelector.scss";
@import "./views/elements/_AddressTile.scss";
+@import "./views/elements/_DesktopBuildsNotice.scss";
@import "./views/elements/_DirectorySearchBox.scss";
@import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss";
@@ -188,7 +190,6 @@
@import "./views/rooms/_RoomHeader.scss";
@import "./views/rooms/_RoomList.scss";
@import "./views/rooms/_RoomPreviewBar.scss";
-@import "./views/rooms/_RoomRecoveryReminder.scss";
@import "./views/rooms/_RoomSublist.scss";
@import "./views/rooms/_RoomTile.scss";
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
diff --git a/res/css/structures/_FilePanel.scss b/res/css/structures/_FilePanel.scss
index 21b30d804a..2aa068b674 100644
--- a/res/css/structures/_FilePanel.scss
+++ b/res/css/structures/_FilePanel.scss
@@ -23,6 +23,13 @@ limitations under the License.
.mx_FilePanel .mx_RoomView_messageListWrapper {
margin-right: 20px;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+}
+
+.mx_FilePanel .mx_RoomView_MessageList {
+ width: 100%;
}
.mx_FilePanel .mx_RoomView_MessageList h2 {
diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss
index 715a94fe2c..1258ace069 100644
--- a/res/css/structures/_NotificationPanel.scss
+++ b/res/css/structures/_NotificationPanel.scss
@@ -22,7 +22,13 @@ limitations under the License.
}
.mx_NotificationPanel .mx_RoomView_messageListWrapper {
- margin-right: 20px;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+}
+
+.mx_NotificationPanel .mx_RoomView_MessageList {
+ width: 100%;
}
.mx_NotificationPanel .mx_RoomView_MessageList h2 {
@@ -35,11 +41,32 @@ limitations under the License.
.mx_NotificationPanel .mx_EventTile {
word-break: break-word;
+ position: relative;
+ padding-bottom: 18px;
+
+ &:not(.mx_EventTile_last):not(.mx_EventTile_lastInSection)::after {
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ background-color: $tertiary-fg-color;
+ height: 1px;
+ opacity: 0.4;
+ content: '';
+ }
}
.mx_NotificationPanel .mx_EventTile_roomName {
font-weight: bold;
font-size: $font-14px;
+
+ > * {
+ vertical-align: middle;
+ }
+
+ > .mx_BaseAvatar {
+ margin-right: 8px;
+ }
}
.mx_NotificationPanel .mx_EventTile_roomName a {
@@ -47,8 +74,7 @@ limitations under the License.
}
.mx_NotificationPanel .mx_EventTile_avatar {
- top: 8px;
- left: 0px;
+ display: none; // we don't need this in this view
}
.mx_NotificationPanel .mx_EventTile .mx_SenderProfile,
@@ -60,8 +86,7 @@ limitations under the License.
}
.mx_NotificationPanel .mx_EventTile_senderDetails {
- padding-left: 32px;
- padding-top: 8px;
+ padding-left: 36px; // align with the room name
position: relative;
a {
@@ -82,7 +107,7 @@ limitations under the License.
.mx_NotificationPanel .mx_EventTile_line {
margin-right: 0px;
- padding-left: 32px;
+ padding-left: 36px; // align with the room name
padding-top: 0px;
padding-bottom: 0px;
padding-right: 0px;
diff --git a/res/css/structures/_RoomView.scss b/res/css/structures/_RoomView.scss
index 3b60c4e62b..572c7166d2 100644
--- a/res/css/structures/_RoomView.scss
+++ b/res/css/structures/_RoomView.scss
@@ -185,13 +185,11 @@ limitations under the License.
}
.mx_RoomView_empty {
- flex: 1 1 auto;
font-size: $font-13px;
- padding-left: 3em;
- padding-right: 3em;
- margin-right: 20px;
- margin-top: 33%;
+ padding: 0 24px;
+ margin-right: 30px;
text-align: center;
+ margin-bottom: 80px; // visually center the content (intentional offset)
}
.mx_RoomView_MessageList {
diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss
index 544dcbc180..c381668a6a 100644
--- a/res/css/structures/_ToastContainer.scss
+++ b/res/css/structures/_ToastContainer.scss
@@ -80,6 +80,11 @@ limitations under the License.
}
}
+ &.mx_Toast_icon_secure_backup::after {
+ mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
+ background-color: $primary-fg-color;
+ }
+
.mx_Toast_title, .mx_Toast_body {
grid-column: 2;
}
diff --git a/res/css/views/auth/_Welcome.scss b/res/css/views/auth/_Welcome.scss
index 9043289184..f0e2b3de33 100644
--- a/res/css/views/auth/_Welcome.scss
+++ b/res/css/views/auth/_Welcome.scss
@@ -18,6 +18,12 @@ limitations under the License.
display: flex;
flex-direction: column;
align-items: center;
+
+ &.mx_WelcomePage_registrationDisabled {
+ .mx_ButtonCreateAccount {
+ display: none;
+ }
+ }
}
.mx_Welcome .mx_AuthBody_language {
diff --git a/res/css/views/dialogs/_ShareDialog.scss b/res/css/views/dialogs/_ShareDialog.scss
index c343b872fd..ce3fdd021f 100644
--- a/res/css/views/dialogs/_ShareDialog.scss
+++ b/res/css/views/dialogs/_ShareDialog.scss
@@ -71,9 +71,12 @@ limitations under the License.
margin-right: 64px;
}
+.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container {
+ width: 299px;
+}
+
.mx_ShareDialog_social_container {
display: inline-block;
- width: 299px;
}
.mx_ShareDialog_social_icon {
diff --git a/res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss b/res/css/views/dialogs/security/_AccessSecretStorageDialog.scss
similarity index 100%
rename from res/css/views/dialogs/secretstorage/_AccessSecretStorageDialog.scss
rename to res/css/views/dialogs/security/_AccessSecretStorageDialog.scss
diff --git a/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss b/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss
new file mode 100644
index 0000000000..8303e02b9e
--- /dev/null
+++ b/res/css/views/dialogs/security/_CreateCrossSigningDialog.scss
@@ -0,0 +1,33 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+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_CreateCrossSigningDialog {
+ // Why you ask? Because CompleteSecurityBody is 600px so this is the width
+ // we end up when in there, but when in our own dialog we set our own width
+ // so need to fix it to something sensible as otherwise we'd end up either
+ // really wide or really narrow depending on the phase. I bet you wish you
+ // never asked.
+ width: 560px;
+
+ details .mx_AccessibleButton {
+ margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules
+ }
+}
+
+.mx_CreateCrossSigningDialog .mx_Dialog_title {
+ /* TODO: Consider setting this for all dialog titles. */
+ margin-bottom: 1em;
+}
diff --git a/res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss b/res/css/views/dialogs/security/_CreateKeyBackupDialog.scss
similarity index 100%
rename from res/css/views/dialogs/keybackup/_CreateKeyBackupDialog.scss
rename to res/css/views/dialogs/security/_CreateKeyBackupDialog.scss
diff --git a/res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss b/res/css/views/dialogs/security/_CreateSecretStorageDialog.scss
similarity index 100%
rename from res/css/views/dialogs/secretstorage/_CreateSecretStorageDialog.scss
rename to res/css/views/dialogs/security/_CreateSecretStorageDialog.scss
diff --git a/res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss b/res/css/views/dialogs/security/_KeyBackupFailedDialog.scss
similarity index 100%
rename from res/css/views/dialogs/keybackup/_KeyBackupFailedDialog.scss
rename to res/css/views/dialogs/security/_KeyBackupFailedDialog.scss
diff --git a/res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss b/res/css/views/dialogs/security/_RestoreKeyBackupDialog.scss
similarity index 100%
rename from res/css/views/dialogs/keybackup/_RestoreKeyBackupDialog.scss
rename to res/css/views/dialogs/security/_RestoreKeyBackupDialog.scss
diff --git a/res/css/views/rooms/_RoomRecoveryReminder.scss b/res/css/views/elements/_DesktopBuildsNotice.scss
similarity index 54%
rename from res/css/views/rooms/_RoomRecoveryReminder.scss
rename to res/css/views/elements/_DesktopBuildsNotice.scss
index 09b28ae235..3672595bf1 100644
--- a/res/css/views/rooms/_RoomRecoveryReminder.scss
+++ b/res/css/views/elements/_DesktopBuildsNotice.scss
@@ -1,5 +1,5 @@
/*
-Copyright 2018 New Vector Ltd
+Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -14,26 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_RoomRecoveryReminder {
- display: flex;
- flex-direction: column;
+.mx_DesktopBuildsNotice {
text-align: center;
- background-color: $room-warning-bg-color;
- padding: 20px;
- border: 1px solid $primary-hairline-color;
- border-bottom: unset;
-}
+ padding: 0 16px;
-.mx_RoomRecoveryReminder_header {
- font-weight: bold;
- margin-bottom: 1em;
-}
+ > * {
+ vertical-align: middle;
+ }
-.mx_RoomRecoveryReminder_body {
- margin-bottom: 1em;
-}
-
-.mx_RoomRecoveryReminder_secondary {
- font-size: 90%;
- margin-top: 1em;
+ > img {
+ margin-right: 8px;
+ }
}
diff --git a/res/css/views/rooms/_SearchBar.scss b/res/css/views/rooms/_SearchBar.scss
index fecc8d78d8..d9f730a8b6 100644
--- a/res/css/views/rooms/_SearchBar.scss
+++ b/res/css/views/rooms/_SearchBar.scss
@@ -68,3 +68,4 @@ limitations under the License.
cursor: pointer;
}
}
+
diff --git a/res/css/views/settings/_AvatarSetting.scss b/res/css/views/settings/_AvatarSetting.scss
index eddcf9f55a..52a0ee95d7 100644
--- a/res/css/views/settings/_AvatarSetting.scss
+++ b/res/css/views/settings/_AvatarSetting.scss
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -15,13 +15,55 @@ limitations under the License.
*/
.mx_AvatarSetting_avatar {
- width: $font-88px;
- height: $font-88px;
- margin-left: 13px;
+ width: 90px;
+ height: 90px;
+ margin-top: 8px;
position: relative;
+ .mx_AvatarSetting_hover {
+ transition: opacity $hover-transition;
+
+ // position to place the hover bg over the entire thing
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ pointer-events: none; // let the pointer fall through the underlying thing
+
+ line-height: 90px;
+ text-align: center;
+
+ > span {
+ color: #fff; // hardcoded to contrast with background
+ position: relative; // tricks the layout engine into putting this on top of the bg
+ font-weight: 500;
+ }
+
+ .mx_AvatarSetting_hoverBg {
+ // absolute position to lazily fill the entire container
+ position: absolute;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+
+ opacity: 0.5;
+ background-color: $settings-profile-overlay-placeholder-fg-color;
+ border-radius: 90px;
+ }
+ }
+
+ &.mx_AvatarSetting_avatar_hovering .mx_AvatarSetting_hover {
+ opacity: 1;
+ }
+
+ &:not(.mx_AvatarSetting_avatar_hovering) .mx_AvatarSetting_hover {
+ opacity: 0;
+ }
+
& > * {
- width: $font-88px;
box-sizing: border-box;
}
@@ -30,7 +72,7 @@ limitations under the License.
}
.mx_AccessibleButton.mx_AccessibleButton_kind_link_sm {
- color: $button-danger-bg-color;
+ width: 100%;
}
& > img {
@@ -41,8 +83,9 @@ limitations under the License.
& > img,
.mx_AvatarSetting_avatarPlaceholder {
display: block;
- height: $font-88px;
- border-radius: 4px;
+ height: 90px;
+ border-radius: 90px;
+ cursor: pointer;
}
.mx_AvatarSetting_avatarPlaceholder::before {
@@ -58,6 +101,29 @@ limitations under the License.
left: 0;
right: 0;
}
+
+ .mx_AvatarSetting_uploadButton {
+ width: 32px;
+ height: 32px;
+ border-radius: 32px;
+ background-color: $settings-profile-button-bg-color;
+
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ }
+
+ .mx_AvatarSetting_uploadButton::before {
+ content: "";
+ display: block;
+ width: 100%;
+ height: 100%;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: 55%;
+ background-color: $settings-profile-button-fg-color;
+ mask-image: url('$(res)/img/feather-customised/edit.svg');
+ }
}
.mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder {
diff --git a/res/css/views/settings/_CrossSigningPanel.scss b/res/css/views/settings/_CrossSigningPanel.scss
index fa9f76a963..12a0e36835 100644
--- a/res/css/views/settings/_CrossSigningPanel.scss
+++ b/res/css/views/settings/_CrossSigningPanel.scss
@@ -28,4 +28,8 @@ limitations under the License.
.mx_CrossSigningPanel_buttonRow {
margin: 1em 0;
+
+ :nth-child(n + 1) {
+ margin-inline-end: 10px;
+ }
}
diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss
index 58624d1597..732cbedf02 100644
--- a/res/css/views/settings/_ProfileSettings.scss
+++ b/res/css/views/settings/_ProfileSettings.scss
@@ -1,5 +1,5 @@
/*
-Copyright 2019 New Vector Ltd
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -20,6 +20,13 @@ limitations under the License.
.mx_ProfileSettings_controls {
flex-grow: 1;
+ margin-right: 54px;
+
+ // We put the header under the controls with some minor styling to cheat
+ // alignment of the field with the avatar
+ .mx_SettingsTab_subheading {
+ margin-top: 0;
+ }
}
.mx_ProfileSettings_controls .mx_Field #profileTopic {
@@ -41,3 +48,17 @@ limitations under the License.
.mx_ProfileSettings_avatarUpload {
display: none;
}
+
+.mx_ProfileSettings_profileForm {
+ @mixin mx_Settings_fullWidthField;
+ border-bottom: 1px solid $menu-border-color;
+}
+
+.mx_ProfileSettings_buttons {
+ margin-top: 10px; // 18px is already accounted for by the
above the buttons
+ margin-bottom: 28px;
+
+ > .mx_AccessibleButton_kind_link {
+ padding-left: 0; // to align with left side
+ }
+}
diff --git a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
index 6c9b89cf5a..8b73e69031 100644
--- a/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_GeneralUserSettingsTab.scss
@@ -22,6 +22,13 @@ limitations under the License.
margin-top: 0;
}
+// TODO: Make this selector less painful
+.mx_GeneralUserSettingsTab_accountSection .mx_SettingsTab_subheading:nth-child(n + 1),
+.mx_GeneralUserSettingsTab_discovery .mx_SettingsTab_subheading:nth-child(n + 2),
+.mx_SetIdServer .mx_SettingsTab_subheading {
+ margin-top: 24px;
+}
+
.mx_GeneralUserSettingsTab_accountSection .mx_Spinner,
.mx_GeneralUserSettingsTab_discovery .mx_Spinner {
// Move the spinner to the left side of the container (default center)
diff --git a/res/img/element-desktop-logo.svg b/res/img/element-desktop-logo.svg
new file mode 100644
index 0000000000..2031733ce3
--- /dev/null
+++ b/res/img/element-desktop-logo.svg
@@ -0,0 +1,157 @@
+
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index a3b03c777e..331b5f4692 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -87,11 +87,10 @@ $dialog-background-bg-color: $header-panel-bg-color;
$lightbox-background-bg-color: #000;
$settings-grey-fg-color: #a2a2a2;
-$settings-profile-placeholder-bg-color: #e7e7e7;
-$settings-profile-overlay-bg-color: #000;
-$settings-profile-overlay-placeholder-bg-color: transparent;
-$settings-profile-overlay-fg-color: #fff;
+$settings-profile-placeholder-bg-color: #21262c;
$settings-profile-overlay-placeholder-fg-color: #454545;
+$settings-profile-button-bg-color: #e7e7e7;
+$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
$settings-subsection-fg-color: $text-secondary-color;
$topleftmenu-color: $text-primary-color;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 2741dcebf8..14ce264bc0 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -86,10 +86,9 @@ $lightbox-background-bg-color: #000;
$settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7;
-$settings-profile-overlay-bg-color: #000;
-$settings-profile-overlay-placeholder-bg-color: transparent;
-$settings-profile-overlay-fg-color: #fff;
$settings-profile-overlay-placeholder-fg-color: #454545;
+$settings-profile-button-bg-color: #e7e7e7;
+$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
$settings-subsection-fg-color: $text-secondary-color;
$topleftmenu-color: $text-primary-color;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 4fd2a3615b..b030fb7423 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -144,10 +144,9 @@ $blockquote-fg-color: #777;
$settings-grey-fg-color: #a2a2a2;
$settings-profile-placeholder-bg-color: #e7e7e7;
-$settings-profile-overlay-bg-color: #000;
-$settings-profile-overlay-placeholder-bg-color: transparent;
-$settings-profile-overlay-fg-color: #fff;
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
+$settings-profile-button-bg-color: #e7e7e7;
+$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
$settings-subsection-fg-color: #61708b;
$voip-decline-color: #f48080;
diff --git a/res/themes/light-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss
index b830e86e02..6bb46e8a67 100644
--- a/res/themes/light-custom/css/_custom.scss
+++ b/res/themes/light-custom/css/_custom.scss
@@ -124,15 +124,15 @@ $pinned-unread-color: var(--warning-color);
$warning-color: var(--warning-color);
$button-danger-disabled-bg-color: var(--warning-color-50pct); // still needs alpha at 0.5
//
-// --username colors
-$username-variant1-color: var(--username-colors_1, $username-variant1-color);
-$username-variant2-color: var(--username-colors_2, $username-variant2-color);
-$username-variant3-color: var(--username-colors_3, $username-variant3-color);
-$username-variant4-color: var(--username-colors_4, $username-variant4-color);
-$username-variant5-color: var(--username-colors_5, $username-variant5-color);
-$username-variant6-color: var(--username-colors_6, $username-variant6-color);
-$username-variant7-color: var(--username-colors_7, $username-variant7-color);
-$username-variant8-color: var(--username-colors_8, $username-variant8-color);
+// --username colors (which use a 0-based index)
+$username-variant1-color: var(--username-colors_0, $username-variant1-color);
+$username-variant2-color: var(--username-colors_1, $username-variant2-color);
+$username-variant3-color: var(--username-colors_2, $username-variant3-color);
+$username-variant4-color: var(--username-colors_3, $username-variant4-color);
+$username-variant5-color: var(--username-colors_4, $username-variant5-color);
+$username-variant6-color: var(--username-colors_5, $username-variant6-color);
+$username-variant7-color: var(--username-colors_6, $username-variant7-color);
+$username-variant8-color: var(--username-colors_7, $username-variant8-color);
//
// --timeline-highlights-color
$event-selected-color: var(--timeline-highlights-color);
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 05302a2a80..140783212d 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -137,11 +137,10 @@ $blockquote-bar-color: #ddd;
$blockquote-fg-color: #777;
$settings-grey-fg-color: #a2a2a2;
-$settings-profile-placeholder-bg-color: #e7e7e7;
-$settings-profile-overlay-bg-color: #000;
-$settings-profile-overlay-placeholder-bg-color: transparent;
-$settings-profile-overlay-fg-color: #fff;
+$settings-profile-placeholder-bg-color: #f4f6fa;
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
+$settings-profile-button-bg-color: #e7e7e7;
+$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
$settings-subsection-fg-color: #61708b;
$voip-decline-color: #f48080;
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index e1111a8a94..91b91de90d 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -30,6 +30,7 @@ import {Notifier} from "../Notifier";
import type {Renderer} from "react-dom";
import RightPanelStore from "../stores/RightPanelStore";
import WidgetStore from "../stores/WidgetStore";
+import CallHandler from "../CallHandler";
declare global {
interface Window {
@@ -53,6 +54,7 @@ declare global {
mxNotifier: typeof Notifier;
mxRightPanelStore: RightPanelStore;
mxWidgetStore: WidgetStore;
+ mxCallHandler: CallHandler;
}
interface Document {
@@ -62,6 +64,9 @@ declare global {
interface Navigator {
userLanguage?: string;
+ // https://github.com/Microsoft/TypeScript/issues/19473
+ // https://developer.mozilla.org/en-US/docs/Web/API/MediaSession
+ mediaSession: any;
}
interface StorageEstimate {
diff --git a/src/@types/sanitize-html.ts b/src/@types/sanitize-html.ts
new file mode 100644
index 0000000000..4cada29845
--- /dev/null
+++ b/src/@types/sanitize-html.ts
@@ -0,0 +1,23 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+
+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 sanitizeHtml from 'sanitize-html';
+
+export interface IExtendedSanitizeOptions extends sanitizeHtml.IOptions {
+ // This option only exists in 2.x RCs so far, so not yet present in the
+ // separate type definition module.
+ nestingLimit?: number;
+}
diff --git a/src/CallHandler.js b/src/CallHandler.js
deleted file mode 100644
index ad40332af5..0000000000
--- a/src/CallHandler.js
+++ /dev/null
@@ -1,526 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017, 2018 New Vector Ltd
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
-
-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.
-*/
-
-/*
- * Manages a list of all the currently active calls.
- *
- * This handler dispatches when voip calls are added/updated/removed from this list:
- * {
- * action: 'call_state'
- * room_id:
- * }
- *
- * To know the state of the call, this handler exposes a getter to
- * obtain the call for a room:
- * var call = CallHandler.getCall(roomId)
- * var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
- *
- * This handler listens for and handles the following actions:
- * {
- * action: 'place_call',
- * type: 'voice|video',
- * room_id:
- * }
- *
- * {
- * action: 'incoming_call'
- * call: MatrixCall
- * }
- *
- * {
- * action: 'hangup'
- * room_id:
- * }
- *
- * {
- * action: 'answer'
- * room_id:
- * }
- */
-
-import {MatrixClientPeg} from './MatrixClientPeg';
-import PlatformPeg from './PlatformPeg';
-import Modal from './Modal';
-import { _t } from './languageHandler';
-import Matrix from 'matrix-js-sdk';
-import dis from './dispatcher/dispatcher';
-import WidgetUtils from './utils/WidgetUtils';
-import WidgetEchoStore from './stores/WidgetEchoStore';
-import SettingsStore from './settings/SettingsStore';
-import {generateHumanReadableId} from "./utils/NamingUtils";
-import {Jitsi} from "./widgets/Jitsi";
-import {WidgetType} from "./widgets/WidgetType";
-import {SettingLevel} from "./settings/SettingLevel";
-import {base32} from "rfc4648";
-
-import QuestionDialog from "./components/views/dialogs/QuestionDialog";
-import ErrorDialog from "./components/views/dialogs/ErrorDialog";
-
-global.mxCalls = {
- //room_id: MatrixCall
-};
-const calls = global.mxCalls;
-let ConferenceHandler = null;
-
-const audioPromises = {};
-
-function play(audioId) {
- // TODO: Attach an invisible element for this instead
- // which listens?
- const audio = document.getElementById(audioId);
- if (audio) {
- const playAudio = async () => {
- try {
- // This still causes the chrome debugger to break on promise rejection if
- // the promise is rejected, even though we're catching the exception.
- await audio.play();
- } catch (e) {
- // This is usually because the user hasn't interacted with the document,
- // or chrome doesn't think so and is denying the request. Not sure what
- // we can really do here...
- // https://github.com/vector-im/element-web/issues/7657
- console.log("Unable to play audio clip", e);
- }
- };
- if (audioPromises[audioId]) {
- audioPromises[audioId] = audioPromises[audioId].then(()=>{
- audio.load();
- return playAudio();
- });
- } else {
- audioPromises[audioId] = playAudio();
- }
- }
-}
-
-function pause(audioId) {
- // TODO: Attach an invisible element for this instead
- // which listens?
- const audio = document.getElementById(audioId);
- if (audio) {
- if (audioPromises[audioId]) {
- audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause());
- } else {
- // pause doesn't actually return a promise, but might as well do this for symmetry with play();
- audioPromises[audioId] = audio.pause();
- }
- }
-}
-
-function _setCallListeners(call) {
- call.on("error", function(err) {
- console.error("Call error:", err);
- if (
- MatrixClientPeg.get().getTurnServers().length === 0 &&
- SettingsStore.getValue("fallbackICEServerAllowed") === null
- ) {
- _showICEFallbackPrompt();
- return;
- }
-
- Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
- title: _t('Call Failed'),
- description: err.message,
- });
- });
- call.on("hangup", function() {
- _setCallState(undefined, call.roomId, "ended");
- });
- // map web rtc states to dummy UI state
- // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
- call.on("state", function(newState, oldState) {
- if (newState === "ringing") {
- _setCallState(call, call.roomId, "ringing");
- pause("ringbackAudio");
- } else if (newState === "invite_sent") {
- _setCallState(call, call.roomId, "ringback");
- play("ringbackAudio");
- } else if (newState === "ended" && oldState === "connected") {
- _setCallState(undefined, call.roomId, "ended");
- pause("ringbackAudio");
- play("callendAudio");
- } else if (newState === "ended" && oldState === "invite_sent" &&
- (call.hangupParty === "remote" ||
- (call.hangupParty === "local" && call.hangupReason === "invite_timeout")
- )) {
- _setCallState(call, call.roomId, "busy");
- pause("ringbackAudio");
- play("busyAudio");
- Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
- title: _t('Call Timeout'),
- description: _t('The remote side failed to pick up') + '.',
- });
- } else if (oldState === "invite_sent") {
- _setCallState(call, call.roomId, "stop_ringback");
- pause("ringbackAudio");
- } else if (oldState === "ringing") {
- _setCallState(call, call.roomId, "stop_ringing");
- pause("ringbackAudio");
- } else if (newState === "connected") {
- _setCallState(call, call.roomId, "connected");
- pause("ringbackAudio");
- }
- });
-}
-
-function _setCallState(call, roomId, status) {
- console.log(
- `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
- );
- calls[roomId] = call;
-
- if (status === "ringing") {
- play("ringAudio");
- } else if (call && call.call_state === "ringing") {
- pause("ringAudio");
- }
-
- if (call) {
- call.call_state = status;
- }
- dis.dispatch({
- action: 'call_state',
- room_id: roomId,
- state: status,
- });
-}
-
-function _showICEFallbackPrompt() {
- const cli = MatrixClientPeg.get();
- const code = sub => {sub};
- Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
- title: _t("Call failed due to misconfigured server"),
- description:
-
{_t(
- "Please ask the administrator of your homeserver " +
- "(%(homeserverDomain)s) to configure a TURN server in " +
- "order for calls to work reliably.",
- { homeserverDomain: cli.getDomain() }, { code },
- )}
-
{_t(
- "Alternatively, you can try to use the public server at " +
- "turn.matrix.org, but this will not be as reliable, and " +
- "it will share your IP address with that server. You can also manage " +
- "this in Settings.",
- null, { code },
- )}
-
,
- button: _t('Try using turn.matrix.org'),
- cancelButton: _t('OK'),
- onFinished: (allow) => {
- SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
- cli.setFallbackICEServerAllowed(allow);
- },
- }, null, true);
-}
-
-function _onAction(payload) {
- function placeCall(newCall) {
- _setCallListeners(newCall);
- if (payload.type === 'voice') {
- newCall.placeVoiceCall();
- } else if (payload.type === 'video') {
- newCall.placeVideoCall(
- payload.remote_element,
- payload.local_element,
- );
- } else if (payload.type === 'screensharing') {
- const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
- if (screenCapErrorString) {
- _setCallState(undefined, newCall.roomId, "ended");
- console.log("Can't capture screen: " + screenCapErrorString);
- Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
- title: _t('Unable to capture screen'),
- description: screenCapErrorString,
- });
- return;
- }
- newCall.placeScreenSharingCall(
- payload.remote_element,
- payload.local_element,
- );
- } else {
- console.error("Unknown conf call type: %s", payload.type);
- }
- }
-
- switch (payload.action) {
- case 'place_call':
- {
- if (callHandler.getAnyActiveCall()) {
- Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
- title: _t('Existing Call'),
- description: _t('You are already in a call.'),
- });
- return; // don't allow >1 call to be placed.
- }
-
- // if the runtime env doesn't do VoIP, whine.
- if (!MatrixClientPeg.get().supportsVoip()) {
- Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
- title: _t('VoIP is unsupported'),
- description: _t('You cannot place VoIP calls in this browser.'),
- });
- return;
- }
-
- const room = MatrixClientPeg.get().getRoom(payload.room_id);
- if (!room) {
- console.error("Room %s does not exist.", payload.room_id);
- return;
- }
-
- const members = room.getJoinedMembers();
- if (members.length <= 1) {
- Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
- description: _t('You cannot place a call with yourself.'),
- });
- return;
- } else if (members.length === 2) {
- console.info("Place %s call in %s", payload.type, payload.room_id);
- const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
- placeCall(call);
- } else { // > 2
- dis.dispatch({
- action: "place_conference_call",
- room_id: payload.room_id,
- type: payload.type,
- remote_element: payload.remote_element,
- local_element: payload.local_element,
- });
- }
- }
- break;
- case 'place_conference_call':
- console.info("Place conference call in %s", payload.room_id);
- _startCallApp(payload.room_id, payload.type);
- break;
- case 'incoming_call':
- {
- if (callHandler.getAnyActiveCall()) {
- // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
- // we avoid rejecting with "busy" in case the user wants to answer it on a different device.
- // in future we could signal a "local busy" as a warning to the caller.
- // see https://github.com/vector-im/vector-web/issues/1964
- return;
- }
-
- // if the runtime env doesn't do VoIP, stop here.
- if (!MatrixClientPeg.get().supportsVoip()) {
- return;
- }
-
- const call = payload.call;
- _setCallListeners(call);
- _setCallState(call, call.roomId, "ringing");
- }
- break;
- case 'hangup':
- if (!calls[payload.room_id]) {
- return; // no call to hangup
- }
- calls[payload.room_id].hangup();
- _setCallState(null, payload.room_id, "ended");
- break;
- case 'answer':
- if (!calls[payload.room_id]) {
- return; // no call to answer
- }
- calls[payload.room_id].answer();
- _setCallState(calls[payload.room_id], payload.room_id, "connected");
- dis.dispatch({
- action: "view_room",
- room_id: payload.room_id,
- });
- break;
- }
-}
-
-async function _startCallApp(roomId, type) {
- dis.dispatch({
- action: 'appsDrawer',
- show: true,
- });
-
- const room = MatrixClientPeg.get().getRoom(roomId);
- const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
-
- if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
- Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
- title: _t('Call in Progress'),
- description: _t('A call is currently being placed!'),
- });
- return;
- }
-
- if (currentJitsiWidgets.length > 0) {
- console.warn(
- "Refusing to start conference call widget in " + roomId +
- " a conference call widget is already present",
- );
-
- if (WidgetUtils.canUserModifyWidgets(roomId)) {
- Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
- title: _t('End Call'),
- description: _t('Remove the group call from the room?'),
- button: _t('End Call'),
- cancelButton: _t('Cancel'),
- onFinished: (endCall) => {
- if (endCall) {
- WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
- }
- },
- });
- } else {
- Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
- title: _t('Call in Progress'),
- description: _t("You don't have permission to remove the call from the room"),
- });
- }
- return;
- }
-
- const jitsiDomain = Jitsi.getInstance().preferredDomain;
- const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
- let confId;
- if (jitsiAuth === 'openidtoken-jwt') {
- // Create conference ID from room ID
- // For compatibility with Jitsi, use base32 without padding.
- // More details here:
- // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
- confId = base32.stringify(Buffer.from(roomId), { pad: false });
- } else {
- // Create a random human readable conference ID
- confId = `JitsiConference${generateHumanReadableId()}`;
- }
-
- let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
-
- // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
- const parsedUrl = new URL(widgetUrl);
- parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead
- parsedUrl.searchParams.set('confId', confId);
- widgetUrl = parsedUrl.toString();
-
- const widgetData = {
- conferenceId: confId,
- isAudioOnly: type === 'voice',
- domain: jitsiDomain,
- auth: jitsiAuth,
- };
-
- const widgetId = (
- 'jitsi_' +
- MatrixClientPeg.get().credentials.userId +
- '_' +
- Date.now()
- );
-
- WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
- console.log('Jitsi widget added');
- }).catch((e) => {
- if (e.errcode === 'M_FORBIDDEN') {
- Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
- title: _t('Permission Required'),
- description: _t("You do not have permission to start a conference call in this room"),
- });
- }
- console.error(e);
- });
-}
-
-// FIXME: Nasty way of making sure we only register
-// with the dispatcher once
-if (!global.mxCallHandler) {
- dis.register(_onAction);
- // add empty handlers for media actions, otherwise the media keys
- // end up causing the audio elements with our ring/ringback etc
- // audio clips in to play.
- if (navigator.mediaSession) {
- navigator.mediaSession.setActionHandler('play', function() {});
- navigator.mediaSession.setActionHandler('pause', function() {});
- navigator.mediaSession.setActionHandler('seekbackward', function() {});
- navigator.mediaSession.setActionHandler('seekforward', function() {});
- navigator.mediaSession.setActionHandler('previoustrack', function() {});
- navigator.mediaSession.setActionHandler('nexttrack', function() {});
- }
-}
-
-const callHandler = {
- getCallForRoom: function(roomId) {
- let call = callHandler.getCall(roomId);
- if (call) return call;
-
- if (ConferenceHandler) {
- call = ConferenceHandler.getConferenceCallForRoom(roomId);
- }
- if (call) return call;
-
- return null;
- },
-
- getCall: function(roomId) {
- return calls[roomId] || null;
- },
-
- getAnyActiveCall: function() {
- const roomsWithCalls = Object.keys(calls);
- for (let i = 0; i < roomsWithCalls.length; i++) {
- if (calls[roomsWithCalls[i]] &&
- calls[roomsWithCalls[i]].call_state !== "ended") {
- return calls[roomsWithCalls[i]];
- }
- }
- return null;
- },
-
- /**
- * The conference handler is a module that deals with implementation-specific
- * multi-party calling implementations. Element passes in its own which creates
- * a one-to-one call with a freeswitch conference bridge. As of July 2018,
- * the de-facto way of conference calling is a Jitsi widget, so this is
- * deprecated. It reamins here for two reasons:
- * 1. So Element still supports joining existing freeswitch conference calls
- * (but doesn't support creating them). After a transition period, we can
- * remove support for joining them too.
- * 2. To hide the one-to-one rooms that old-style conferencing creates. This
- * is much harder to remove: probably either we make Element leave & forget these
- * rooms after we remove support for joining freeswitch conferences, or we
- * accept that random rooms with cryptic users will suddently appear for
- * anyone who's ever used conference calling, or we are stuck with this
- * code forever.
- *
- * @param {object} confHandler The conference handler object
- */
- setConferenceHandler: function(confHandler) {
- ConferenceHandler = confHandler;
- },
-
- getConferenceHandler: function() {
- return ConferenceHandler;
- },
-};
-// Only things in here which actually need to be global are the
-// calls list (done separately) and making sure we only register
-// with the dispatcher once (which uses this mechanism but checks
-// separately). This could be tidied up.
-if (global.mxCallHandler === undefined) {
- global.mxCallHandler = callHandler;
-}
-
-export default global.mxCallHandler;
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
new file mode 100644
index 0000000000..62b91f938b
--- /dev/null
+++ b/src/CallHandler.tsx
@@ -0,0 +1,487 @@
+/*
+Copyright 2015, 2016 OpenMarket Ltd
+Copyright 2017, 2018 New Vector Ltd
+Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+
+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.
+*/
+
+/*
+ * Manages a list of all the currently active calls.
+ *
+ * This handler dispatches when voip calls are added/updated/removed from this list:
+ * {
+ * action: 'call_state'
+ * room_id:
+ * }
+ *
+ * To know the state of the call, this handler exposes a getter to
+ * obtain the call for a room:
+ * var call = CallHandler.getCall(roomId)
+ * var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
+ *
+ * This handler listens for and handles the following actions:
+ * {
+ * action: 'place_call',
+ * type: 'voice|video',
+ * room_id:
+ * }
+ *
+ * {
+ * action: 'incoming_call'
+ * call: MatrixCall
+ * }
+ *
+ * {
+ * action: 'hangup'
+ * room_id:
+ * }
+ *
+ * {
+ * action: 'answer'
+ * room_id:
+ * }
+ */
+
+import React from 'react';
+
+import {MatrixClientPeg} from './MatrixClientPeg';
+import PlatformPeg from './PlatformPeg';
+import Modal from './Modal';
+import { _t } from './languageHandler';
+// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
+import Matrix from 'matrix-js-sdk';
+import dis from './dispatcher/dispatcher';
+import WidgetUtils from './utils/WidgetUtils';
+import WidgetEchoStore from './stores/WidgetEchoStore';
+import SettingsStore from './settings/SettingsStore';
+import {generateHumanReadableId} from "./utils/NamingUtils";
+import {Jitsi} from "./widgets/Jitsi";
+import {WidgetType} from "./widgets/WidgetType";
+import {SettingLevel} from "./settings/SettingLevel";
+import { ActionPayload } from "./dispatcher/payloads";
+import {base32} from "rfc4648";
+
+import QuestionDialog from "./components/views/dialogs/QuestionDialog";
+import ErrorDialog from "./components/views/dialogs/ErrorDialog";
+
+// until we ts-ify the js-sdk voip code
+type Call = any;
+
+export default class CallHandler {
+ private calls = new Map();
+ private audioPromises = new Map>();
+
+ static sharedInstance() {
+ if (!window.mxCallHandler) {
+ window.mxCallHandler = new CallHandler()
+ }
+
+ return window.mxCallHandler;
+ }
+
+ constructor() {
+ dis.register(this.onAction);
+ // add empty handlers for media actions, otherwise the media keys
+ // end up causing the audio elements with our ring/ringback etc
+ // audio clips in to play.
+ if (navigator.mediaSession) {
+ navigator.mediaSession.setActionHandler('play', function() {});
+ navigator.mediaSession.setActionHandler('pause', function() {});
+ navigator.mediaSession.setActionHandler('seekbackward', function() {});
+ navigator.mediaSession.setActionHandler('seekforward', function() {});
+ navigator.mediaSession.setActionHandler('previoustrack', function() {});
+ navigator.mediaSession.setActionHandler('nexttrack', function() {});
+ }
+ }
+
+ getCallForRoom(roomId: string): Call {
+ return this.calls.get(roomId) || null;
+ }
+
+ getAnyActiveCall() {
+ const roomsWithCalls = Object.keys(this.calls);
+ for (let i = 0; i < roomsWithCalls.length; i++) {
+ if (this.calls.get(roomsWithCalls[i]) &&
+ this.calls.get(roomsWithCalls[i]).call_state !== "ended") {
+ return this.calls.get(roomsWithCalls[i]);
+ }
+ }
+ return null;
+ }
+
+ play(audioId: string) {
+ // TODO: Attach an invisible element for this instead
+ // which listens?
+ const audio = document.getElementById(audioId) as HTMLMediaElement;
+ if (audio) {
+ const playAudio = async () => {
+ try {
+ // This still causes the chrome debugger to break on promise rejection if
+ // the promise is rejected, even though we're catching the exception.
+ await audio.play();
+ } catch (e) {
+ // This is usually because the user hasn't interacted with the document,
+ // or chrome doesn't think so and is denying the request. Not sure what
+ // we can really do here...
+ // https://github.com/vector-im/element-web/issues/7657
+ console.log("Unable to play audio clip", e);
+ }
+ };
+ if (this.audioPromises.has(audioId)) {
+ this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => {
+ audio.load();
+ return playAudio();
+ }));
+ } else {
+ this.audioPromises.set(audioId, playAudio());
+ }
+ }
+ }
+
+ pause(audioId: string) {
+ // TODO: Attach an invisible element for this instead
+ // which listens?
+ const audio = document.getElementById(audioId) as HTMLMediaElement;
+ if (audio) {
+ if (this.audioPromises.has(audioId)) {
+ this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => audio.pause()));
+ } else {
+ // pause doesn't return a promise, so just do it
+ audio.pause();
+ }
+ }
+ }
+
+ private setCallListeners(call: Call) {
+ call.on("error", (err) => {
+ console.error("Call error:", err);
+ if (
+ MatrixClientPeg.get().getTurnServers().length === 0 &&
+ SettingsStore.getValue("fallbackICEServerAllowed") === null
+ ) {
+ this.showICEFallbackPrompt();
+ return;
+ }
+
+ Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
+ title: _t('Call Failed'),
+ description: err.message,
+ });
+ });
+ call.on("hangup", () => {
+ this.setCallState(undefined, call.roomId, "ended");
+ });
+ // map web rtc states to dummy UI state
+ // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
+ call.on("state", (newState, oldState) => {
+ if (newState === "ringing") {
+ this.setCallState(call, call.roomId, "ringing");
+ this.pause("ringbackAudio");
+ } else if (newState === "invite_sent") {
+ this.setCallState(call, call.roomId, "ringback");
+ this.play("ringbackAudio");
+ } else if (newState === "ended" && oldState === "connected") {
+ this.setCallState(undefined, call.roomId, "ended");
+ this.pause("ringbackAudio");
+ this.play("callendAudio");
+ } else if (newState === "ended" && oldState === "invite_sent" &&
+ (call.hangupParty === "remote" ||
+ (call.hangupParty === "local" && call.hangupReason === "invite_timeout")
+ )) {
+ this.setCallState(call, call.roomId, "busy");
+ this.pause("ringbackAudio");
+ this.play("busyAudio");
+ Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
+ title: _t('Call Timeout'),
+ description: _t('The remote side failed to pick up') + '.',
+ });
+ } else if (oldState === "invite_sent") {
+ this.setCallState(call, call.roomId, "stop_ringback");
+ this.pause("ringbackAudio");
+ } else if (oldState === "ringing") {
+ this.setCallState(call, call.roomId, "stop_ringing");
+ this.pause("ringbackAudio");
+ } else if (newState === "connected") {
+ this.setCallState(call, call.roomId, "connected");
+ this.pause("ringbackAudio");
+ }
+ });
+ }
+
+ private setCallState(call: Call, roomId: string, status: string) {
+ console.log(
+ `Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
+ );
+ this.calls.set(roomId, call);
+
+ if (status === "ringing") {
+ this.play("ringAudio");
+ } else if (call && call.call_state === "ringing") {
+ this.pause("ringAudio");
+ }
+
+ if (call) {
+ call.call_state = status;
+ }
+ dis.dispatch({
+ action: 'call_state',
+ room_id: roomId,
+ state: status,
+ });
+ }
+
+ private showICEFallbackPrompt() {
+ const cli = MatrixClientPeg.get();
+ const code = sub => {sub};
+ Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
+ title: _t("Call failed due to misconfigured server"),
+ description:
+
{_t(
+ "Please ask the administrator of your homeserver " +
+ "(%(homeserverDomain)s) to configure a TURN server in " +
+ "order for calls to work reliably.",
+ { homeserverDomain: cli.getDomain() }, { code },
+ )}
+
{_t(
+ "Alternatively, you can try to use the public server at " +
+ "turn.matrix.org, but this will not be as reliable, and " +
+ "it will share your IP address with that server. You can also manage " +
+ "this in Settings.",
+ null, { code },
+ )}
+
,
+ button: _t('Try using turn.matrix.org'),
+ cancelButton: _t('OK'),
+ onFinished: (allow) => {
+ SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
+ cli.setFallbackICEServerAllowed(allow);
+ },
+ }, null, true);
+ }
+
+ private onAction = (payload: ActionPayload) => {
+ const placeCall = (newCall) => {
+ this.setCallListeners(newCall);
+ if (payload.type === 'voice') {
+ newCall.placeVoiceCall();
+ } else if (payload.type === 'video') {
+ newCall.placeVideoCall(
+ payload.remote_element,
+ payload.local_element,
+ );
+ } else if (payload.type === 'screensharing') {
+ const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
+ if (screenCapErrorString) {
+ this.setCallState(undefined, newCall.roomId, "ended");
+ console.log("Can't capture screen: " + screenCapErrorString);
+ Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
+ title: _t('Unable to capture screen'),
+ description: screenCapErrorString,
+ });
+ return;
+ }
+ newCall.placeScreenSharingCall(
+ payload.remote_element,
+ payload.local_element,
+ );
+ } else {
+ console.error("Unknown conf call type: %s", payload.type);
+ }
+ }
+
+ switch (payload.action) {
+ case 'place_call':
+ {
+ if (this.getAnyActiveCall()) {
+ Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
+ title: _t('Existing Call'),
+ description: _t('You are already in a call.'),
+ });
+ return; // don't allow >1 call to be placed.
+ }
+
+ // if the runtime env doesn't do VoIP, whine.
+ if (!MatrixClientPeg.get().supportsVoip()) {
+ Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
+ title: _t('VoIP is unsupported'),
+ description: _t('You cannot place VoIP calls in this browser.'),
+ });
+ return;
+ }
+
+ const room = MatrixClientPeg.get().getRoom(payload.room_id);
+ if (!room) {
+ console.error("Room %s does not exist.", payload.room_id);
+ return;
+ }
+
+ const members = room.getJoinedMembers();
+ if (members.length <= 1) {
+ Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
+ description: _t('You cannot place a call with yourself.'),
+ });
+ return;
+ } else if (members.length === 2) {
+ console.info("Place %s call in %s", payload.type, payload.room_id);
+ const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
+ placeCall(call);
+ } else { // > 2
+ dis.dispatch({
+ action: "place_conference_call",
+ room_id: payload.room_id,
+ type: payload.type,
+ remote_element: payload.remote_element,
+ local_element: payload.local_element,
+ });
+ }
+ }
+ break;
+ case 'place_conference_call':
+ console.info("Place conference call in %s", payload.room_id);
+ this.startCallApp(payload.room_id, payload.type);
+ break;
+ case 'incoming_call':
+ {
+ if (this.getAnyActiveCall()) {
+ // ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
+ // we avoid rejecting with "busy" in case the user wants to answer it on a different device.
+ // in future we could signal a "local busy" as a warning to the caller.
+ // see https://github.com/vector-im/vector-web/issues/1964
+ return;
+ }
+
+ // if the runtime env doesn't do VoIP, stop here.
+ if (!MatrixClientPeg.get().supportsVoip()) {
+ return;
+ }
+
+ const call = payload.call;
+ this.setCallListeners(call);
+ this.setCallState(call, call.roomId, "ringing");
+ }
+ break;
+ case 'hangup':
+ if (!this.calls.get(payload.room_id)) {
+ return; // no call to hangup
+ }
+ this.calls.get(payload.room_id).hangup();
+ this.setCallState(null, payload.room_id, "ended");
+ break;
+ case 'answer':
+ if (!this.calls.get(payload.room_id)) {
+ return; // no call to answer
+ }
+ this.calls.get(payload.room_id).answer();
+ this.setCallState(this.calls.get(payload.room_id), payload.room_id, "connected");
+ dis.dispatch({
+ action: "view_room",
+ room_id: payload.room_id,
+ });
+ break;
+ }
+ }
+
+ private async startCallApp(roomId: string, type: string) {
+ dis.dispatch({
+ action: 'appsDrawer',
+ show: true,
+ });
+
+ const room = MatrixClientPeg.get().getRoom(roomId);
+ const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
+
+ if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
+ Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
+ title: _t('Call in Progress'),
+ description: _t('A call is currently being placed!'),
+ });
+ return;
+ }
+
+ if (currentJitsiWidgets.length > 0) {
+ console.warn(
+ "Refusing to start conference call widget in " + roomId +
+ " a conference call widget is already present",
+ );
+
+ if (WidgetUtils.canUserModifyWidgets(roomId)) {
+ Modal.createTrackedDialog('Already have Jitsi Widget', '', QuestionDialog, {
+ title: _t('End Call'),
+ description: _t('Remove the group call from the room?'),
+ button: _t('End Call'),
+ cancelButton: _t('Cancel'),
+ onFinished: (endCall) => {
+ if (endCall) {
+ WidgetUtils.setRoomWidget(roomId, currentJitsiWidgets[0].getContent()['id']);
+ }
+ },
+ });
+ } else {
+ Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
+ title: _t('Call in Progress'),
+ description: _t("You don't have permission to remove the call from the room"),
+ });
+ }
+ return;
+ }
+
+ const jitsiDomain = Jitsi.getInstance().preferredDomain;
+ const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
+ let confId;
+ if (jitsiAuth === 'openidtoken-jwt') {
+ // Create conference ID from room ID
+ // For compatibility with Jitsi, use base32 without padding.
+ // More details here:
+ // https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
+ confId = base32.stringify(Buffer.from(roomId), { pad: false });
+ } else {
+ // Create a random human readable conference ID
+ confId = `JitsiConference${generateHumanReadableId()}`;
+ }
+
+ let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
+
+ // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
+ const parsedUrl = new URL(widgetUrl);
+ parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead
+ parsedUrl.searchParams.set('confId', confId);
+ widgetUrl = parsedUrl.toString();
+
+ const widgetData = {
+ conferenceId: confId,
+ isAudioOnly: type === 'voice',
+ domain: jitsiDomain,
+ auth: jitsiAuth,
+ };
+
+ const widgetId = (
+ 'jitsi_' +
+ MatrixClientPeg.get().credentials.userId +
+ '_' +
+ Date.now()
+ );
+
+ WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
+ console.log('Jitsi widget added');
+ }).catch((e) => {
+ if (e.errcode === 'M_FORBIDDEN') {
+ Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
+ title: _t('Permission Required'),
+ description: _t("You do not have permission to start a conference call in this room"),
+ });
+ }
+ console.error(e);
+ });
+ }
+}
diff --git a/src/DeviceListener.ts b/src/DeviceListener.ts
index aa0508924d..df494e6bdd 100644
--- a/src/DeviceListener.ts
+++ b/src/DeviceListener.ts
@@ -29,11 +29,10 @@ import {
hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast,
} from "./toasts/UnverifiedSessionToast";
-import { privateShouldBeEncrypted } from "./createRoom";
import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityManager";
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import { isLoggedIn } from './components/structures/MatrixChat';
-
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
@@ -66,6 +65,7 @@ export default class DeviceListener {
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
MatrixClientPeg.get().on('accountData', this._onAccountData);
MatrixClientPeg.get().on('sync', this._onSync);
+ MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents);
this.dispatcherRef = dis.register(this._onAction);
this._recheck();
}
@@ -79,6 +79,7 @@ export default class DeviceListener {
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync);
+ MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents);
}
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
@@ -169,6 +170,16 @@ export default class DeviceListener {
if (state === 'PREPARED' && prevState === null) this._recheck();
};
+ _onRoomStateEvents = (ev: MatrixEvent) => {
+ if (ev.getType() !== "m.room.encryption") {
+ return;
+ }
+
+ // If a room changes to encrypted, re-check as it may be our first
+ // encrypted room. This also catches encrypted room creation as well.
+ this._recheck();
+ };
+
_onAction = ({ action }) => {
if (action !== "on_logged_in") return;
this._recheck();
@@ -189,9 +200,7 @@ export default class DeviceListener {
// If we're in the middle of a secret storage operation, we're likely
// modifying the state involved here, so don't add new toasts to setup.
if (isSecretStorageBeingAccessed()) return false;
- // In a default configuration, show the toasts. If the well-known config causes e2ee default to be false
- // then do not show the toasts until user is in at least one encrypted room.
- if (privateShouldBeEncrypted()) return true;
+ // Show setup toasts once the user is in at least one encrypted room.
const cli = MatrixClientPeg.get();
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
}
@@ -207,8 +216,6 @@ export default class DeviceListener {
// (we add a listener on sync to do once check after the initial sync is done)
if (!cli.isInitialSyncComplete()) return;
- // JRS: This will change again in the next PR which moves secret storage
- // later in the process.
const crossSigningReady = await cli.isCrossSigningReady();
const secretStorageReady = await cli.isSecretStorageReady();
const allSystemsReady = crossSigningReady && secretStorageReady;
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index bd314c2e5f..f991d2df5d 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -19,6 +19,7 @@ limitations under the License.
import React from 'react';
import sanitizeHtml from 'sanitize-html';
+import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import * as linkify from 'linkifyjs';
import linkifyMatrix from './linkify-matrix';
import _linkifyElement from 'linkifyjs/element';
@@ -151,7 +152,7 @@ export function isUrlPermitted(inputUrl: string) {
}
}
-const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to matrix
+const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to matrix
// add blank targets to all hyperlinks except vector URLs
'a': function(tagName: string, attribs: sanitizeHtml.Attributes) {
if (attribs.href) {
@@ -224,7 +225,7 @@ const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to mat
},
};
-const sanitizeHtmlParams: sanitizeHtml.IOptions = {
+const sanitizeHtmlParams: IExtendedSanitizeOptions = {
allowedTags: [
'font', // custom to matrix for IRC-style font coloring
'del', // for markdown
@@ -245,13 +246,14 @@ const sanitizeHtmlParams: sanitizeHtml.IOptions = {
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
// URL schemes we permit
allowedSchemes: PERMITTED_URL_SCHEMES,
-
allowProtocolRelative: false,
transformTags,
+ // 50 levels deep "should be enough for anyone"
+ nestingLimit: 50,
};
// this is the same as the above except with less rewriting
-const composerSanitizeHtmlParams: sanitizeHtml.IOptions = {
+const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
...sanitizeHtmlParams,
transformTags: {
'code': transformTags['code'],
diff --git a/src/Rooms.js b/src/Rooms.js
index 218e970f35..3da2b9bc14 100644
--- a/src/Rooms.js
+++ b/src/Rooms.js
@@ -26,58 +26,6 @@ export function getDisplayAliasForRoom(room) {
return room.getCanonicalAlias() || room.getAltAliases()[0];
}
-/**
- * If the room contains only two members including the logged-in user,
- * return the other one. Otherwise, return null.
- */
-export function getOnlyOtherMember(room, myUserId) {
- if (room.currentState.getJoinedMemberCount() === 2) {
- return room.getJoinedMembers().filter(function(m) {
- return m.userId !== myUserId;
- })[0];
- }
-
- return null;
-}
-
-function _isConfCallRoom(room, myUserId, conferenceHandler) {
- if (!conferenceHandler) return false;
-
- const myMembership = room.getMyMembership();
- if (myMembership != "join") {
- return false;
- }
-
- const otherMember = getOnlyOtherMember(room, myUserId);
- if (!otherMember) {
- return false;
- }
-
- if (conferenceHandler.isConferenceUser(otherMember.userId)) {
- return true;
- }
-
- return false;
-}
-
-// Cache whether a room is a conference call. Assumes that rooms will always
-// either will or will not be a conference call room.
-const isConfCallRoomCache = {
- // $roomId: bool
-};
-
-export function isConfCallRoom(room, myUserId, conferenceHandler) {
- if (isConfCallRoomCache[room.roomId] !== undefined) {
- return isConfCallRoomCache[room.roomId];
- }
-
- const result = _isConfCallRoom(room, myUserId, conferenceHandler);
-
- isConfCallRoomCache[room.roomId] = result;
-
- return result;
-}
-
export function looksLikeDirectMessageRoom(room, myUserId) {
const myMembership = room.getMyMembership();
const me = room.getMember(myUserId);
diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts
index b914aaaf6d..7d7caa2d24 100644
--- a/src/SdkConfig.ts
+++ b/src/SdkConfig.ts
@@ -33,6 +33,11 @@ export const DEFAULTS: ConfigOptions = {
// Default conference domain
preferredDomain: "jitsi.riot.im",
},
+ desktopBuilds: {
+ available: true,
+ logo: require("../res/img/element-desktop-logo.svg"),
+ url: "https://element.io/get-started",
+ },
};
export default class SdkConfig {
diff --git a/src/SecurityManager.js b/src/SecurityManager.js
index cc7db3ead7..f6b9c993d0 100644
--- a/src/SecurityManager.js
+++ b/src/SecurityManager.js
@@ -22,6 +22,8 @@ import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
import { _t } from './languageHandler';
import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
import { isSecureBackupRequired } from './utils/WellKnownUtils';
+import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
+import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
// This stores the secret storage private keys in memory for the JS SDK. This is
// only meant to act as a cache to avoid prompting the user multiple times
@@ -87,8 +89,6 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
return decodeRecoveryKey(recoveryKey);
}
};
- const AccessSecretStorageDialog =
- sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
AccessSecretStorageDialog,
/* props= */
@@ -181,7 +181,6 @@ export const crossSigningCallbacks = {
export async function promptForBackupPassphrase() {
let key;
- const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, {
showSummary: false, keyCallback: k => key = k,
}, null, /* priority = */ false, /* static = */ true);
@@ -221,7 +220,7 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
// This dialog calls bootstrap itself after guiding the user through
// passphrase creation.
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
- import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
+ import("./async-components/views/dialogs/security/CreateSecretStorageDialog"),
{
forceReset,
},
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index c55380bd9b..34d40bf1fd 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {MatrixClientPeg} from './MatrixClientPeg';
-import CallHandler from './CallHandler';
import { _t } from './languageHandler';
import * as Roles from './Roles';
import {isValid3pidInvite} from "./RoomInvite";
@@ -28,7 +27,6 @@ function textForMemberEvent(ev) {
const prevContent = ev.getPrevContent();
const content = ev.getContent();
- const ConferenceHandler = CallHandler.getConferenceHandler();
const reason = content.reason ? (_t('Reason') + ': ' + content.reason) : '';
switch (content.membership) {
case 'invite': {
@@ -43,11 +41,7 @@ function textForMemberEvent(ev) {
return _t('%(targetName)s accepted an invitation.', {targetName});
}
} else {
- if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
- return _t('%(senderName)s requested a VoIP conference.', {senderName});
- } else {
- return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
- }
+ return _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
}
}
case 'ban':
@@ -84,17 +78,11 @@ function textForMemberEvent(ev) {
}
} else {
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
- if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
- return _t('VoIP conference started.');
- } else {
- return _t('%(targetName)s joined the room.', {targetName});
- }
+ return _t('%(targetName)s joined the room.', {targetName});
}
case 'leave':
if (ev.getSender() === ev.getStateKey()) {
- if (ConferenceHandler && ConferenceHandler.isConferenceUser(ev.getStateKey())) {
- return _t('VoIP conference finished.');
- } else if (prevContent.membership === "invite") {
+ if (prevContent.membership === "invite") {
return _t('%(targetName)s rejected the invitation.', {targetName});
} else {
return _t('%(targetName)s left the room.', {targetName});
diff --git a/src/VectorConferenceHandler.js b/src/VectorConferenceHandler.js
deleted file mode 100644
index c10bc659ae..0000000000
--- a/src/VectorConferenceHandler.js
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
-
-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 {createNewMatrixCall as jsCreateNewMatrixCall, Room} from "matrix-js-sdk";
-import CallHandler from './CallHandler';
-import {MatrixClientPeg} from "./MatrixClientPeg";
-
-// FIXME: this is Element specific code, but will be removed shortly when we
-// switch over to Jitsi entirely for video conferencing.
-
-// FIXME: This currently forces Element to try to hit the matrix.org AS for
-// conferencing. This is bad because it prevents people running their own ASes
-// from being used. This isn't permanent and will be customisable in the future:
-// see the proposal at docs/conferencing.md for more info.
-const USER_PREFIX = "fs_";
-const DOMAIN = "matrix.org";
-
-export function ConferenceCall(matrixClient, groupChatRoomId) {
- this.client = matrixClient;
- this.groupRoomId = groupChatRoomId;
- this.confUserId = getConferenceUserIdForRoom(this.groupRoomId);
-}
-
-ConferenceCall.prototype.setup = function() {
- const self = this;
- return this._joinConferenceUser().then(function() {
- return self._getConferenceUserRoom();
- }).then(function(room) {
- // return a call for *this* room to be placed. We also tack on
- // confUserId to speed up lookups (else we'd need to loop every room
- // looking for a 1:1 room with this conf user ID!)
- const call = jsCreateNewMatrixCall(self.client, room.roomId);
- call.confUserId = self.confUserId;
- call.groupRoomId = self.groupRoomId;
- return call;
- });
-};
-
-ConferenceCall.prototype._joinConferenceUser = function() {
- // Make sure the conference user is in the group chat room
- const groupRoom = this.client.getRoom(this.groupRoomId);
- if (!groupRoom) {
- return Promise.reject("Bad group room ID");
- }
- const member = groupRoom.getMember(this.confUserId);
- if (member && member.membership === "join") {
- return Promise.resolve();
- }
- return this.client.invite(this.groupRoomId, this.confUserId);
-};
-
-ConferenceCall.prototype._getConferenceUserRoom = function() {
- // Use an existing 1:1 with the conference user; else make one
- const rooms = this.client.getRooms();
- let confRoom = null;
- for (let i = 0; i < rooms.length; i++) {
- const confUser = rooms[i].getMember(this.confUserId);
- if (confUser && confUser.membership === "join" &&
- rooms[i].getJoinedMemberCount() === 2) {
- confRoom = rooms[i];
- break;
- }
- }
- if (confRoom) {
- return Promise.resolve(confRoom);
- }
- return this.client.createRoom({
- preset: "private_chat",
- invite: [this.confUserId],
- }).then(function(res) {
- return new Room(res.room_id, null, MatrixClientPeg.get().getUserId());
- });
-};
-
-/**
- * Check if this user ID is in fact a conference bot.
- * @param {string} userId The user ID to check.
- * @return {boolean} True if it is a conference bot.
- */
-export function isConferenceUser(userId) {
- if (userId.indexOf("@" + USER_PREFIX) !== 0) {
- return false;
- }
- const base64part = userId.split(":")[0].substring(1 + USER_PREFIX.length);
- if (base64part) {
- const decoded = new Buffer(base64part, "base64").toString();
- // ! $STUFF : $STUFF
- return /^!.+:.+/.test(decoded);
- }
- return false;
-}
-
-export function getConferenceUserIdForRoom(roomId) {
- // abuse browserify's core node Buffer support (strip padding ='s)
- const base64RoomId = new Buffer(roomId).toString("base64").replace(/=/g, "");
- return "@" + USER_PREFIX + base64RoomId + ":" + DOMAIN;
-}
-
-export function createNewMatrixCall(client, roomId) {
- const confCall = new ConferenceCall(
- client, roomId,
- );
- return confCall.setup();
-}
-
-export function getConferenceCallForRoom(roomId) {
- // search for a conference 1:1 call for this group chat room ID
- const activeCall = CallHandler.getAnyActiveCall();
- if (activeCall && activeCall.confUserId) {
- const thisRoomConfUserId = getConferenceUserIdForRoom(
- roomId,
- );
- if (thisRoomConfUserId === activeCall.confUserId) {
- return activeCall;
- }
- }
- return null;
-}
-
-// TODO: Document this.
-export const slot = 'conference';
diff --git a/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js b/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js
deleted file mode 100644
index b79911c66e..0000000000
--- a/src/async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog.js
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import React from "react";
-import PropTypes from "prop-types";
-import * as sdk from "../../../../index";
-import { _t } from "../../../../languageHandler";
-
-export default class IgnoreRecoveryReminderDialog extends React.PureComponent {
- static propTypes = {
- onDontAskAgain: PropTypes.func.isRequired,
- onFinished: PropTypes.func.isRequired,
- onSetup: PropTypes.func.isRequired,
- }
-
- onDontAskAgainClick = () => {
- this.props.onFinished();
- this.props.onDontAskAgain();
- }
-
- onSetupClick = () => {
- this.props.onFinished();
- this.props.onSetup();
- }
-
- render() {
- const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
- const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
-
- return (
-
-
-
{_t(
- "Without setting up Secure Message Recovery, " +
- "you'll lose your secure message history when you " +
- "log out.",
- )}
-
{_t(
- "If you don't want to set this up now, you can later " +
- "in Settings.",
- )}
-
-
-
-
-
- );
- }
-}
diff --git a/src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
similarity index 100%
rename from src/async-components/views/dialogs/keybackup/CreateKeyBackupDialog.js
rename to src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
diff --git a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
similarity index 90%
rename from src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
rename to src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
index d4b1a73c3e..00aad2a0ce 100644
--- a/src/async-components/views/dialogs/secretstorage/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
@@ -30,7 +30,8 @@ import StyledRadioButton from '../../../../components/views/elements/StyledRadio
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
-import { isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
+import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
+import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
@@ -86,10 +87,16 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
accountPasswordCorrect: null,
- passPhraseKeySelected: CREATE_STORAGE_OPTION_KEY,
canSkip: !isSecureBackupRequired(),
};
+ const setupMethods = getSecureBackupSetupMethods();
+ if (setupMethods.includes("key")) {
+ this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_KEY;
+ } else {
+ this.state.passPhraseKeySelected = CREATE_STORAGE_OPTION_PASSPHRASE;
+ }
+
this._passphraseField = createRef();
this._fetchBackupInfo();
@@ -280,21 +287,21 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const { forceReset } = this.props;
try {
- // JRS: In an upcoming change, the cross-signing steps will be
- // removed from here and this will instead be about secret storage
- // only.
if (forceReset) {
- console.log("Forcing cross-signing and secret storage reset");
+ console.log("Forcing secret storage reset");
await cli.bootstrapSecretStorage({
createSecretStorageKey: async () => this._recoveryKey,
setupNewKeyBackup: true,
setupNewSecretStorage: true,
});
- await cli.bootstrapCrossSigning({
- authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
- setupNewCrossSigning: true,
- });
} else {
+ // For password authentication users after 2020-09, this cross-signing
+ // step will be a no-op since it is now setup during registration or login
+ // when needed. We should keep this here to cover other cases such as:
+ // * Users with existing sessions prior to 2020-09 changes
+ // * SSO authentication users which require interactive auth to upload
+ // keys (and also happen to skip all post-authentication flows at the
+ // moment via token login)
await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
});
@@ -341,7 +348,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
// so let's stash it here, rather than prompting for it twice.
const keyCallback = k => this._backupKey = k;
- const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
const { finished } = Modal.createTrackedDialog(
'Restore Backup', '', RestoreKeyBackupDialog,
{
@@ -441,39 +447,55 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
});
}
+ _renderOptionKey() {
+ return (
+
+
+
+ {_t("Generate a Security Key")}
+
+
{_t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}