diff --git a/CHANGELOG.md b/CHANGELOG.md
index ef31ef886f..8c460b4f81 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,63 @@
+Changes in [3.12.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0) (2021-01-18)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0-rc.1...v3.12.0)
+
+ * Upgrade to JS SDK 9.5.0
+ * Fix incoming call box on dark theme
+ [\#5543](https://github.com/matrix-org/matrix-react-sdk/pull/5543)
+
+Changes in [3.12.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0-rc.1) (2021-01-13)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.1...v3.12.0-rc.1)
+
+ * Upgrade to JS SDK 9.5.0-rc.1
+ * Fix soft crash on soft logout page
+ [\#5539](https://github.com/matrix-org/matrix-react-sdk/pull/5539)
+ * Translations update from Weblate
+ [\#5538](https://github.com/matrix-org/matrix-react-sdk/pull/5538)
+ * Run TypeScript tests
+ [\#5537](https://github.com/matrix-org/matrix-react-sdk/pull/5537)
+ * Add a basic widget explorer to devtools (per-room)
+ [\#5528](https://github.com/matrix-org/matrix-react-sdk/pull/5528)
+ * Add to security key field
+ [\#5534](https://github.com/matrix-org/matrix-react-sdk/pull/5534)
+ * Fix avatar upload prompt/tooltip floating wrong and permissions
+ [\#5526](https://github.com/matrix-org/matrix-react-sdk/pull/5526)
+ * Add a dialpad UI for PSTN lookup
+ [\#5523](https://github.com/matrix-org/matrix-react-sdk/pull/5523)
+ * Basic call transfer initiation support
+ [\#5494](https://github.com/matrix-org/matrix-react-sdk/pull/5494)
+ * Fix #15988
+ [\#5524](https://github.com/matrix-org/matrix-react-sdk/pull/5524)
+ * Bump node-notifier from 8.0.0 to 8.0.1
+ [\#5520](https://github.com/matrix-org/matrix-react-sdk/pull/5520)
+ * Use TypeScript source for development, swap to build during release
+ [\#5503](https://github.com/matrix-org/matrix-react-sdk/pull/5503)
+ * Look for emoji in the body that will be displayed
+ [\#5517](https://github.com/matrix-org/matrix-react-sdk/pull/5517)
+ * Bump ini from 1.3.5 to 1.3.7
+ [\#5486](https://github.com/matrix-org/matrix-react-sdk/pull/5486)
+ * Recognise `*.element.io` links as Element permalinks
+ [\#5514](https://github.com/matrix-org/matrix-react-sdk/pull/5514)
+ * Fixes for call UI
+ [\#5509](https://github.com/matrix-org/matrix-react-sdk/pull/5509)
+ * Add a snowfall chat effect (with /snowfall command)
+ [\#5511](https://github.com/matrix-org/matrix-react-sdk/pull/5511)
+ * fireworks effect
+ [\#5507](https://github.com/matrix-org/matrix-react-sdk/pull/5507)
+ * Don't play call end sound for calls that never started
+ [\#5506](https://github.com/matrix-org/matrix-react-sdk/pull/5506)
+ * Add /tableflip slash command
+ [\#5485](https://github.com/matrix-org/matrix-react-sdk/pull/5485)
+ * Import from src in IncomingCallBox.tsx
+ [\#5504](https://github.com/matrix-org/matrix-react-sdk/pull/5504)
+ * Social Login support both https and mxc icons
+ [\#5499](https://github.com/matrix-org/matrix-react-sdk/pull/5499)
+ * Fix padding in confirmation email registration prompt
+ [\#5501](https://github.com/matrix-org/matrix-react-sdk/pull/5501)
+ * Fix room list help prompt alignment
+ [\#5500](https://github.com/matrix-org/matrix-react-sdk/pull/5500)
+
Changes in [3.11.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.1) (2020-12-21)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0...v3.11.1)
diff --git a/docs/widget-layouts.md b/docs/widget-layouts.md
new file mode 100644
index 0000000000..e7f72e2001
--- /dev/null
+++ b/docs/widget-layouts.md
@@ -0,0 +1,60 @@
+# Widget layout support
+
+Rooms can have a default widget layout to auto-pin certain widgets, make the container different
+sizes, etc. These are defined through the `io.element.widgets.layout` state event (empty state key).
+
+Full example content:
+```json5
+{
+ "widgets": {
+ "first-widget-id": {
+ "container": "top",
+ "index": 0,
+ "width": 60,
+ "height": 40
+ },
+ "second-widget-id": {
+ "container": "right"
+ }
+ }
+}
+```
+
+As shown, there are two containers possible for widgets. These containers have different behaviour
+and interpret the other options differently.
+
+## `top` container
+
+This is the "App Drawer" or any pinned widgets in a room. This is by far the most versatile container
+though does introduce potential usability issues upon members of the room (widgets take up space and
+therefore fewer messages can be shown).
+
+The `index` for a widget determines which order the widgets show up in from left to right. Widgets
+without an `index` will show up as the rightmost widgets. Tiebreaks (same `index` or multiple defined
+without an `index`) are resolved by comparing widget IDs. A maximum of 3 widgets can be in the top
+container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers
+represent leftmost widgets.
+
+The `width` is relative width within the container in percentage points. This will be clamped to a
+range of 0-100 (inclusive). The widgets will attempt to scale to relative proportions when more than
+100% space is allocated. For example, if 3 widgets are defined at 40% width each then the client will
+attempt to show them at 33% width each.
+
+Note that the client may impose minimum widths on the widgets, such as a 10% minimum to avoid pinning
+hidden widgets. In general, widgets defined in the 30-70% range each will be free of these restrictions.
+
+The `height` is not in fact applied per-widget but is recorded per-widget for potential future
+capabilities in future containers. The top container will take the tallest `height` and use that for
+the height of the whole container, and thus all widgets in that container. The `height` is relative
+to the container, like with `width`, meaning that 100% will consume as much space as the client is
+willing to sacrifice to the widget container. Like with `width`, the client may impose minimums to avoid
+the container being uselessly small. Heights in the 30-100% range are generally acceptable. The height
+is also clamped to be within 0-100, inclusive.
+
+## `right` container
+
+This is the default container and has no special configuration. Widgets which overflow from the top
+container will be put in this container instead. Putting a widget in the right container does not
+automatically show it - it only mentions that widgets should not be in another container.
+
+The behaviour of this container may change in the future.
diff --git a/package.json b/package.json
index 7086ec2348..1316b26030 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.11.1",
+ "version": "3.12.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -27,11 +27,12 @@
"matrix-gen-i18n": "scripts/gen-i18n.js",
"matrix-prune-i18n": "scripts/prune-i18n.js"
},
- "main": "./lib/index.js",
- "typings": "./lib/index.d.ts",
+ "main": "./src/index.js",
"matrix_src_main": "./src/index.js",
+ "matrix_lib_main": "./lib/index.js",
+ "matrix_lib_typings": "./lib/index.d.ts",
"scripts": {
- "prepare": "yarn build",
+ "prepublishOnly": "yarn build",
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
@@ -81,7 +82,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.10",
+ "matrix-widget-api": "0.1.0-beta.11",
"minimist": "^1.2.5",
"pako": "^1.0.11",
"parse5": "^5.1.1",
@@ -127,6 +128,7 @@
"@types/classnames": "^2.2.10",
"@types/counterpart": "^0.18.1",
"@types/flux": "^3.1.9",
+ "@types/jest": "^26.0.20",
"@types/linkifyjs": "^2.1.3",
"@types/lodash": "^4.14.158",
"@types/modernizr": "^3.5.3",
@@ -171,7 +173,7 @@
"jest": {
"testEnvironment": "./__test-utils__/environment.js",
"testMatch": [
- "/test/**/*-test.js"
+ "/test/**/*-test.[jt]s"
],
"setupFiles": [
"jest-canvas-mock"
diff --git a/res/css/_components.scss b/res/css/_components.scss
index d8bc238db5..a6e1f81583 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -236,4 +236,7 @@
@import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallContainer.scss";
@import "./views/voip/_CallView.scss";
+@import "./views/voip/_DialPad.scss";
+@import "./views/voip/_DialPadContextMenu.scss";
+@import "./views/voip/_DialPadModal.scss";
@import "./views/voip/_VideoFeed.scss";
diff --git a/res/css/structures/_LeftPanel.scss b/res/css/structures/_LeftPanel.scss
index 1424d9cda0..168590502d 100644
--- a/res/css/structures/_LeftPanel.scss
+++ b/res/css/structures/_LeftPanel.scss
@@ -180,6 +180,11 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
.mx_LeftPanel_roomListContainer {
width: 68px;
+ .mx_LeftPanel_userHeader {
+ flex-direction: row;
+ justify-content: center;
+ }
+
.mx_LeftPanel_filterContainer {
// Organize the flexbox into a centered column layout
flex-direction: column;
diff --git a/res/css/structures/_UserMenu.scss b/res/css/structures/_UserMenu.scss
index 84c21364ce..0673ebb058 100644
--- a/res/css/structures/_UserMenu.scss
+++ b/res/css/structures/_UserMenu.scss
@@ -119,14 +119,10 @@ limitations under the License.
}
&.mx_UserMenu_minimized {
- .mx_UserMenu_userHeader {
- .mx_UserMenu_row {
- justify-content: center;
- }
+ padding-right: 0px;
- .mx_UserMenu_userAvatarContainer {
- margin-right: 0;
- }
+ .mx_UserMenu_userAvatarContainer {
+ margin-right: 0px;
}
}
}
diff --git a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss
index a1793cc75e..c97a3b69b7 100644
--- a/res/css/views/dialogs/_RoomSettingsDialogBridges.scss
+++ b/res/css/views/dialogs/_RoomSettingsDialogBridges.scss
@@ -89,24 +89,18 @@ limitations under the License.
}
}
- .mx_showMore {
- display: block;
- text-align: left;
- margin-top: 10px;
- }
-
.metadata {
color: $muted-fg-color;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
margin-bottom: 0;
- }
-
- .metadata.visible {
overflow-y: visible;
text-overflow: ellipsis;
white-space: normal;
+ padding: 0;
+
+ > li {
+ padding: 0;
+ border: 0;
+ }
}
}
}
diff --git a/res/css/views/elements/_MiniAvatarUploader.scss b/res/css/views/elements/_MiniAvatarUploader.scss
index 2502977331..698184a095 100644
--- a/res/css/views/elements/_MiniAvatarUploader.scss
+++ b/res/css/views/elements/_MiniAvatarUploader.scss
@@ -18,6 +18,16 @@ limitations under the License.
position: relative;
width: min-content;
+ // this isn't a floating tooltip so override some things to not need to bother with z-index and floating
+ .mx_Tooltip {
+ display: inline-block;
+ position: absolute;
+ z-index: unset;
+ width: max-content;
+ left: 72px;
+ top: 0;
+ }
+
&::before, &::after {
content: '';
position: absolute;
diff --git a/res/css/views/elements/_SSOButtons.scss b/res/css/views/elements/_SSOButtons.scss
index f762468c7f..c61247655c 100644
--- a/res/css/views/elements/_SSOButtons.scss
+++ b/res/css/views/elements/_SSOButtons.scss
@@ -16,8 +16,15 @@ limitations under the License.
.mx_SSOButtons {
display: flex;
+ flex-wrap: wrap;
justify-content: center;
+ .mx_SSOButtons_row {
+ & + .mx_SSOButtons_row {
+ margin-top: 16px;
+ }
+ }
+
.mx_SSOButton {
position: relative;
width: 100%;
@@ -36,6 +43,8 @@ limitations under the License.
box-sizing: border-box;
width: 50px; // 48px + 1px border on all sides
height: 50px; // 48px + 1px border on all sides
+ min-width: 50px; // prevent crushing by the flexbox
+ padding: 12px;
> img {
left: 12px;
@@ -43,7 +52,7 @@ limitations under the License.
}
& + .mx_SSOButton_mini {
- margin-left: 24px;
+ margin-left: 16px;
}
}
}
diff --git a/res/css/views/rooms/_AppsDrawer.scss b/res/css/views/rooms/_AppsDrawer.scss
index 8731d22660..492ed95973 100644
--- a/res/css/views/rooms/_AppsDrawer.scss
+++ b/res/css/views/rooms/_AppsDrawer.scss
@@ -24,26 +24,45 @@ $MiniAppTileHeight: 200px;
flex-direction: column;
overflow: hidden;
+ .mx_AppsContainer_resizerHandleContainer {
+ width: 100%;
+ height: 10px;
+ margin-top: -3px; // move it up so the interactions are slightly more comfortable
+ display: block;
+ position: relative;
+ }
+
.mx_AppsContainer_resizerHandle {
cursor: ns-resize;
- border-radius: 3px;
- // Override styles from library
- width: unset !important;
- height: 4px !important;
+ // Override styles from library, making the whole area the target area
+ width: 100% !important;
+ height: 100% !important;
// This is positioned directly below frame
position: absolute;
- bottom: -8px !important; // override from library
+ bottom: 0 !important; // override from library
- // Together, these make the bar 64px wide
- // These are also overridden from the library
- left: calc(50% - 32px) !important;
- right: calc(50% - 32px) !important;
+ // We then render the pill handle in an ::after to keep it in the handle's
+ // area without being a massive line across the screen
+ &::after {
+ content: '';
+ position: absolute;
+ border-radius: 3px;
+
+ // The combination of these two should make the pill 4px high
+ top: 6px;
+ bottom: 0;
+
+ // Together, these make the bar 64px wide
+ // These are also overridden from the library
+ left: calc(50% - 32px);
+ right: calc(50% - 32px);
+ }
}
&:hover {
- .mx_AppsContainer_resizerHandle {
+ .mx_AppsContainer_resizerHandle::after {
opacity: 0.8;
background: $primary-fg-color;
}
diff --git a/res/css/views/rooms/_RoomList.scss b/res/css/views/rooms/_RoomList.scss
index b7759d265f..66e1b827d0 100644
--- a/res/css/views/rooms/_RoomList.scss
+++ b/res/css/views/rooms/_RoomList.scss
@@ -24,6 +24,9 @@ limitations under the License.
.mx_RoomList_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
}
+.mx_RoomList_iconDialpad::before {
+ mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg');
+}
.mx_RoomList_explorePrompt {
margin: 4px 12px 4px;
diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss
index 732cbedf02..4cbcb8e708 100644
--- a/res/css/views/settings/_ProfileSettings.scss
+++ b/res/css/views/settings/_ProfileSettings.scss
@@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+.mx_ProfileSettings_controls_topic {
+ & > textarea {
+ resize: vertical;
+ }
+}
+
.mx_ProfileSettings_profile {
display: flex;
}
diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss
index ae1d37de71..8262075559 100644
--- a/res/css/views/voip/_CallContainer.scss
+++ b/res/css/views/voip/_CallContainer.scss
@@ -46,7 +46,7 @@ limitations under the License.
.mx_IncomingCallBox {
min-width: 250px;
- background-color: $secondary-accent-color;
+ background-color: $voipcall-plinth-color;
padding: 8px;
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
border-radius: 8px;
diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index a9b02ff5d8..7eb329594a 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -310,8 +310,14 @@ limitations under the License.
}
}
-// Makes the alignment correct
-.mx_CallView_callControls_nothing {
+.mx_CallView_callControls_dialpad {
+ margin-right: auto;
+ &::before {
+ background-image: url('$(res)/img/voip/dialpad.svg');
+ }
+}
+
+.mx_CallView_callControls_button_dialpad_hidden {
margin-right: auto;
cursor: initial;
}
diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss
new file mode 100644
index 0000000000..0c7bff0ce8
--- /dev/null
+++ b/res/css/views/voip/_DialPad.scss
@@ -0,0 +1,62 @@
+/*
+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_DialPad {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+}
+
+.mx_DialPad_button {
+ width: 40px;
+ height: 40px;
+ background-color: $theme-button-bg-color;
+ border-radius: 40px;
+ font-size: 18px;
+ font-weight: 600;
+ text-align: center;
+ vertical-align: middle;
+ line-height: 40px;
+}
+
+.mx_DialPad_deleteButton, .mx_DialPad_dialButton {
+ &::before {
+ content: '';
+ display: inline-block;
+ height: 40px;
+ width: 40px;
+ vertical-align: middle;
+ mask-repeat: no-repeat;
+ mask-size: 20px;
+ mask-position: center;
+ background-color: $primary-bg-color;
+ }
+}
+
+.mx_DialPad_deleteButton {
+ background-color: $notice-primary-color;
+ &::before {
+ mask-image: url('$(res)/img/element-icons/call/delete.svg');
+ mask-position: 9px; // delete icon is right-heavy so have to be slightly to the left to look centered
+ }
+}
+
+.mx_DialPad_dialButton {
+ background-color: $accent-color;
+ &::before {
+ mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+ }
+}
diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss
new file mode 100644
index 0000000000..520f51cf93
--- /dev/null
+++ b/res/css/views/voip/_DialPadContextMenu.scss
@@ -0,0 +1,47 @@
+/*
+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_DialPadContextMenu_header {
+ margin-top: 12px;
+ margin-left: 12px;
+ margin-right: 12px;
+}
+
+.mx_DialPadContextMenu_title {
+ color: $muted-fg-color;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.mx_DialPadContextMenu_dialled {
+ height: 1em;
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.mx_DialPadContextMenu_dialPad {
+ margin: 16px;
+}
+
+.mx_DialPadContextMenu_horizSep {
+ position: relative;
+ &::before {
+ content: '';
+ position: absolute;
+ width: 100%;
+ border-bottom: 1px solid $input-darker-bg-color;
+ }
+}
diff --git a/res/css/views/voip/_DialPadModal.scss b/res/css/views/voip/_DialPadModal.scss
new file mode 100644
index 0000000000..f9d7673a38
--- /dev/null
+++ b/res/css/views/voip/_DialPadModal.scss
@@ -0,0 +1,74 @@
+/*
+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_Dialog_dialPadWrapper .mx_Dialog {
+ padding: 0px;
+}
+
+.mx_DialPadModal {
+ width: 192px;
+ height: 368px;
+}
+
+.mx_DialPadModal_header {
+ margin-top: 12px;
+ margin-left: 12px;
+ margin-right: 12px;
+}
+
+.mx_DialPadModal_title {
+ color: $muted-fg-color;
+ font-size: 12px;
+ font-weight: 600;
+}
+
+.mx_DialPadModal_cancel {
+ float: right;
+ mask: url('$(res)/img/feather-customised/cancel.svg');
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: cover;
+ width: 14px;
+ height: 14px;
+ background-color: $dialog-close-fg-color;
+ cursor: pointer;
+}
+
+.mx_DialPadModal_field {
+ border: none;
+ margin: 0px;
+}
+
+.mx_DialPadModal_field input {
+ font-size: 18px;
+ font-weight: 600;
+}
+
+.mx_DialPadModal_dialPad {
+ margin-left: 16px;
+ margin-right: 16px;
+ margin-top: 16px;
+}
+
+.mx_DialPadModal_horizSep {
+ position: relative;
+ &::before {
+ content: '';
+ position: absolute;
+ width: 100%;
+ border-bottom: 1px solid $input-darker-bg-color;
+ }
+}
diff --git a/res/img/element-icons/call/delete.svg b/res/img/element-icons/call/delete.svg
new file mode 100644
index 0000000000..133bdad4ca
--- /dev/null
+++ b/res/img/element-icons/call/delete.svg
@@ -0,0 +1,10 @@
+
diff --git a/res/img/element-icons/roomlist/dialpad.svg b/res/img/element-icons/roomlist/dialpad.svg
new file mode 100644
index 0000000000..b51d4a4dc9
--- /dev/null
+++ b/res/img/element-icons/roomlist/dialpad.svg
@@ -0,0 +1,3 @@
+
diff --git a/res/img/voip/dialpad.svg b/res/img/voip/dialpad.svg
new file mode 100644
index 0000000000..79c9ba1612
--- /dev/null
+++ b/res/img/voip/dialpad.svg
@@ -0,0 +1,17 @@
+
diff --git a/scripts/ci/install-deps.sh b/scripts/ci/install-deps.sh
index 14b5fc5393..bbda74ef9d 100755
--- a/scripts/ci/install-deps.sh
+++ b/scripts/ci/install-deps.sh
@@ -7,7 +7,6 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk
pushd matrix-js-sdk
yarn link
yarn install $@
-yarn build
popd
yarn link matrix-js-sdk
diff --git a/scripts/ci/layered.sh b/scripts/ci/layered.sh
index 306f9c9974..039f90c7df 100755
--- a/scripts/ci/layered.sh
+++ b/scripts/ci/layered.sh
@@ -20,6 +20,7 @@ popd
yarn link matrix-js-sdk
yarn link
yarn install
+yarn reskindex
# Finally, set up element-web
scripts/fetchdep.sh vector-im element-web
diff --git a/scripts/reskindex.js b/scripts/reskindex.js
index 9fb0e1a7c0..12310b77c1 100755
--- a/scripts/reskindex.js
+++ b/scripts/reskindex.js
@@ -1,29 +1,30 @@
#!/usr/bin/env node
-var fs = require('fs');
-var path = require('path');
-var glob = require('glob');
-var args = require('minimist')(process.argv);
-var chokidar = require('chokidar');
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const util = require('util');
+const args = require('minimist')(process.argv);
+const chokidar = require('chokidar');
-var componentIndex = path.join('src', 'component-index.js');
-var componentIndexTmp = componentIndex+".tmp";
-var componentsDir = path.join('src', 'components');
-var componentJsGlob = '**/*.js';
-var componentTsGlob = '**/*.tsx';
-var prevFiles = [];
+const componentIndex = path.join('src', 'component-index.js');
+const componentIndexTmp = componentIndex+".tmp";
+const componentsDir = path.join('src', 'components');
+const componentJsGlob = '**/*.js';
+const componentTsGlob = '**/*.tsx';
+let prevFiles = [];
-function reskindex() {
- var jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort();
- var tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort();
- var files = [...tsFiles, ...jsFiles];
+async function reskindex() {
+ const jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort();
+ const tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort();
+ const files = [...tsFiles, ...jsFiles];
if (!filesHaveChanged(files, prevFiles)) {
return;
}
prevFiles = files;
- var header = args.h || args.header;
+ const header = args.h || args.header;
- var strm = fs.createWriteStream(componentIndexTmp);
+ const strm = fs.createWriteStream(componentIndexTmp);
if (header) {
strm.write(fs.readFileSync(header));
@@ -38,11 +39,11 @@ function reskindex() {
strm.write(" */\n\n");
strm.write("let components = {};\n");
- for (var i = 0; i < files.length; ++i) {
- var file = files[i].replace('.js', '').replace('.tsx', '');
+ for (let i = 0; i < files.length; ++i) {
+ const file = files[i].replace('.js', '').replace('.tsx', '');
- var moduleName = (file.replace(/\//g, '.'));
- var importName = moduleName.replace(/\./g, "$");
+ const moduleName = (file.replace(/\//g, '.'));
+ const importName = moduleName.replace(/\./g, "$");
strm.write("import " + importName + " from './components/" + file + "';\n");
strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
@@ -51,9 +52,10 @@ function reskindex() {
}
strm.write("export {components};\n");
- strm.end();
+ // Ensure the file has been fully written to disk before proceeding
+ await util.promisify(strm.end);
fs.rename(componentIndexTmp, componentIndex, function(err) {
- if(err) {
+ if (err) {
console.error("Error moving new index into place: " + err);
} else {
console.log('Reskindex: completed');
@@ -67,7 +69,7 @@ function filesHaveChanged(files, prevFiles) {
return true;
}
// Check for name changes
- for (var i = 0; i < files.length; i++) {
+ for (let i = 0; i < files.length; i++) {
if (prevFiles[i] !== files[i]) {
return true;
}
@@ -81,7 +83,7 @@ if (!args.w) {
return;
}
-var watchDebouncer = null;
+let watchDebouncer = null;
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
if (path === componentIndex) return;
if (watchDebouncer) clearTimeout(watchDebouncer);
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 741798761f..2a28c8e43f 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -36,6 +36,7 @@ import {Analytics} from "../Analytics";
import CountlyAnalytics from "../CountlyAnalytics";
import UserActivity from "../UserActivity";
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
+import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
declare global {
interface Window {
@@ -59,6 +60,7 @@ declare global {
mxNotifier: typeof Notifier;
mxRightPanelStore: RightPanelStore;
mxWidgetStore: WidgetStore;
+ mxWidgetLayoutStore: WidgetLayoutStore;
mxCallHandler: CallHandler;
mxAnalytics: Analytics;
mxCountlyAnalytics: typeof CountlyAnalytics;
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 504dae5c84..bc57f25813 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -82,6 +82,10 @@ import CountlyAnalytics from "./CountlyAnalytics";
import {UIFeature} from "./settings/UIFeature";
import { CallError } from "matrix-js-sdk/src/webrtc/call";
import { logger } from 'matrix-js-sdk/src/logger';
+import { Action } from './dispatcher/actions';
+import { roomForVirtualRoom, getOrCreateVirtualRoomForRoom } from './VoipUserMapper';
+
+const CHECK_PSTN_SUPPORT_ATTEMPTS = 3;
enum AudioID {
Ring = 'ringAudio',
@@ -119,6 +123,8 @@ export default class CallHandler {
private calls = new Map(); // roomId -> call
private audioPromises = new Map>();
private dispatcherRef: string = null;
+ private supportsPstnProtocol = null;
+ private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser
static sharedInstance() {
if (!window.mxCallHandler) {
@@ -128,6 +134,15 @@ export default class CallHandler {
return window.mxCallHandler;
}
+ /*
+ * Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
+ * if a voip_mxid_translate_pattern is set in the config)
+ */
+ public static roomIdForCall(call: MatrixCall) {
+ if (!call) return null;
+ return roomForVirtualRoom(call.roomId) || call.roomId;
+ }
+
start() {
this.dispatcherRef = dis.register(this.onAction);
// add empty handlers for media actions, otherwise the media keys
@@ -145,6 +160,8 @@ export default class CallHandler {
if (SettingsStore.getValue(UIFeature.Voip)) {
MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming);
}
+
+ this.checkForPstnSupport(CHECK_PSTN_SUPPORT_ATTEMPTS);
}
stop() {
@@ -158,6 +175,33 @@ export default class CallHandler {
}
}
+ private async checkForPstnSupport(maxTries) {
+ try {
+ const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
+ if (protocols['im.vector.protocol.pstn'] !== undefined) {
+ this.supportsPstnProtocol = protocols['im.vector.protocol.pstn'];
+ } else if (protocols['m.protocol.pstn'] !== undefined) {
+ this.supportsPstnProtocol = protocols['m.protocol.pstn'];
+ } else {
+ this.supportsPstnProtocol = null;
+ }
+ dis.dispatch({action: Action.PstnSupportUpdated});
+ } catch (e) {
+ if (maxTries === 1) {
+ console.log("Failed to check for pstn protocol support and no retries remain: assuming no support", e);
+ } else {
+ console.log("Failed to check for pstn protocol support: will retry", e);
+ this.pstnSupportCheckTimer = setTimeout(() => {
+ this.checkForPstnSupport(maxTries - 1);
+ }, 10000);
+ }
+ }
+ }
+
+ getSupportsPstnProtocol() {
+ return this.supportsPstnProtocol;
+ }
+
private onCallIncoming = (call) => {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
@@ -250,11 +294,15 @@ export default class CallHandler {
// We don't allow placing more than one call per room, but that doesn't mean there
// can't be more than one, eg. in a glare situation. This checks that the given call
// is the call we consider 'the' call for its room.
- const callForThisRoom = this.getCallForRoom(call.roomId);
+ const mappedRoomId = CallHandler.roomIdForCall(call);
+
+ const callForThisRoom = this.getCallForRoom(mappedRoomId);
return callForThisRoom && call.callId === callForThisRoom.callId;
}
private setCallListeners(call: MatrixCall) {
+ const mappedRoomId = CallHandler.roomIdForCall(call);
+
call.on(CallEvent.Error, (err: CallError) => {
if (!this.matchesCallForThisRoom(call)) return;
@@ -284,7 +332,7 @@ export default class CallHandler {
Analytics.trackEvent('voip', 'callHangup');
- this.removeCallForRoom(call.roomId);
+ this.removeCallForRoom(mappedRoomId);
});
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
if (!this.matchesCallForThisRoom(call)) return;
@@ -309,7 +357,7 @@ export default class CallHandler {
break;
case CallState.Ended:
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
- this.removeCallForRoom(call.roomId);
+ this.removeCallForRoom(mappedRoomId);
if (oldState === CallState.InviteSent && (
call.hangupParty === CallParty.Remote ||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
@@ -358,7 +406,7 @@ export default class CallHandler {
this.pause(AudioID.Ringback);
}
- this.calls.set(newCall.roomId, newCall);
+ this.calls.set(mappedRoomId, newCall);
this.setCallListeners(newCall);
this.setCallState(newCall, newCall.state);
});
@@ -370,13 +418,15 @@ export default class CallHandler {
}
private setCallState(call: MatrixCall, status: CallState) {
+ const mappedRoomId = CallHandler.roomIdForCall(call);
+
console.log(
- `Call state in ${call.roomId} changed to ${status}`,
+ `Call state in ${mappedRoomId} changed to ${status}`,
);
dis.dispatch({
action: 'call_state',
- room_id: call.roomId,
+ room_id: mappedRoomId,
state: status,
});
}
@@ -443,14 +493,20 @@ export default class CallHandler {
}, null, true);
}
- private placeCall(
+ private async placeCall(
roomId: string, type: PlaceCallType,
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
) {
Analytics.trackEvent('voip', 'placeCall', 'type', type);
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
- const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
+
+ const mappedRoomId = (await getOrCreateVirtualRoomForRoom(roomId)) || roomId;
+ logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
+
+ const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
+
this.calls.set(roomId, call);
+
this.setCallListeners(call);
this.setCallAudioElement(call);
@@ -552,13 +608,14 @@ export default class CallHandler {
const call = payload.call as MatrixCall;
- if (this.getCallForRoom(call.roomId)) {
+ const mappedRoomId = CallHandler.roomIdForCall(call);
+ if (this.getCallForRoom(mappedRoomId)) {
// ignore multiple incoming calls to the same room
return;
}
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
- this.calls.set(call.roomId, call)
+ this.calls.set(mappedRoomId, call)
this.setCallListeners(call);
}
break;
diff --git a/src/TextForEvent.js b/src/TextForEvent.js
index 56e9abc0f2..3afe41d216 100644
--- a/src/TextForEvent.js
+++ b/src/TextForEvent.js
@@ -19,6 +19,7 @@ import * as Roles from './Roles';
import {isValid3pidInvite} from "./RoomInvite";
import SettingsStore from "./settings/SettingsStore";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
+import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages"
@@ -477,6 +478,11 @@ function textForWidgetEvent(event) {
}
}
+function textForWidgetLayoutEvent(event) {
+ const senderName = event.sender?.name || event.getSender();
+ return _t("%(senderName)s has updated the widget layout", {senderName});
+}
+
function textForMjolnirEvent(event) {
const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent();
@@ -583,6 +589,7 @@ const stateHandlers = {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
'im.vector.modular.widgets': textForWidgetEvent,
+ [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
};
// Add all the Mjolnir stuff to the renderer
diff --git a/src/VoipUserMapper.ts b/src/VoipUserMapper.ts
new file mode 100644
index 0000000000..a4f5822065
--- /dev/null
+++ b/src/VoipUserMapper.ts
@@ -0,0 +1,79 @@
+/*
+Copyright 2021 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 { ensureDMExists, findDMForUser } from './createRoom';
+import { MatrixClientPeg } from "./MatrixClientPeg";
+import DMRoomMap from "./utils/DMRoomMap";
+import SdkConfig from "./SdkConfig";
+
+// Functions for mapping users & rooms for the voip_mxid_translate_pattern
+// config option
+
+export function voipUserMapperEnabled(): boolean {
+ return SdkConfig.get()['voip_mxid_translate_pattern'] !== undefined;
+}
+
+// only exported for tests
+export function userToVirtualUser(userId: string, templateString?: string): string {
+ if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
+ if (!templateString) return null;
+ return templateString.replace('${mxid}', encodeURIComponent(userId).replace(/%/g, '=').toLowerCase());
+}
+
+// only exported for tests
+export function virtualUserToUser(userId: string, templateString?: string): string {
+ if (templateString === undefined) templateString = SdkConfig.get()['voip_mxid_translate_pattern'];
+ if (!templateString) return null;
+
+ const regexString = templateString.replace('${mxid}', '(.+)');
+
+ const match = userId.match('^' + regexString + '$');
+ if (!match) return null;
+
+ return decodeURIComponent(match[1].replace(/=/g, '%'));
+}
+
+async function getOrCreateVirtualRoomForUser(userId: string):Promise {
+ const virtualUser = userToVirtualUser(userId);
+ if (!virtualUser) return null;
+
+ return await ensureDMExists(MatrixClientPeg.get(), virtualUser);
+}
+
+export async function getOrCreateVirtualRoomForRoom(roomId: string):Promise {
+ const user = DMRoomMap.shared().getUserIdForRoomId(roomId);
+ if (!user) return null;
+ return getOrCreateVirtualRoomForUser(user);
+}
+
+export function roomForVirtualRoom(roomId: string):string {
+ const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
+ if (!virtualUser) return null;
+ const realUser = virtualUserToUser(virtualUser);
+ const room = findDMForUser(MatrixClientPeg.get(), realUser);
+ if (room) {
+ return room.roomId;
+ } else {
+ return null;
+ }
+}
+
+export function isVirtualRoom(roomId: string):boolean {
+ const virtualUser = DMRoomMap.shared().getUserIdForRoomId(roomId);
+ if (!virtualUser) return null;
+ const realUser = virtualUserToUser(virtualUser);
+ return Boolean(realUser);
+}
diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
index ab39a094db..863ee2b427 100644
--- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
+++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
@@ -95,7 +95,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const blob = new Blob([this._keyBackupInfo.recovery_key], {
type: 'text/plain;charset=us-ascii',
});
- FileSaver.saveAs(blob, 'recovery-key.txt');
+ FileSaver.saveAs(blob, 'security-key.txt');
this.setState({
downloaded: true,
@@ -238,7 +238,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
)}
{_t(
"We'll store an encrypted copy of your keys on our server. " +
- "Secure your backup with a recovery passphrase.",
+ "Secure your backup with a Security Phrase.",
)}
{_t("For maximum security, this should be different from your account password.")}
@@ -252,10 +252,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField}
autoFocus={true}
- label={_td("Enter a recovery passphrase")}
- labelEnterPassword={_td("Enter a recovery passphrase")}
- labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")}
- labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")}
+ label={_td("Enter a Security Phrase")}
+ labelEnterPassword={_td("Enter a Security Phrase")}
+ labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
+ labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/>
@@ -270,7 +270,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{_t("Advanced")}
- {_t("Set up with a recovery key")}
+ {_t("Set up with a Security Key")}
;
@@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return