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/README.md b/README.md
index e468d272d0..4db02418ba 100644
--- a/README.md
+++ b/README.md
@@ -160,8 +160,8 @@ yarn link matrix-js-sdk
yarn install
```
-See the [help for `yarn link`](https://yarnpkg.com/docs/cli/link) for more
-details about this.
+See the [help for `yarn link`](https://classic.yarnpkg.com/docs/cli/link) for
+more details about this.
Running tests
=============
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 156cbb1bc8..e66d0aabcf 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": {
@@ -79,6 +79,7 @@
"linkifyjs": "^2.1.9",
"lodash": "^4.17.19",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
+ "matrix-widget-api": "^0.1.0-beta.2",
"minimist": "^1.2.5",
"pako": "^1.0.11",
"parse5": "^5.1.1",
@@ -95,7 +96,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",
diff --git a/release.sh b/release.sh
index 23b8822041..e2cefcbe74 100755
--- a/release.sh
+++ b/release.sh
@@ -9,6 +9,9 @@ set -e
cd `dirname $0`
+# This link seems to get eaten by the release process, so ensure it exists.
+yarn link matrix-js-sdk
+
for i in matrix-js-sdk
do
echo "Checking version of $i..."
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 de1d9f861e..4d45b1076e 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -101,6 +101,7 @@
@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";
@@ -141,6 +142,7 @@
@import "./views/messages/_MFileBody.scss";
@import "./views/messages/_MImageBody.scss";
@import "./views/messages/_MImageReplyBody.scss";
+@import "./views/messages/_MJitsiWidgetEvent.scss";
@import "./views/messages/_MNoticeBody.scss";
@import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss";
diff --git a/res/css/views/elements/_DesktopBuildsNotice.scss b/res/css/views/elements/_DesktopBuildsNotice.scss
new file mode 100644
index 0000000000..3672595bf1
--- /dev/null
+++ b/res/css/views/elements/_DesktopBuildsNotice.scss
@@ -0,0 +1,28 @@
+/*
+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_DesktopBuildsNotice {
+ text-align: center;
+ padding: 0 16px;
+
+ > * {
+ vertical-align: middle;
+ }
+
+ > img {
+ margin-right: 8px;
+ }
+}
diff --git a/res/css/views/messages/_MJitsiWidgetEvent.scss b/res/css/views/messages/_MJitsiWidgetEvent.scss
new file mode 100644
index 0000000000..3e51e89744
--- /dev/null
+++ b/res/css/views/messages/_MJitsiWidgetEvent.scss
@@ -0,0 +1,55 @@
+/*
+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_MJitsiWidgetEvent {
+ display: grid;
+ grid-template-columns: 24px minmax(0, 1fr) min-content;
+
+ &::before {
+ grid-column: 1;
+ grid-row: 1 / 3;
+ width: 16px;
+ height: 16px;
+ content: "";
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ background-color: $composer-e2e-icon-color; // XXX: Variable abuse
+ margin-top: 4px;
+ mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+ }
+
+ .mx_MJitsiWidgetEvent_title {
+ font-weight: 600;
+ font-size: $font-15px;
+ grid-column: 2;
+ grid-row: 1;
+ }
+
+ .mx_MJitsiWidgetEvent_subtitle {
+ grid-column: 2;
+ grid-row: 2;
+ }
+
+ .mx_MJitsiWidgetEvent_title,
+ .mx_MJitsiWidgetEvent_subtitle {
+ overflow-wrap: break-word;
+ }
+}
diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss
index fee3d61153..244e88ca3e 100644
--- a/res/css/views/rooms/_AppsDrawer.scss
+++ b/res/css/views/rooms/_AppsDrawer.scss
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-$MiniAppTileHeight: 114px;
+$MiniAppTileHeight: 200px;
.mx_AppsDrawer {
margin: 5px 5px 5px 18px;
@@ -220,9 +220,10 @@ $MiniAppTileHeight: 114px;
}
.mx_AppTileBody_mini {
- height: 112px;
+ height: $MiniAppTileHeight;
width: 100%;
overflow: hidden;
+ border-radius: 8px;
}
.mx_AppTile .mx_AppTileBody,
diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index a403a8dc4c..71c0db947e 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -217,7 +217,7 @@ limitations under the License.
}
}
- &.mx_MessageComposer_hangup::before {
+ &.mx_MessageComposer_hangup:not(.mx_AccessibleButton_disabled)::before {
background-color: $warning-color;
}
}
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/_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/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss
index 4d26d8a312..650302b7e1 100644
--- a/res/css/views/voip/_CallContainer.scss
+++ b/res/css/views/voip/_CallContainer.scss
@@ -23,9 +23,16 @@ limitations under the License.
z-index: 100;
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
- cursor: pointer;
+ // Disable pointer events for Jitsi widgets to function. Direct
+ // calls have their own cursor and behaviour, but we need to make
+ // sure the cursor hits the iframe for Jitsi which will be at a
+ // different level.
+ pointer-events: none;
.mx_CallPreview {
+ pointer-events: initial; // restore pointer events so the user can leave/interact
+ cursor: pointer;
+
.mx_VideoView {
width: 350px;
}
@@ -37,7 +44,7 @@ limitations under the License.
}
.mx_AppTile_persistedWrapper div {
- min-width: 300px;
+ min-width: 350px;
}
.mx_IncomingCallBox {
@@ -45,6 +52,9 @@ limitations under the License.
background-color: $primary-bg-color;
padding: 8px;
+ pointer-events: initial; // restore pointer events so the user can accept/decline
+ cursor: pointer;
+
.mx_IncomingCallBox_CallerInfo {
display: flex;
direction: row;
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..5b368016b6
--- /dev/null
+++ b/src/CallHandler.tsx
@@ -0,0 +1,513 @@
+/*
+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";
+import WidgetStore from "./stores/WidgetStore";
+import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
+import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
+
+// 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() {
+ for (const call of this.calls.values()) {
+ if (call.state !== "ended") {
+ return call;
+ }
+ }
+ 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.removeCallForRoom(call.roomId);
+ });
+ // 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.removeCallForRoom(call.roomId);
+ 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 : "-"})`,
+ );
+ if (call) {
+ this.calls.set(roomId, call);
+ } else {
+ this.calls.delete(roomId);
+ }
+
+ 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 removeCallForRoom(roomId: string) {
+ this.setCallState(null, roomId, null);
+ }
+
+ 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.removeCallForRoom(newCall.roomId);
+ 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 'end_conference':
+ console.info("Terminating conference call in %s", payload.room_id);
+ this.terminateCallApp(payload.room_id);
+ break;
+ case 'hangup_conference':
+ console.info("Leaving conference call in %s", payload.room_id);
+ this.hangupCallApp(payload.room_id);
+ 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.removeCallForRoom(payload.room_id);
+ 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,
+ });
+
+ // prevent double clicking the call button
+ const room = MatrixClientPeg.get().getRoom(roomId);
+ const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
+ const hasJitsi = currentJitsiWidgets.length > 0
+ || WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
+ if (hasJitsi) {
+ Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
+ title: _t('Call in Progress'),
+ description: _t('A call is currently being placed!'),
+ });
+ 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);
+ });
+ }
+
+ private terminateCallApp(roomId: string) {
+ Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
+ hasCancelButton: true,
+ title: _t("End conference"),
+ description: _t("This will end the conference for everyone. Continue?"),
+ button: _t("End conference"),
+ onFinished: (proceed) => {
+ if (!proceed) return;
+
+ // We'll just obliterate them all. There should only ever be one, but might as well
+ // be safe.
+ const roomInfo = WidgetStore.instance.getRoom(roomId);
+ const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+ jitsiWidgets.forEach(w => {
+ // setting invalid content removes it
+ WidgetUtils.setRoomWidget(roomId, w.id);
+ });
+ },
+ });
+ }
+
+ private hangupCallApp(roomId: string) {
+ const roomInfo = WidgetStore.instance.getRoom(roomId);
+ if (!roomInfo) return; // "should never happen" clauses go here
+
+ const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
+ jitsiWidgets.forEach(w => {
+ const messaging = WidgetMessagingStore.instance.getMessagingForId(w.id);
+ if (!messaging) return; // more "should never happen" words
+
+ messaging.transport.send(ElementWidgetActions.HangupCall, {});
+ });
+ }
+}
diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js
deleted file mode 100644
index d5d7c08d50..0000000000
--- a/src/FromWidgetPostMessageApi.js
+++ /dev/null
@@ -1,275 +0,0 @@
-/*
-Copyright 2018 New Vector Ltd
-Copyright 2019 Travis Ralston
-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 URL from 'url';
-import dis from './dispatcher/dispatcher';
-import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
-import ActiveWidgetStore from './stores/ActiveWidgetStore';
-import {MatrixClientPeg} from "./MatrixClientPeg";
-import RoomViewStore from "./stores/RoomViewStore";
-import {IntegrationManagers} from "./integrations/IntegrationManagers";
-import SettingsStore from "./settings/SettingsStore";
-import {Capability} from "./widgets/WidgetApi";
-import {objectClone} from "./utils/objects";
-
-const WIDGET_API_VERSION = '0.0.2'; // Current API version
-const SUPPORTED_WIDGET_API_VERSIONS = [
- '0.0.1',
- '0.0.2',
-];
-const INBOUND_API_NAME = 'fromWidget';
-
-// Listen for and handle incoming requests using the 'fromWidget' postMessage
-// API and initiate responses
-export default class FromWidgetPostMessageApi {
- constructor() {
- this.widgetMessagingEndpoints = [];
- this.widgetListeners = {}; // {action: func[]}
-
- this.start = this.start.bind(this);
- this.stop = this.stop.bind(this);
- this.onPostMessage = this.onPostMessage.bind(this);
- }
-
- start() {
- window.addEventListener('message', this.onPostMessage);
- }
-
- stop() {
- window.removeEventListener('message', this.onPostMessage);
- }
-
- /**
- * Adds a listener for a given action
- * @param {string} action The action to listen for.
- * @param {Function} callbackFn A callback function to be called when the action is
- * encountered. Called with two parameters: the interesting request information and
- * the raw event received from the postMessage API. The raw event is meant to be used
- * for sendResponse and similar functions.
- */
- addListener(action, callbackFn) {
- if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
- this.widgetListeners[action].push(callbackFn);
- }
-
- /**
- * Removes a listener for a given action.
- * @param {string} action The action that was subscribed to.
- * @param {Function} callbackFn The original callback function that was used to subscribe
- * to updates.
- */
- removeListener(action, callbackFn) {
- if (!this.widgetListeners[action]) return;
-
- const idx = this.widgetListeners[action].indexOf(callbackFn);
- if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
- }
-
- /**
- * Register a widget endpoint for trusted postMessage communication
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
- */
- addEndpoint(widgetId, endpointUrl) {
- const u = URL.parse(endpointUrl);
- if (!u || !u.protocol || !u.host) {
- console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
- return;
- }
-
- const origin = u.protocol + '//' + u.host;
- const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
- if (this.widgetMessagingEndpoints.some(function(ep) {
- return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
- })) {
- // Message endpoint already registered
- console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
- return;
- } else {
- console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
- this.widgetMessagingEndpoints.push(endpoint);
- }
- }
-
- /**
- * De-register a widget endpoint from trusted communication sources
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
- * @return {boolean} True if endpoint was successfully removed
- */
- removeEndpoint(widgetId, endpointUrl) {
- const u = URL.parse(endpointUrl);
- if (!u || !u.protocol || !u.host) {
- console.warn('Remove widget messaging endpoint - Invalid origin');
- return;
- }
-
- const origin = u.protocol + '//' + u.host;
- if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
- const length = this.widgetMessagingEndpoints.length;
- this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
- .filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
- return (length > this.widgetMessagingEndpoints.length);
- }
- return false;
- }
-
- /**
- * Handle widget postMessage events
- * Messages are only handled where a valid, registered messaging endpoints
- * @param {Event} event Event to handle
- * @return {undefined}
- */
- onPostMessage(event) {
- if (!event.origin) { // Handle chrome
- event.origin = event.originalEvent.origin;
- }
-
- // Event origin is empty string if undefined
- if (
- event.origin.length === 0 ||
- !this.trustedEndpoint(event.origin) ||
- event.data.api !== INBOUND_API_NAME ||
- !event.data.widgetId
- ) {
- return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
- }
-
- // Call any listeners we have registered
- if (this.widgetListeners[event.data.action]) {
- for (const fn of this.widgetListeners[event.data.action]) {
- fn(event.data, event);
- }
- }
-
- // Although the requestId is required, we don't use it. We'll be nice and process the message
- // if the property is missing, but with a warning for widget developers.
- if (!event.data.requestId) {
- console.warn("fromWidget action '" + event.data.action + "' does not have a requestId");
- }
-
- const action = event.data.action;
- const widgetId = event.data.widgetId;
- if (action === 'content_loaded') {
- console.log('Widget reported content loaded for', widgetId);
- dis.dispatch({
- action: 'widget_content_loaded',
- widgetId: widgetId,
- });
- this.sendResponse(event, {success: true});
- } else if (action === 'supported_api_versions') {
- this.sendResponse(event, {
- api: INBOUND_API_NAME,
- supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
- });
- } else if (action === 'api_version') {
- this.sendResponse(event, {
- api: INBOUND_API_NAME,
- version: WIDGET_API_VERSION,
- });
- } else if (action === 'm.sticker') {
- // console.warn('Got sticker message from widget', widgetId);
- // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
- const data = event.data.data || event.data.widgetData;
- dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId});
- } else if (action === 'integration_manager_open') {
- // Close the stickerpicker
- dis.dispatch({action: 'stickerpicker_close'});
- // Open the integration manager
- // NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
- const data = event.data.data || event.data.widgetData;
- const integType = (data && data.integType) ? data.integType : null;
- const integId = (data && data.integId) ? data.integId : null;
-
- // TODO: Open the right integration manager for the widget
- if (SettingsStore.getValue("feature_many_integration_managers")) {
- IntegrationManagers.sharedInstance().openAll(
- MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
- `type_${integType}`,
- integId,
- );
- } else {
- IntegrationManagers.sharedInstance().getPrimaryManager().open(
- MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
- `type_${integType}`,
- integId,
- );
- }
- } else if (action === 'set_always_on_screen') {
- // This is a new message: there is no reason to support the deprecated widgetData here
- const data = event.data.data;
- const val = data.value;
-
- if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
- ActiveWidgetStore.setWidgetPersistence(widgetId, val);
- }
- } else if (action === 'get_openid') {
- // Handled by caller
- } else {
- console.warn('Widget postMessage event unhandled');
- this.sendError(event, {message: 'The postMessage was unhandled'});
- }
- }
-
- /**
- * Check if message origin is registered as trusted
- * @param {string} origin PostMessage origin to check
- * @return {boolean} True if trusted
- */
- trustedEndpoint(origin) {
- if (!origin) {
- return false;
- }
-
- return this.widgetMessagingEndpoints.some((endpoint) => {
- // TODO / FIXME -- Should this also check the widgetId?
- return endpoint.endpointUrl === origin;
- });
- }
-
- /**
- * Send a postmessage response to a postMessage request
- * @param {Event} event The original postMessage request event
- * @param {Object} res Response data
- */
- sendResponse(event, res) {
- const data = objectClone(event.data);
- data.response = res;
- event.source.postMessage(data, event.origin);
- }
-
- /**
- * Send an error response to a postMessage request
- * @param {Event} event The original postMessage request event
- * @param {string} msg Error message
- * @param {Error} nestedError Nested error event (optional)
- */
- sendError(event, msg, nestedError) {
- console.error('Action:' + event.data.action + ' failed with message: ' + msg);
- const data = objectClone(event.data);
- data.response = {
- error: {
- message: msg,
- },
- };
- if (nestedError) {
- data.response.error._error = nestedError;
- }
- event.source.postMessage(data, event.origin);
- }
-}
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/TextForEvent.js b/src/TextForEvent.js
index a76c1f59e6..34d40bf1fd 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -14,12 +14,10 @@ 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";
import SettingsStore from "./settings/SettingsStore";
-import {WidgetType} from "./widgets/WidgetType";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
function textForMemberEvent(ev) {
@@ -29,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': {
@@ -44,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':
@@ -85,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});
@@ -476,10 +463,6 @@ function textForWidgetEvent(event) {
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
const {name, type, url} = event.getContent() || {};
- if (WidgetType.JITSI.matches(type) || WidgetType.JITSI.matches(prevType)) {
- return textForJitsiWidgetEvent(event, senderName, url, prevUrl);
- }
-
let widgetName = name || prevName || type || prevType || '';
// Apply sentence case to widget name
if (widgetName && widgetName.length > 0) {
@@ -505,24 +488,6 @@ function textForWidgetEvent(event) {
}
}
-function textForJitsiWidgetEvent(event, senderName, url, prevUrl) {
- if (url) {
- if (prevUrl) {
- return _t('Group call modified by %(senderName)s', {
- senderName,
- });
- } else {
- return _t('Group call started by %(senderName)s', {
- senderName,
- });
- }
- } else {
- return _t('Group call ended by %(senderName)s', {
- senderName,
- });
- }
-}
-
function textForMjolnirEvent(event) {
const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent();
diff --git a/src/ToWidgetPostMessageApi.js b/src/ToWidgetPostMessageApi.js
deleted file mode 100644
index 00309d252c..0000000000
--- a/src/ToWidgetPostMessageApi.js
+++ /dev/null
@@ -1,84 +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.
-*/
-
-// const OUTBOUND_API_NAME = 'toWidget';
-
-// Initiate requests using the "toWidget" postMessage API and handle responses
-// NOTE: ToWidgetPostMessageApi only handles message events with a data payload with a
-// response field
-export default class ToWidgetPostMessageApi {
- constructor(timeoutMs) {
- this._timeoutMs = timeoutMs || 5000; // default to 5s timer
- this._counter = 0;
- this._requestMap = {
- // $ID: {resolve, reject}
- };
- this.start = this.start.bind(this);
- this.stop = this.stop.bind(this);
- this.onPostMessage = this.onPostMessage.bind(this);
- }
-
- start() {
- window.addEventListener('message', this.onPostMessage);
- }
-
- stop() {
- window.removeEventListener('message', this.onPostMessage);
- }
-
- onPostMessage(ev) {
- // THIS IS ALL UNSAFE EXECUTION.
- // We do not verify who the sender of `ev` is!
- const payload = ev.data;
- // NOTE: Workaround for running in a mobile WebView where a
- // postMessage immediately triggers this callback even though it is
- // not the response.
- if (payload.response === undefined) {
- return;
- }
- const promise = this._requestMap[payload.requestId];
- if (!promise) {
- return;
- }
- delete this._requestMap[payload.requestId];
- promise.resolve(payload);
- }
-
- // Initiate outbound requests (toWidget)
- exec(action, targetWindow, targetOrigin) {
- targetWindow = targetWindow || window.parent; // default to parent window
- targetOrigin = targetOrigin || "*";
- this._counter += 1;
- action.requestId = Date.now() + "-" + Math.random().toString(36) + "-" + this._counter;
-
- return new Promise((resolve, reject) => {
- this._requestMap[action.requestId] = {resolve, reject};
- targetWindow.postMessage(action, targetOrigin);
-
- if (this._timeoutMs > 0) {
- setTimeout(() => {
- if (!this._requestMap[action.requestId]) {
- return;
- }
- console.error("postMessage request timed out. Sent object: " + JSON.stringify(action),
- this._requestMap);
- this._requestMap[action.requestId].reject(new Error("Timed out"));
- delete this._requestMap[action.requestId];
- }, this._timeoutMs);
- }
- });
- }
-}
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/WidgetMessaging.js b/src/WidgetMessaging.js
deleted file mode 100644
index c68e926ac1..0000000000
--- a/src/WidgetMessaging.js
+++ /dev/null
@@ -1,212 +0,0 @@
-/*
-Copyright 2017 New Vector Ltd
-Copyright 2019 Travis Ralston
-
-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.
-*/
-
-/*
-* See - https://docs.google.com/document/d/1uPF7XWY_dXTKVKV7jZQ2KmsI19wn9-kFRgQ1tFQP7wQ/edit?usp=sharing for
-* spec. details / documentation.
-*/
-
-import FromWidgetPostMessageApi from './FromWidgetPostMessageApi';
-import ToWidgetPostMessageApi from './ToWidgetPostMessageApi';
-import Modal from "./Modal";
-import {MatrixClientPeg} from "./MatrixClientPeg";
-import SettingsStore from "./settings/SettingsStore";
-import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog";
-import WidgetUtils from "./utils/WidgetUtils";
-import {KnownWidgetActions} from "./widgets/WidgetApi";
-
-if (!global.mxFromWidgetMessaging) {
- global.mxFromWidgetMessaging = new FromWidgetPostMessageApi();
- global.mxFromWidgetMessaging.start();
-}
-if (!global.mxToWidgetMessaging) {
- global.mxToWidgetMessaging = new ToWidgetPostMessageApi();
- global.mxToWidgetMessaging.start();
-}
-
-const OUTBOUND_API_NAME = 'toWidget';
-
-export default class WidgetMessaging {
- /**
- * @param {string} widgetId The widget's ID
- * @param {string} wurl The raw URL of the widget as in the event (the 'wURL')
- * @param {string} renderedUrl The url used in the widget's iframe (either similar to the wURL
- * or a different URL of the clients choosing if it is using its own impl).
- * @param {bool} isUserWidget If true, the widget is a user widget, otherwise it's a room widget
- * @param {object} target Where widget messages should be sent (eg. the iframe object)
- */
- constructor(widgetId, wurl, renderedUrl, isUserWidget, target) {
- this.widgetId = widgetId;
- this.wurl = wurl;
- this.renderedUrl = renderedUrl;
- this.isUserWidget = isUserWidget;
- this.target = target;
- this.fromWidget = global.mxFromWidgetMessaging;
- this.toWidget = global.mxToWidgetMessaging;
- this._onOpenIdRequest = this._onOpenIdRequest.bind(this);
- this.start();
- }
-
- messageToWidget(action) {
- action.widgetId = this.widgetId; // Required to be sent for all outbound requests
-
- return this.toWidget.exec(action, this.target).then((data) => {
- // Check for errors and reject if found
- if (data.response === undefined) { // null is valid
- throw new Error("Missing 'response' field");
- }
- if (data.response && data.response.error) {
- const err = data.response.error;
- const msg = String(err.message ? err.message : "An error was returned");
- if (err._error) {
- console.error(err._error);
- }
- // Potential XSS attack if 'msg' is not appropriately sanitized,
- // as it is untrusted input by our parent window (which we assume is Element).
- // We can't aggressively sanitize [A-z0-9] since it might be a translation.
- throw new Error(msg);
- }
- // Return the response field for the request
- return data.response;
- });
- }
-
- /**
- * Tells the widget that the client is ready to handle further widget requests.
- * @returns {Promise<*>} Resolves after the widget has acknowledged the ready message.
- */
- flagReadyToContinue() {
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: KnownWidgetActions.ClientReady,
- });
- }
-
- /**
- * Tells the widget that it should terminate now.
- * @returns {Promise<*>} Resolves when widget has acknowledged the message.
- */
- terminate() {
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: KnownWidgetActions.Terminate,
- });
- }
-
- /**
- * Request a screenshot from a widget
- * @return {Promise} To be resolved with screenshot data when it has been generated
- */
- getScreenshot() {
- console.log('Requesting screenshot for', this.widgetId);
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: "screenshot",
- })
- .catch((error) => new Error("Failed to get screenshot: " + error.message))
- .then((response) => response.screenshot);
- }
-
- /**
- * Request capabilities required by the widget
- * @return {Promise} To be resolved with an array of requested widget capabilities
- */
- getCapabilities() {
- console.log('Requesting capabilities for', this.widgetId);
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: "capabilities",
- }).then((response) => {
- console.log('Got capabilities for', this.widgetId, response.capabilities);
- return response.capabilities;
- });
- }
-
- sendVisibility(visible) {
- return this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: "visibility",
- visible,
- })
- .catch((error) => {
- console.error("Failed to send visibility: ", error);
- });
- }
-
- start() {
- this.fromWidget.addEndpoint(this.widgetId, this.renderedUrl);
- this.fromWidget.addListener("get_openid", this._onOpenIdRequest);
- }
-
- stop() {
- this.fromWidget.removeEndpoint(this.widgetId, this.renderedUrl);
- this.fromWidget.removeListener("get_openid", this._onOpenIdRequest);
- }
-
- async _onOpenIdRequest(ev, rawEv) {
- if (ev.widgetId !== this.widgetId) return; // not interesting
-
- const widgetSecurityKey = WidgetUtils.getWidgetSecurityKey(this.widgetId, this.wurl, this.isUserWidget);
-
- const settings = SettingsStore.getValue("widgetOpenIDPermissions");
- if (settings.deny && settings.deny.includes(widgetSecurityKey)) {
- this.fromWidget.sendResponse(rawEv, {state: "blocked"});
- return;
- }
- if (settings.allow && settings.allow.includes(widgetSecurityKey)) {
- const responseBody = {state: "allowed"};
- const credentials = await MatrixClientPeg.get().getOpenIdToken();
- Object.assign(responseBody, credentials);
- this.fromWidget.sendResponse(rawEv, responseBody);
- return;
- }
-
- // Confirm that we received the request
- this.fromWidget.sendResponse(rawEv, {state: "request"});
-
- // Actually ask for permission to send the user's data
- Modal.createTrackedDialog("OpenID widget permissions", '',
- WidgetOpenIDPermissionsDialog, {
- widgetUrl: this.wurl,
- widgetId: this.widgetId,
- isUserWidget: this.isUserWidget,
-
- onFinished: async (confirm) => {
- const responseBody = {
- // Legacy (early draft) fields
- success: confirm,
-
- // New style MSC1960 fields
- state: confirm ? "allowed" : "blocked",
- original_request_id: ev.requestId, // eslint-disable-line camelcase
- };
- if (confirm) {
- const credentials = await MatrixClientPeg.get().getOpenIdToken();
- Object.assign(responseBody, credentials);
- }
- this.messageToWidget({
- api: OUTBOUND_API_NAME,
- action: "openid_credentials",
- data: responseBody,
- }).catch((error) => {
- console.error("Failed to send OpenID credentials: ", error);
- });
- },
- },
- );
- }
-}
diff --git a/src/WidgetMessagingEndpoint.js b/src/WidgetMessagingEndpoint.js
deleted file mode 100644
index 9114e12137..0000000000
--- a/src/WidgetMessagingEndpoint.js
+++ /dev/null
@@ -1,37 +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.
-*/
-
-
-/**
- * Represents mapping of widget instance to URLs for trusted postMessage communication.
- */
-export default class WidgetMessageEndpoint {
- /**
- * Mapping of widget instance to URL for trusted postMessage communication.
- * @param {string} widgetId Unique widget identifier
- * @param {string} endpointUrl Widget wurl origin.
- */
- constructor(widgetId, endpointUrl) {
- if (!widgetId) {
- throw new Error("No widgetId specified in widgetMessageEndpoint constructor");
- }
- if (!endpointUrl) {
- throw new Error("No endpoint specified in widgetMessageEndpoint constructor");
- }
- this.widgetId = widgetId;
- this.endpointUrl = endpointUrl;
- }
-}
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
index f3b52da141..00aad2a0ce 100644
--- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
@@ -31,7 +31,7 @@ import AccessibleButton from "../../../../components/views/elements/AccessibleBu
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
-import { isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
+import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
const PHASE_LOADING = 0;
const PHASE_LOADERROR = 1;
@@ -87,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();
@@ -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.")}