diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000000..2c068fff33
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @matrix-org/element-web
diff --git a/.node-version b/.node-version
new file mode 100644
index 0000000000..8351c19397
--- /dev/null
+++ b/.node-version
@@ -0,0 +1 @@
+14
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73b383d76d..4d65a524d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,122 @@
+Changes in [3.27.0](https://github.com/vector-im/element-desktop/releases/tag/v3.27.0) (2021-07-02)
+===================================================================================================
+
+## ๐ SECURITY FIXES
+ * Sanitize untrusted variables from message previews before translation
+ Fixes vector-im/element-web#18314
+
+## โจ Features
+ * Fix editing of `` & ` & ``
+ [\#6469](https://github.com/matrix-org/matrix-react-sdk/pull/6469)
+ Fixes vector-im/element-web#18211
+ * Zoom images in lightbox to where the cursor points
+ [\#6418](https://github.com/matrix-org/matrix-react-sdk/pull/6418)
+ Fixes vector-im/element-web#17870
+ * Avoid hitting the settings store from TextForEvent
+ [\#6205](https://github.com/matrix-org/matrix-react-sdk/pull/6205)
+ Fixes vector-im/element-web#17650
+ * Initial MSC3083 + MSC3244 support
+ [\#6212](https://github.com/matrix-org/matrix-react-sdk/pull/6212)
+ Fixes vector-im/element-web#17686 and vector-im/element-web#17661
+ * Navigate to the first room with notifications when clicked on space notification dot
+ [\#5974](https://github.com/matrix-org/matrix-react-sdk/pull/5974)
+ * Add matrix: to the list of permitted URL schemes
+ [\#6388](https://github.com/matrix-org/matrix-react-sdk/pull/6388)
+ * Add "Copy Link" to room context menu
+ [\#6374](https://github.com/matrix-org/matrix-react-sdk/pull/6374)
+ * ๐ญ Message bubble layout
+ [\#6291](https://github.com/matrix-org/matrix-react-sdk/pull/6291)
+ Fixes vector-im/element-web#4635, vector-im/element-web#17773 vector-im/element-web#16220 and vector-im/element-web#7687
+ * Play only one audio file at a time
+ [\#6417](https://github.com/matrix-org/matrix-react-sdk/pull/6417)
+ Fixes vector-im/element-web#17439
+ * Move download button for media to the action bar
+ [\#6386](https://github.com/matrix-org/matrix-react-sdk/pull/6386)
+ Fixes vector-im/element-web#17943
+ * Improved display of one-to-one call history with summary boxes for each call
+ [\#6121](https://github.com/matrix-org/matrix-react-sdk/pull/6121)
+ Fixes vector-im/element-web#16409
+ * Notification settings UI refresh
+ [\#6352](https://github.com/matrix-org/matrix-react-sdk/pull/6352)
+ Fixes vector-im/element-web#17782
+ * Fix EventIndex double handling events and erroring
+ [\#6385](https://github.com/matrix-org/matrix-react-sdk/pull/6385)
+ Fixes vector-im/element-web#18008
+ * Improve reply rendering
+ [\#3553](https://github.com/matrix-org/matrix-react-sdk/pull/3553)
+ Fixes vector-im/riot-web#9217, vector-im/riot-web#7633, vector-im/riot-web#7530, vector-im/riot-web#7169, vector-im/riot-web#7151, vector-im/riot-web#6692 vector-im/riot-web#6579 and vector-im/element-web#17440
+
+## ๐ Bug Fixes
+ * Fix CreateRoomDialog exploding when making public room outside of a space
+ [\#6493](https://github.com/matrix-org/matrix-react-sdk/pull/6493)
+ * Fix regression where registration would soft-crash on captcha
+ [\#6505](https://github.com/matrix-org/matrix-react-sdk/pull/6505)
+ Fixes vector-im/element-web#18284
+ * only send join rule event if we have a join rule to put in it
+ [\#6517](https://github.com/matrix-org/matrix-react-sdk/pull/6517)
+ * Improve the new download button's discoverability and interactions.
+ [\#6510](https://github.com/matrix-org/matrix-react-sdk/pull/6510)
+ * Fix voice recording UI looking broken while microphone permissions are being requested.
+ [\#6479](https://github.com/matrix-org/matrix-react-sdk/pull/6479)
+ Fixes vector-im/element-web#18223
+ * Match colors of room and user avatars in DMs
+ [\#6393](https://github.com/matrix-org/matrix-react-sdk/pull/6393)
+ Fixes vector-im/element-web#2449
+ * Fix onPaste handler to work with copying files from Finder
+ [\#5389](https://github.com/matrix-org/matrix-react-sdk/pull/5389)
+ Fixes vector-im/element-web#15536 and vector-im/element-web#16255
+ * Fix infinite pagination loop when offline
+ [\#6478](https://github.com/matrix-org/matrix-react-sdk/pull/6478)
+ Fixes vector-im/element-web#18242
+ * Fix blurhash rounded corners missing regression
+ [\#6467](https://github.com/matrix-org/matrix-react-sdk/pull/6467)
+ Fixes vector-im/element-web#18110
+ * Fix position of the space hierarchy spinner
+ [\#6462](https://github.com/matrix-org/matrix-react-sdk/pull/6462)
+ Fixes vector-im/element-web#18182
+ * Fix display of image messages that lack thumbnails
+ [\#6456](https://github.com/matrix-org/matrix-react-sdk/pull/6456)
+ Fixes vector-im/element-web#18175
+ * Fix crash with large audio files.
+ [\#6436](https://github.com/matrix-org/matrix-react-sdk/pull/6436)
+ Fixes vector-im/element-web#18149
+ * Make diff colors in codeblocks more pleasant
+ [\#6355](https://github.com/matrix-org/matrix-react-sdk/pull/6355)
+ Fixes vector-im/element-web#17939
+ * Show the correct audio file duration while loading the file.
+ [\#6435](https://github.com/matrix-org/matrix-react-sdk/pull/6435)
+ Fixes vector-im/element-web#18160
+ * Fix various timeline settings not applying immediately.
+ [\#6261](https://github.com/matrix-org/matrix-react-sdk/pull/6261)
+ Fixes vector-im/element-web#17748
+ * Fix issues with room list duplication
+ [\#6391](https://github.com/matrix-org/matrix-react-sdk/pull/6391)
+ Fixes vector-im/element-web#14508
+ * Fix grecaptcha throwing useless error sometimes
+ [\#6401](https://github.com/matrix-org/matrix-react-sdk/pull/6401)
+ Fixes vector-im/element-web#15142
+ * Update Emojibase and Twemoji and switch to IamCal (Slack-style) shortcodes
+ [\#6347](https://github.com/matrix-org/matrix-react-sdk/pull/6347)
+ Fixes vector-im/element-web#13857 and vector-im/element-web#13334
+ * Respect compound emojis in default avatar initial generation
+ [\#6397](https://github.com/matrix-org/matrix-react-sdk/pull/6397)
+ Fixes vector-im/element-web#18040
+ * Fix bug where the 'other homeserver' field in the server selection dialog would become briefly focus and then unfocus when clicked.
+ [\#6394](https://github.com/matrix-org/matrix-react-sdk/pull/6394)
+ Fixes vector-im/element-web#18031
+ * Standardise spelling and casing of homeserver, identity server, and integration manager
+ [\#6365](https://github.com/matrix-org/matrix-react-sdk/pull/6365)
+ * Fix widgets not receiving decrypted events when they have permission.
+ [\#6371](https://github.com/matrix-org/matrix-react-sdk/pull/6371)
+ Fixes vector-im/element-web#17615
+ * Prevent client hangs when calculating blurhashes
+ [\#6366](https://github.com/matrix-org/matrix-react-sdk/pull/6366)
+ Fixes vector-im/element-web#17945
+ * Exclude state events from widgets reading room events
+ [\#6378](https://github.com/matrix-org/matrix-react-sdk/pull/6378)
+ * Cache feature_spaces\* flags to improve performance
+ [\#6381](https://github.com/matrix-org/matrix-react-sdk/pull/6381)
+
Changes in [3.26.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0) (2021-07-19)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.26.0-rc.1...v3.26.0)
diff --git a/README.md b/README.md
index b3e96ef001..67e5e12f59 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ All code lands on the `develop` branch - `master` is only used for stable releas
**Please file PRs against `develop`!!**
Please follow the standard Matrix contributor's guide:
-https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst
+https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.md
Please follow the Matrix JS/React code style as per:
https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md
diff --git a/package.json b/package.json
index b73462d188..2445e3c973 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.26.0",
+ "version": "3.27.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -80,13 +80,14 @@
"katex": "^0.12.0",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.20",
- "matrix-js-sdk": "12.1.0",
+ "matrix-js-sdk": "12.2.0",
"matrix-widget-api": "^0.1.0-beta.15",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"parse5": "^6.0.1",
"png-chunks-extract": "^1.0.0",
+ "posthog-js": "1.12.2",
"prop-types": "^15.7.2",
"qrcode": "^1.4.4",
"re-resizable": "^6.9.0",
@@ -123,6 +124,7 @@
"@babel/traverse": "^7.12.12",
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
"@peculiar/webcrypto": "^1.1.4",
+ "@sentry/types": "^6.10.0",
"@sinonjs/fake-timers": "^7.0.2",
"@types/classnames": "^2.2.11",
"@types/commonmark": "^0.27.4",
@@ -147,13 +149,14 @@
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
+ "allchange": "github:matrix-org/allchange",
"babel-jest": "^26.6.3",
"chokidar": "^3.5.1",
"concurrently": "^5.3.0",
"enzyme": "^3.11.0",
"eslint": "7.18.0",
"eslint-config-google": "^0.14.0",
- "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
+ "eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#2306b3d4da4eba908b256014b979f1d3d43d2945",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"glob": "^7.1.6",
@@ -166,6 +169,7 @@
"matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
"react-test-renderer": "^17.0.2",
"rimraf": "^3.0.2",
+ "rrweb-snapshot": "1.1.7",
"stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0",
@@ -189,7 +193,8 @@
"decoderWorker\\.min\\.js": "/__mocks__/empty.js",
"decoderWorker\\.min\\.wasm": "/__mocks__/empty.js",
"waveWorker\\.min\\.js": "/__mocks__/empty.js",
- "workers/(.+)\\.worker\\.ts": "/__mocks__/workerMock.js"
+ "workers/(.+)\\.worker\\.ts": "/__mocks__/workerMock.js",
+ "RecorderWorklet": "/__mocks__/empty.js"
},
"transformIgnorePatterns": [
"/node_modules/(?!matrix-js-sdk).+$"
diff --git a/release_config.yaml b/release_config.yaml
new file mode 100644
index 0000000000..12e857cbdd
--- /dev/null
+++ b/release_config.yaml
@@ -0,0 +1,4 @@
+subprojects:
+ matrix-js-sdk:
+ includeByDefault: false
+
diff --git a/res/css/_components.scss b/res/css/_components.scss
index 20b2461960..92d2bfe7f3 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -67,7 +67,6 @@
@import "./views/dialogs/_AddExistingToSpaceDialog.scss";
@import "./views/dialogs/_AddressPickerDialog.scss";
@import "./views/dialogs/_Analytics.scss";
-@import "./views/dialogs/_BetaFeedbackDialog.scss";
@import "./views/dialogs/_BugReportDialog.scss";
@import "./views/dialogs/_ChangelogDialog.scss";
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
@@ -76,16 +75,20 @@
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
@import "./views/dialogs/_CreateGroupDialog.scss";
@import "./views/dialogs/_CreateRoomDialog.scss";
+@import "./views/dialogs/_CreateSubspaceDialog.scss";
@import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_ForwardDialog.scss";
+@import "./views/dialogs/_GenericFeatureFeedbackDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss";
+@import "./views/dialogs/_JoinRuleDropdown.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
+@import "./views/dialogs/_LeaveSpaceDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss";
@@ -263,6 +266,7 @@
@import "./views/spaces/_SpacePublicShare.scss";
@import "./views/terms/_InlineTermsAgreement.scss";
@import "./views/toasts/_AnalyticsToast.scss";
+@import "./views/toasts/_IncomingCallToast.scss";
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
@import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallContainer.scss";
diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
index e64057d16c..1dea6332f5 100644
--- a/res/css/structures/_SpacePanel.scss
+++ b/res/css/structures/_SpacePanel.scss
@@ -297,7 +297,7 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceButton:hover,
.mx_SpaceButton:focus-within,
.mx_SpaceButton_hasMenuOpen {
- &:not(.mx_SpaceButton_home):not(.mx_SpaceButton_invite) {
+ &:not(.mx_SpaceButton_invite) {
// Hide the badge container on hover because it'll be a menu button
.mx_SpacePanel_badgeContainer {
width: 0;
@@ -368,6 +368,14 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpacePanel_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
}
+
+ .mx_SpacePanel_noIcon {
+ display: none;
+
+ & + .mx_IconizedContextMenu_label {
+ padding-left: 5px !important; // override default iconized label style to align with header
+ }
+ }
}
diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss
index bc343f535c..cb91aa3c7d 100644
--- a/res/css/structures/_SpaceRoomDirectory.scss
+++ b/res/css/structures/_SpaceRoomDirectory.scss
@@ -61,6 +61,7 @@ limitations under the License.
.mx_AccessibleButton_kind_link {
padding: 0;
+ font-size: inherit;
}
.mx_SearchBox {
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index e4832d9430..58a4b426c2 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -335,24 +335,17 @@ $SpaceRoomViewInnerWidth: 428px;
word-wrap: break-word;
}
- > hr {
- border: none;
- height: 1px;
- background-color: $groupFilterPanel-bg-color;
- }
-
.mx_SearchBox {
margin: 0 0 20px;
flex: 0;
}
.mx_SpaceFeedbackPrompt {
- margin-bottom: 16px;
-
- // hide the HR as we have our own
- & + hr {
- display: none;
- }
+ padding: 7px; // 8px - 1px border
+ border: 1px solid $menu-border-color;
+ border-radius: 8px;
+ width: max-content;
+ margin: 0 0 -40px auto; // collapse its own height to not push other components down
}
.mx_SpaceRoomDirectory_list {
@@ -513,66 +506,3 @@ $SpaceRoomViewInnerWidth: 428px;
}
}
}
-
-.mx_SpaceFeedbackPrompt {
- margin-top: 18px;
- margin-bottom: 12px;
-
- > hr {
- border: none;
- border-top: 1px solid $input-border-color;
- margin-bottom: 12px;
- }
-
- > div {
- display: flex;
- flex-direction: row;
- font-size: $font-15px;
- line-height: $font-24px;
-
- > span {
- color: $secondary-fg-color;
- position: relative;
- padding-left: 32px;
- font-size: inherit;
- line-height: inherit;
- margin-right: auto;
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- top: 2px;
- height: 20px;
- width: 20px;
- background-color: $secondary-fg-color;
- mask-repeat: no-repeat;
- mask-size: contain;
- mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
- mask-position: center;
- }
- }
-
- .mx_AccessibleButton_kind_link {
- color: $accent-color;
- position: relative;
- padding: 0 0 0 24px;
- margin-left: 8px;
- font-size: inherit;
- line-height: inherit;
-
- &::before {
- content: '';
- position: absolute;
- left: 0;
- height: 16px;
- width: 16px;
- background-color: $accent-color;
- mask-repeat: no-repeat;
- mask-size: contain;
- mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
- mask-position: center;
- }
- }
- }
-}
diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss
index d248568740..2c3f1c705c 100644
--- a/res/css/structures/_ToastContainer.scss
+++ b/res/css/structures/_ToastContainer.scss
@@ -28,7 +28,7 @@ limitations under the License.
margin: 0 4px;
grid-row: 2 / 4;
grid-column: 1;
- background-color: $dark-panel-bg-color;
+ background-color: $toast-bg-color;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
border-radius: 8px;
}
@@ -37,7 +37,7 @@ limitations under the License.
grid-row: 1 / 3;
grid-column: 1;
color: $primary-fg-color;
- background-color: $dark-panel-bg-color;
+ background-color: $toast-bg-color;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.5);
border-radius: 8px;
overflow: hidden;
diff --git a/res/css/views/context_menus/_IconizedContextMenu.scss b/res/css/views/context_menus/_IconizedContextMenu.scss
index 204435995f..ff176eef7e 100644
--- a/res/css/views/context_menus/_IconizedContextMenu.scss
+++ b/res/css/views/context_menus/_IconizedContextMenu.scss
@@ -99,6 +99,10 @@ limitations under the License.
.mx_IconizedContextMenu_icon + .mx_IconizedContextMenu_label {
padding-left: 14px;
}
+
+ .mx_BetaCard_betaPill {
+ margin-left: 16px;
+ }
}
}
@@ -145,12 +149,17 @@ limitations under the License.
}
}
- .mx_IconizedContextMenu_checked {
+ .mx_IconizedContextMenu_checked,
+ .mx_IconizedContextMenu_unchecked {
margin-left: 16px;
margin-right: -5px;
+ }
- &::before {
- mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
- }
+ .mx_IconizedContextMenu_checked::before {
+ mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
+ }
+
+ .mx_IconizedContextMenu_unchecked::before {
+ content: unset;
}
}
diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
index 2776c477fc..42e17c8d98 100644
--- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
+++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
@@ -50,64 +50,11 @@ limitations under the License.
line-height: $font-15px;
}
- .mx_AddExistingToSpace_entry {
- display: flex;
- margin-top: 12px;
-
- // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
- .mx_DecoratedRoomAvatar {
- margin-right: 12px;
- }
-
- .mx_AddExistingToSpace_entry_name {
- font-size: $font-15px;
- line-height: 30px;
- flex-grow: 1;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- margin-right: 12px;
- }
-
- .mx_Checkbox {
- align-items: center;
- }
- }
- }
-
- .mx_AddExistingToSpace_section_spaces {
- .mx_BaseAvatar {
- margin-right: 12px;
- }
-
- .mx_BaseAvatar_image {
- border-radius: 8px;
- }
- }
-
- .mx_AddExistingToSpace_section_experimental {
- position: relative;
- border-radius: 8px;
- margin: 12px 0;
- padding: 8px 8px 8px 42px;
- background-color: $header-panel-bg-color;
-
- font-size: $font-12px;
- line-height: $font-15px;
- color: $secondary-fg-color;
-
- &::before {
- content: '';
- position: absolute;
- left: 10px;
- top: calc(50% - 8px); // vertical centering
- height: 16px;
- width: 16px;
- background-color: $secondary-fg-color;
- mask-repeat: no-repeat;
- mask-size: contain;
- mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
- mask-position: center;
+ .mx_AccessibleButton_kind_link {
+ font-size: $font-12px;
+ line-height: $font-15px;
+ margin-top: 8px;
+ padding: 0;
}
}
@@ -205,77 +152,106 @@ limitations under the License.
min-height: 0;
height: 80vh;
- .mx_Dialog_title {
- display: flex;
-
- .mx_BaseAvatar_image {
- border-radius: 8px;
- margin: 0;
- vertical-align: unset;
- }
-
- .mx_BaseAvatar {
- display: inline-flex;
- margin: auto 16px auto 5px;
- vertical-align: middle;
- }
-
- > div {
- > h1 {
- font-weight: $font-semi-bold;
- font-size: $font-18px;
- line-height: $font-22px;
- margin: 0;
- }
-
- .mx_AddExistingToSpaceDialog_onlySpace {
- color: $secondary-fg-color;
- font-size: $font-15px;
- line-height: $font-24px;
- }
- }
-
- .mx_Dropdown_input {
- border: none;
-
- > .mx_Dropdown_option {
- padding-left: 0;
- flex: unset;
- height: unset;
- color: $secondary-fg-color;
- font-size: $font-15px;
- line-height: $font-24px;
-
- .mx_BaseAvatar {
- display: none;
- }
- }
-
- .mx_Dropdown_menu {
- .mx_AddExistingToSpaceDialog_dropdownOptionActive {
- color: $accent-color;
- padding-right: 32px;
- position: relative;
-
- &::before {
- content: '';
- width: 20px;
- height: 20px;
- top: 8px;
- right: 0;
- position: absolute;
- mask-position: center;
- mask-size: contain;
- mask-repeat: no-repeat;
- background-color: $accent-color;
- mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
- }
- }
- }
- }
- }
-
.mx_AddExistingToSpace {
display: contents;
}
}
+
+.mx_SubspaceSelector {
+ display: flex;
+
+ .mx_BaseAvatar_image {
+ border-radius: 8px;
+ margin: 0;
+ vertical-align: unset;
+ }
+
+ .mx_BaseAvatar {
+ display: inline-flex;
+ margin: auto 16px auto 5px;
+ vertical-align: middle;
+ }
+
+ > div {
+ > h1 {
+ font-weight: $font-semi-bold;
+ font-size: $font-18px;
+ line-height: $font-22px;
+ margin: 0;
+ }
+ }
+
+ .mx_Dropdown_input {
+ border: none;
+
+ > .mx_Dropdown_option {
+ padding-left: 0;
+ flex: unset;
+ height: unset;
+ color: $secondary-fg-color;
+ font-size: $font-15px;
+ line-height: $font-24px;
+
+ .mx_BaseAvatar {
+ display: none;
+ }
+ }
+
+ .mx_Dropdown_menu {
+ .mx_SubspaceSelector_dropdownOptionActive {
+ color: $accent-color;
+ padding-right: 32px;
+ position: relative;
+
+ &::before {
+ content: '';
+ width: 20px;
+ height: 20px;
+ top: 8px;
+ right: 0;
+ position: absolute;
+ mask-position: center;
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ background-color: $accent-color;
+ mask-image: url('$(res)/img/element-icons/roomlist/checkmark.svg');
+ }
+ }
+ }
+ }
+
+ .mx_SubspaceSelector_onlySpace {
+ color: $secondary-fg-color;
+ font-size: $font-15px;
+ line-height: $font-24px;
+ }
+}
+
+.mx_AddExistingToSpace_entry {
+ display: flex;
+ margin-top: 12px;
+
+ .mx_DecoratedRoomAvatar, // we can't target .mx_BaseAvatar here as it'll break the decorated avatar styling
+ .mx_BaseAvatar.mx_RoomAvatar_isSpaceRoom {
+ margin-right: 12px;
+ }
+
+ img.mx_RoomAvatar_isSpaceRoom,
+ .mx_RoomAvatar_isSpaceRoom img {
+ border-radius: 8px;
+ }
+
+ .mx_AddExistingToSpace_entry_name {
+ font-size: $font-15px;
+ line-height: 30px;
+ flex-grow: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ margin-right: 12px;
+ }
+
+ .mx_Checkbox {
+ align-items: center;
+ }
+}
diff --git a/res/css/views/dialogs/_CreateRoomDialog.scss b/res/css/views/dialogs/_CreateRoomDialog.scss
index 5321d8ff69..e7cfbf6050 100644
--- a/res/css/views/dialogs/_CreateRoomDialog.scss
+++ b/res/css/views/dialogs/_CreateRoomDialog.scss
@@ -109,56 +109,4 @@ limitations under the License.
margin: 0 85px 0 0;
font-size: $font-12px;
}
-
- .mx_Dropdown {
- margin-bottom: 8px;
- font-weight: normal;
- font-family: $font-family;
- font-size: $font-14px;
- color: $primary-fg-color;
-
- .mx_Dropdown_input {
- border: 1px solid $input-border-color;
- }
-
- .mx_Dropdown_option {
- font-size: $font-14px;
- line-height: $font-32px;
- height: 32px;
- min-height: 32px;
-
- > div {
- padding-left: 30px;
- position: relative;
-
- &::before {
- content: "";
- position: absolute;
- height: 16px;
- width: 16px;
- left: 6px;
- top: 8px;
- mask-repeat: no-repeat;
- mask-position: center;
- background-color: $secondary-fg-color;
- }
- }
- }
-
- .mx_CreateRoomDialog_dropdown_invite::before {
- mask-image: url('$(res)/img/element-icons/lock.svg');
- mask-size: contain;
- }
-
- .mx_CreateRoomDialog_dropdown_public::before {
- mask-image: url('$(res)/img/globe.svg');
- mask-size: 12px;
- }
-
- .mx_CreateRoomDialog_dropdown_restricted::before {
- mask-image: url('$(res)/img/element-icons/community-members.svg');
- mask-size: contain;
- }
- }
}
-
diff --git a/res/css/views/dialogs/_CreateSubspaceDialog.scss b/res/css/views/dialogs/_CreateSubspaceDialog.scss
new file mode 100644
index 0000000000..1ec4731ae6
--- /dev/null
+++ b/res/css/views/dialogs/_CreateSubspaceDialog.scss
@@ -0,0 +1,81 @@
+/*
+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.
+*/
+
+.mx_CreateSubspaceDialog_wrapper {
+ .mx_Dialog {
+ display: flex;
+ flex-direction: column;
+ }
+}
+
+.mx_CreateSubspaceDialog {
+ width: 480px;
+ color: $primary-fg-color;
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ min-height: 0;
+
+ .mx_CreateSubspaceDialog_content {
+ flex-grow: 1;
+
+ .mx_CreateSubspaceDialog_betaNotice {
+ padding: 12px 16px;
+ border-radius: 8px;
+ background-color: $header-panel-bg-color;
+
+ .mx_BetaCard_betaPill {
+ margin-right: 8px;
+ vertical-align: middle;
+ }
+ }
+
+ .mx_JoinRuleDropdown + p {
+ color: $muted-fg-color;
+ font-size: $font-12px;
+ }
+ }
+
+ .mx_CreateSubspaceDialog_footer {
+ display: flex;
+ margin-top: 20px;
+
+ .mx_CreateSubspaceDialog_footer_prompt {
+ flex-grow: 1;
+ font-size: $font-12px;
+ line-height: $font-15px;
+ color: $secondary-fg-color;
+
+ > * {
+ vertical-align: middle;
+ }
+ }
+
+ .mx_AccessibleButton {
+ display: inline-block;
+ align-self: center;
+ }
+
+ .mx_AccessibleButton_kind_primary {
+ margin-left: 16px;
+ padding: 8px 36px;
+ }
+
+ .mx_AccessibleButton_kind_link {
+ padding: 0;
+ }
+ }
+}
diff --git a/res/css/views/dialogs/_BetaFeedbackDialog.scss b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss
similarity index 90%
rename from res/css/views/dialogs/_BetaFeedbackDialog.scss
rename to res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss
index 9f5f6b512e..f83eed9c53 100644
--- a/res/css/views/dialogs/_BetaFeedbackDialog.scss
+++ b/res/css/views/dialogs/_GenericFeatureFeedbackDialog.scss
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_BetaFeedbackDialog {
- .mx_BetaFeedbackDialog_subheading {
+.mx_GenericFeatureFeedbackDialog {
+ .mx_GenericFeatureFeedbackDialog_subheading {
color: $primary-fg-color;
font-size: $font-14px;
line-height: $font-20px;
diff --git a/res/css/views/dialogs/_JoinRuleDropdown.scss b/res/css/views/dialogs/_JoinRuleDropdown.scss
new file mode 100644
index 0000000000..c48a79af3c
--- /dev/null
+++ b/res/css/views/dialogs/_JoinRuleDropdown.scss
@@ -0,0 +1,67 @@
+/*
+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.
+*/
+
+.mx_JoinRuleDropdown {
+ margin-bottom: 8px;
+ font-weight: normal;
+ font-family: $font-family;
+ font-size: $font-14px;
+ color: $primary-fg-color;
+
+ .mx_Dropdown_input {
+ border: 1px solid $input-border-color;
+ }
+
+ .mx_Dropdown_option {
+ font-size: $font-14px;
+ line-height: $font-32px;
+ height: 32px;
+ min-height: 32px;
+
+ > div {
+ padding-left: 30px;
+ position: relative;
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: 16px;
+ width: 16px;
+ left: 6px;
+ top: 8px;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ background-color: $secondary-fg-color;
+ }
+ }
+ }
+
+ .mx_JoinRuleDropdown_invite::before {
+ mask-image: url('$(res)/img/element-icons/lock.svg');
+ mask-size: contain;
+ }
+
+ .mx_JoinRuleDropdown_public::before {
+ mask-image: url('$(res)/img/globe.svg');
+ mask-size: 12px;
+ }
+
+ .mx_JoinRuleDropdown_restricted::before {
+ mask-image: url('$(res)/img/element-icons/community-members.svg');
+ mask-size: contain;
+ }
+}
+
diff --git a/res/css/views/dialogs/_LeaveSpaceDialog.scss b/res/css/views/dialogs/_LeaveSpaceDialog.scss
new file mode 100644
index 0000000000..c982f50e52
--- /dev/null
+++ b/res/css/views/dialogs/_LeaveSpaceDialog.scss
@@ -0,0 +1,96 @@
+/*
+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.
+*/
+
+.mx_LeaveSpaceDialog_wrapper {
+ .mx_Dialog {
+ display: flex;
+ flex-direction: column;
+ padding: 24px 32px;
+ }
+}
+
+.mx_LeaveSpaceDialog {
+ width: 440px;
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ max-height: 520px;
+
+ .mx_Dialog_content {
+ flex-grow: 1;
+ margin: 0;
+ overflow-y: auto;
+
+ .mx_RadioButton + .mx_RadioButton {
+ margin-top: 16px;
+ }
+
+ .mx_SearchBox {
+ // To match the space around the title
+ margin: 0 0 15px 0;
+ flex-grow: 0;
+ border-radius: 8px;
+ }
+
+ .mx_LeaveSpaceDialog_noResults {
+ display: block;
+ margin-top: 24px;
+ }
+
+ .mx_LeaveSpaceDialog_section {
+ margin: 16px 0;
+ }
+
+ .mx_LeaveSpaceDialog_section_warning {
+ position: relative;
+ border-radius: 8px;
+ margin: 12px 0 0;
+ padding: 12px 8px 12px 42px;
+ background-color: $header-panel-bg-color;
+
+ font-size: $font-12px;
+ line-height: $font-15px;
+ color: $secondary-fg-color;
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 10px;
+ top: calc(50% - 8px); // vertical centering
+ height: 16px;
+ width: 16px;
+ background-color: $secondary-fg-color;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
+ mask-position: center;
+ }
+ }
+
+ > p {
+ color: $primary-fg-color;
+ }
+ }
+
+ .mx_Dialog_buttons {
+ margin-top: 20px;
+
+ .mx_Dialog_primary {
+ background-color: $notice-primary-color !important; // override default colour
+ border-color: $notice-primary-color;
+ }
+ }
+}
diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss
index 49a0a44417..bd81aafef3 100644
--- a/res/css/views/elements/_DesktopCapturerSourcePicker.scss
+++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss
@@ -35,7 +35,6 @@ limitations under the License.
.mx_desktopCapturerSourcePicker_source_thumbnail {
margin: 4px;
padding: 4px;
- width: 312px;
border-width: 2px;
border-radius: 8px;
border-style: solid;
@@ -53,6 +52,5 @@ limitations under the License.
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
- width: 312px;
}
}
diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss
index f2ceb086c4..0c1b41ca38 100644
--- a/res/css/views/messages/_CallEvent.scss
+++ b/res/css/views/messages/_CallEvent.scss
@@ -23,7 +23,7 @@ limitations under the License.
background-color: $dark-panel-bg-color;
border-radius: 8px;
margin: 10px auto;
- max-width: 75%;
+ width: 75%;
box-sizing: border-box;
height: 60px;
diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss
index 403f671673..d941a8132f 100644
--- a/res/css/views/messages/_MFileBody.scss
+++ b/res/css/views/messages/_MFileBody.scss
@@ -1,5 +1,5 @@
/*
-Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C.
+Copyright 2015 - 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.
@@ -60,6 +60,8 @@ limitations under the License.
}
.mx_MFileBody_info {
+ cursor: pointer;
+
.mx_MFileBody_info_icon {
background-color: $message-body-panel-icon-bg-color;
border-radius: 20px;
diff --git a/res/css/views/messages/_ViewSourceEvent.scss b/res/css/views/messages/_ViewSourceEvent.scss
index 66825030e0..b0e40a5152 100644
--- a/res/css/views/messages/_ViewSourceEvent.scss
+++ b/res/css/views/messages/_ViewSourceEvent.scss
@@ -43,8 +43,10 @@ limitations under the License.
margin-bottom: 7px;
mask-image: url('$(res)/img/feather-customised/minimise.svg');
}
+}
- &:hover .mx_ViewSourceEvent_toggle {
+.mx_EventTile:hover {
+ .mx_ViewSourceEvent_toggle {
visibility: visible;
}
}
diff --git a/res/css/views/rooms/_EventBubbleTile.scss b/res/css/views/rooms/_EventBubbleTile.scss
index 1d43c3030d..1e25deba26 100644
--- a/res/css/views/rooms/_EventBubbleTile.scss
+++ b/res/css/views/rooms/_EventBubbleTile.scss
@@ -38,18 +38,22 @@ limitations under the License.
padding-top: 0;
}
+ &::before {
+ content: '';
+ position: absolute;
+ top: -1px;
+ bottom: -1px;
+ left: -60px;
+ right: -60px;
+ z-index: -1;
+ border-radius: 4px;
+ }
+
&:hover,
&.mx_EventTile_selected {
+
&::before {
- content: '';
- position: absolute;
- top: -1px;
- bottom: -1px;
- left: -60px;
- right: -60px;
- z-index: -1;
background: $eventbubble-bg-hover;
- border-radius: 4px;
}
.mx_EventTile_avatar {
@@ -267,7 +271,7 @@ limitations under the License.
display: flex;
align-items: center;
- justify-content: center;
+ justify-content: start;
padding: 5px 0;
.mx_EventTile_avatar {
@@ -275,12 +279,27 @@ limitations under the License.
order: -1;
margin-right: 5px;
}
+
+ .mx_EventTile_line,
+ .mx_EventTile_info {
+ min-width: 100%;
+ }
+
+ .mx_EventTile_e2eIcon {
+ margin-left: 9px;
+ }
+
+ .mx_EventTile_line > a {
+ right: auto;
+ top: -15px;
+ left: -68px;
+ }
}
.mx_EventListSummary[data-layout=bubble] {
- --maxWidth: 80%;
+ --maxWidth: 70%;
margin-left: calc(var(--avatarSize) + var(--gutterSize));
- margin-right: calc(var(--gutterSize) + var(--avatarSize));
+ margin-right: 94px;
.mx_EventListSummary_toggle {
float: none;
margin: 0;
diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 4a419244ff..1c9d8e87d9 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -59,7 +59,6 @@ $hover-select-border: 4px;
font-size: $font-14px;
display: inline-block; /* anti-zalgo, with overflow hidden */
overflow: hidden;
- cursor: pointer;
padding-bottom: 0px;
padding-top: 0px;
margin: 0px;
@@ -132,15 +131,6 @@ $hover-select-border: 4px;
}
}
- &.mx_EventTile_info .mx_EventTile_line,
- & ~ .mx_EventListSummary > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line {
- padding-left: calc($left-gutter + 18px);
- }
-
- & ~ .mx_EventListSummary .mx_EventTile_line {
- padding-left: calc($left-gutter);
- }
-
&.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
}
@@ -276,10 +266,19 @@ $hover-select-border: 4px;
.mx_ReactionsRow {
margin: 0;
- padding: 6px 60px;
+ padding: 4px 64px;
}
}
+.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line,
+.mx_EventListSummary:not([data-layout=bubble]) > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line {
+ padding-left: calc($left-gutter + 18px);
+}
+
+.mx_EventListSummary:not([data-layout=bubble]) .mx_EventTile_line {
+ padding-left: calc($left-gutter);
+}
+
/* all the overflow-y: hidden; are to trap Zalgos -
but they introduce an implicit overflow-x: auto.
so make that explicitly hidden too to avoid random
@@ -311,17 +310,19 @@ $hover-select-border: 4px;
}
.mx_RoomView_timeline_rr_enabled {
-
- .mx_EventTile:not([data-layout=bubble]) {
+ .mx_EventTile[data-layout=group] {
.mx_EventTile_line {
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
margin-right: 110px;
}
}
-
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
}
+.mx_SenderProfile {
+ cursor: pointer;
+}
+
.mx_EventTile_bubbleContainer {
display: grid;
grid-template-columns: 1fr 100px;
@@ -456,8 +457,14 @@ $hover-select-border: 4px;
/* Various markdown overrides */
-.mx_EventTile_body pre {
- border: 1px solid transparent;
+.mx_EventTile_body {
+ a:hover {
+ text-decoration: underline;
+ }
+
+ pre {
+ border: 1px solid transparent;
+ }
}
.mx_EventTile_content .markdown-body {
diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss
index 5501ab343e..5d7e733213 100644
--- a/res/css/views/rooms/_VoiceRecordComposerTile.scss
+++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss
@@ -46,6 +46,21 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/trashcan.svg');
}
+.mx_VoiceRecordComposerTile_uploadingState {
+ margin-right: 10px;
+ color: $secondary-fg-color;
+}
+
+.mx_VoiceRecordComposerTile_failedState {
+ margin-right: 21px;
+
+ .mx_VoiceRecordComposerTile_uploadState_badge {
+ display: inline-block;
+ margin-right: 4px;
+ vertical-align: middle;
+ }
+}
+
.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
// Note: remaining class properties are in the PlayerContainer CSS.
@@ -68,7 +83,7 @@ limitations under the License.
height: 10px;
position: absolute;
left: 12px; // 12px from the left edge for container padding
- top: 18px; // vertically center (middle align with clock)
+ top: 16px; // vertically center (middle align with clock)
border-radius: 10px;
}
}
diff --git a/res/css/views/settings/_ProfileSettings.scss b/res/css/views/settings/_ProfileSettings.scss
index 4cbcb8e708..63a5fa7edf 100644
--- a/res/css/views/settings/_ProfileSettings.scss
+++ b/res/css/views/settings/_ProfileSettings.scss
@@ -16,6 +16,7 @@ limitations under the License.
.mx_ProfileSettings_controls_topic {
& > textarea {
+ font-family: inherit;
resize: vertical;
}
}
diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss
index 8e6b99871c..9f40372690 100644
--- a/res/css/views/settings/tabs/_SettingsTab.scss
+++ b/res/css/views/settings/tabs/_SettingsTab.scss
@@ -72,6 +72,13 @@ limitations under the License.
padding-right: 10px;
}
+.mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_microcopy {
+ margin-top: 4px;
+ font-size: $font-12px;
+ line-height: $font-15px;
+ color: $secondary-fg-color;
+}
+
.mx_SettingsTab_section .mx_SettingsFlag .mx_ToggleSwitch {
float: right;
}
diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss
index 88b9d8f693..097b2b648e 100644
--- a/res/css/views/spaces/_SpaceCreateMenu.scss
+++ b/res/css/views/spaces/_SpaceCreateMenu.scss
@@ -43,6 +43,12 @@ $spacePanelWidth: 71px;
color: $secondary-fg-color;
margin: 0;
}
+
+ .mx_SpaceFeedbackPrompt {
+ border-top: 1px solid $input-border-color;
+ padding-top: 12px;
+ margin-top: 16px;
+ }
}
// XXX remove this when spaces leaves Beta
@@ -99,3 +105,25 @@ $spacePanelWidth: 71px;
}
}
}
+
+.mx_SpaceFeedbackPrompt {
+ font-size: $font-15px;
+ line-height: $font-24px;
+
+ > span {
+ color: $secondary-fg-color;
+ position: relative;
+ font-size: inherit;
+ line-height: inherit;
+ margin-right: auto;
+ }
+
+ .mx_AccessibleButton_kind_link {
+ color: $accent-color;
+ position: relative;
+ padding: 0;
+ margin-left: 8px;
+ font-size: inherit;
+ line-height: inherit;
+ }
+}
diff --git a/res/css/views/toasts/_IncomingCallToast.scss b/res/css/views/toasts/_IncomingCallToast.scss
new file mode 100644
index 0000000000..975628f948
--- /dev/null
+++ b/res/css/views/toasts/_IncomingCallToast.scss
@@ -0,0 +1,149 @@
+/*
+Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2021 ล imon Brandner
+
+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_IncomingCallToast {
+ display: flex;
+ flex-direction: row;
+ pointer-events: initial; // restore pointer events so the user can accept/decline
+
+ .mx_IncomingCallToast_content {
+ display: flex;
+ flex-direction: column;
+ margin-left: 8px;
+
+ .mx_CallEvent_caller {
+ font-weight: bold;
+ font-size: $font-15px;
+ line-height: $font-18px;
+
+ margin-top: 2px;
+ }
+
+ .mx_CallEvent_type {
+ font-size: $font-12px;
+ line-height: $font-15px;
+ color: $tertiary-fg-color;
+
+ margin-top: 4px;
+ margin-bottom: 6px;
+
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+
+ .mx_CallEvent_type_icon {
+ height: 16px;
+ width: 16px;
+ margin-right: 6px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ height: inherit;
+ width: inherit;
+ background-color: $tertiary-fg-color;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+ }
+ }
+
+ &.mx_IncomingCallToast_content_voice {
+ .mx_CallEvent_type .mx_CallEvent_type_icon::before,
+ .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before {
+ mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+ }
+ }
+
+ &.mx_IncomingCallToast_content_video {
+ .mx_CallEvent_type .mx_CallEvent_type_icon::before,
+ .mx_IncomingCallToast_buttons .mx_IncomingCallToast_button_accept span::before {
+ mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+ }
+ }
+
+ .mx_IncomingCallToast_buttons {
+ margin-top: 8px;
+ display: flex;
+ flex-direction: row;
+ gap: 12px;
+
+ .mx_IncomingCallToast_button {
+ height: 24px;
+ padding: 0px 8px;
+ flex-shrink: 0;
+ flex-grow: 1;
+ margin-right: 0;
+ font-size: $font-15px;
+ line-height: $font-24px;
+
+ span {
+ padding: 8px 0;
+ display: flex;
+ align-items: center;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ background-color: $button-fg-color;
+ mask-position: center;
+ mask-repeat: no-repeat;
+ margin-right: 8px;
+ }
+ }
+
+ &.mx_IncomingCallToast_button_accept span::before {
+ mask-size: 13px;
+ width: 13px;
+ height: 13px;
+ }
+
+ &.mx_IncomingCallToast_button_decline span::before {
+ mask-image: url('$(res)/img/element-icons/call/hangup.svg');
+ mask-size: 16px;
+ width: 16px;
+ height: 16px;
+ }
+ }
+ }
+ }
+
+ .mx_IncomingCallToast_iconButton {
+ display: flex;
+ height: 20px;
+ width: 20px;
+
+ &::before {
+ content: '';
+
+ height: inherit;
+ width: inherit;
+ background-color: $tertiary-fg-color;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ mask-position: center;
+ }
+ }
+
+ .mx_IncomingCallToast_silence::before {
+ mask-image: url('$(res)/img/voip/silence.svg');
+ }
+
+ .mx_IncomingCallToast_unSilence::before {
+ mask-image: url('$(res)/img/voip/un-silence.svg');
+ }
+}
diff --git a/res/css/views/voip/_CallContainer.scss b/res/css/views/voip/_CallContainer.scss
index 0c09070334..d11ab9bf9f 100644
--- a/res/css/views/voip/_CallContainer.scss
+++ b/res/css/views/voip/_CallContainer.scss
@@ -28,7 +28,6 @@ limitations under the License.
.mx_CallPreview {
pointer-events: initial; // restore pointer events so the user can leave/interact
- cursor: pointer;
.mx_VideoFeed_remote.mx_VideoFeed_voice {
min-height: 150px;
@@ -43,84 +42,4 @@ limitations under the License.
.mx_AppTile_persistedWrapper div {
min-width: 350px;
}
-
- .mx_IncomingCallBox {
- min-width: 250px;
- background-color: $voipcall-plinth-color;
- padding: 8px;
- box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
- border-radius: 8px;
-
- pointer-events: initial; // restore pointer events so the user can accept/decline
- cursor: pointer;
-
- .mx_IncomingCallBox_CallerInfo {
- display: flex;
- direction: row;
-
- img, .mx_BaseAvatar_initial {
- margin: 8px;
- }
-
- > div {
- display: flex;
- flex-direction: column;
-
- justify-content: center;
- }
-
- h1, p {
- margin: 0px;
- padding: 0px;
- font-size: $font-14px;
- line-height: $font-16px;
- }
-
- h1 {
- font-weight: bold;
- }
- }
-
- .mx_IncomingCallBox_buttons {
- padding: 8px;
- display: flex;
- flex-direction: row;
-
- > .mx_IncomingCallBox_spacer {
- width: 8px;
- }
-
- > * {
- flex-shrink: 0;
- flex-grow: 1;
- margin-right: 0;
- font-size: $font-15px;
- line-height: $font-24px;
- }
- }
-
- .mx_IncomingCallBox_iconButton {
- position: absolute;
- right: 8px;
-
- &::before {
- content: '';
-
- height: 20px;
- width: 20px;
- background-color: $icon-button-color;
- mask-repeat: no-repeat;
- mask-size: contain;
- mask-position: center;
- }
- }
-
- .mx_IncomingCallBox_silence::before {
- mask-image: url('$(res)/img/voip/silence.svg');
- }
-
- .mx_IncomingCallBox_unSilence::before {
- mask-image: url('$(res)/img/voip/un-silence.svg');
- }
- }
}
diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index 59298ef8e6..c473a1fc79 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -39,7 +39,7 @@ limitations under the License.
.mx_CallView_pip {
width: 320px;
padding-bottom: 8px;
- background-color: $voipcall-plinth-color;
+ background-color: $toast-bg-color;
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
border-radius: 8px;
@@ -76,16 +76,22 @@ limitations under the License.
&.mx_VideoFeed_voice {
// We don't want to collide with the call controls that have 52px of height
- padding-bottom: 52px;
+ margin-bottom: 52px;
background-color: $inverted-bg-color;
display: flex;
justify-content: center;
align-items: center;
}
- &.mx_VideoFeed_video {
+ .mx_VideoFeed_video {
+ height: 100%;
background-color: #000;
}
+
+ .mx_VideoFeed_mic {
+ left: 10px;
+ bottom: 10px;
+ }
}
}
@@ -202,6 +208,7 @@ limitations under the License.
align-items: center;
justify-content: left;
flex-shrink: 0;
+ cursor: pointer;
}
.mx_CallView_header_callType {
@@ -279,7 +286,7 @@ limitations under the License.
max-width: 240px;
}
-.mx_CallView_header_phoneIcon {
+.mx_CallView_header_callTypeIcon {
display: inline-block;
margin-right: 6px;
height: 16px;
@@ -293,12 +300,19 @@ limitations under the License.
height: 16px;
width: 16px;
- background-color: $warning-color;
+ background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
+ }
+
+ &.mx_CallView_header_callTypeIcon_voice::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
+
+ &.mx_CallView_header_callTypeIcon_video::before {
+ mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+ }
}
.mx_CallView_callControls {
@@ -306,7 +320,6 @@ limitations under the License.
display: flex;
justify-content: center;
bottom: 5px;
- width: 100%;
opacity: 1;
transition: opacity 0.5s;
z-index: 200; // To be above _all_ feeds
diff --git a/res/css/views/voip/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss
index 79bf3cbf09..892a137a32 100644
--- a/res/css/views/voip/_CallViewSidebar.scss
+++ b/res/css/views/voip/_CallViewSidebar.scss
@@ -35,12 +35,23 @@ limitations under the License.
width: 100%;
&.mx_VideoFeed_voice {
+ border-radius: 4px;
+
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 16 / 9;
}
+
+ .mx_VideoFeed_video {
+ border-radius: 4px;
+ }
+
+ .mx_VideoFeed_mic {
+ left: 6px;
+ bottom: 6px;
+ }
}
&.mx_CallViewSidebar_pipMode {
diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss
index 0019994e72..527d223ffc 100644
--- a/res/css/views/voip/_DialPadContextMenu.scss
+++ b/res/css/views/voip/_DialPadContextMenu.scss
@@ -69,7 +69,6 @@ limitations under the License.
overflow: hidden;
max-width: 185px;
text-align: left;
- direction: rtl;
padding: 8px 0px;
background-color: rgb(0, 0, 0, 0);
}
diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss
index 07a4a0e530..3a0f62636e 100644
--- a/res/css/views/voip/_VideoFeed.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -15,18 +15,52 @@ limitations under the License.
*/
.mx_VideoFeed {
- border-radius: 4px;
-
+ overflow: hidden;
+ position: relative;
&.mx_VideoFeed_voice {
background-color: $inverted-bg-color;
}
- &.mx_VideoFeed_video {
+ .mx_VideoFeed_video {
+ width: 100%;
background-color: transparent;
+
+ &.mx_VideoFeed_video_mirror {
+ transform: scale(-1, 1);
+ }
+ }
+
+ .mx_VideoFeed_mic {
+ position: absolute;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ width: 24px;
+ height: 24px;
+
+ background-color: rgba(0, 0, 0, 0.5); // Same on both themes
+ border-radius: 100%;
+
+ &::before {
+ position: absolute;
+ content: "";
+ width: 16px;
+ height: 16px;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ mask-position: center;
+ background-color: white; // Same on both themes
+ border-radius: 7px;
+ }
+
+ &.mx_VideoFeed_mic_muted::before {
+ mask-image: url('$(res)/img/voip/mic-muted.svg');
+ }
+
+ &.mx_VideoFeed_mic_unmuted::before {
+ mask-image: url('$(res)/img/voip/mic-unmuted.svg');
+ }
}
}
-
-.mx_VideoFeed_mirror {
- transform: scale(-1, 1);
-}
diff --git a/res/img/element-icons/room/pin.svg b/res/img/element-icons/room/pin.svg
index 2448fc61c5..f090f60be8 100644
--- a/res/img/element-icons/room/pin.svg
+++ b/res/img/element-icons/room/pin.svg
@@ -1,7 +1,3 @@
diff --git a/res/img/voip/mic-muted.svg b/res/img/voip/mic-muted.svg
new file mode 100644
index 0000000000..0cb7ad1c9e
--- /dev/null
+++ b/res/img/voip/mic-muted.svg
@@ -0,0 +1,5 @@
+
diff --git a/res/img/voip/mic-unmuted.svg b/res/img/voip/mic-unmuted.svg
new file mode 100644
index 0000000000..8334cafa0a
--- /dev/null
+++ b/res/img/voip/mic-unmuted.svg
@@ -0,0 +1,4 @@
+
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 655492661c..e4ea2bb57e 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -1,3 +1,6 @@
+// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
+$system-dark: #21262C;
+
// unified palette
// try to use these colors when possible
$bg-color: #15191E;
@@ -47,7 +50,7 @@ $inverted-bg-color: $base-color;
$selected-color: $room-highlight-color;
// selected for hoverover & selected event tiles
-$event-selected-color: #21262c;
+$event-selected-color: $system-dark;
// used for the hairline dividers in RoomView
$primary-hairline-color: transparent;
@@ -91,7 +94,7 @@ $lightbox-background-bg-color: #000;
$lightbox-background-bg-opacity: 0.85;
$settings-grey-fg-color: #a2a2a2;
-$settings-profile-placeholder-bg-color: #21262c;
+$settings-profile-placeholder-bg-color: $system-dark;
$settings-profile-overlay-placeholder-fg-color: #454545;
$settings-profile-button-bg-color: #e7e7e7;
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
@@ -112,8 +115,8 @@ $eventtile-meta-color: $roomtopic-color;
$header-divider-color: $header-panel-text-primary-color;
$composer-e2e-icon-color: $header-panel-text-primary-color;
-// this probably shouldn't have it's own colour
-$voipcall-plinth-color: #394049;
+$quinary-content-color: #394049;
+$toast-bg-color: $quinary-content-color;
// ********************
@@ -175,7 +178,7 @@ $button-link-bg-color: transparent;
$togglesw-off-color: $room-highlight-color;
$progressbar-fg-color: $accent-color;
-$progressbar-bg-color: #21262c;
+$progressbar-bg-color: $system-dark;
$visual-bell-bg-color: #800;
@@ -210,7 +213,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #394049; // "Dark Tile"
$message-body-panel-icon-fg-color: $secondary-fg-color;
-$message-body-panel-icon-bg-color: #21262C; // "System Dark"
+$message-body-panel-icon-bg-color: $system-dark; // "System Dark"
$voice-record-stop-border-color: $quaternary-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
@@ -228,9 +231,9 @@ $groupFilterPanel-background-blur-amount: 30px;
$composer-shadow-color: rgba(0, 0, 0, 0.28);
// Bubble tiles
-$eventbubble-self-bg: #143A34;
-$eventbubble-others-bg: #394049;
-$eventbubble-bg-hover: #433C23;
+$eventbubble-self-bg: #14322E;
+$eventbubble-others-bg: $event-selected-color;
+$eventbubble-bg-hover: #1C2026;
$eventbubble-avatar-outline: $bg-color;
$eventbubble-reply-color: #C1C6CD;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 0c0197cfb0..064b532bb0 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -111,8 +111,8 @@ $eventtile-meta-color: $roomtopic-color;
$header-divider-color: $header-panel-text-primary-color;
$composer-e2e-icon-color: $header-panel-text-primary-color;
-// this probably shouldn't have it's own colour
-$voipcall-plinth-color: #394049;
+$quinary-content-color: #394049;
+$toast-bg-color: $quinary-content-color;
// ********************
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index b7d45452ff..1a63c9bd07 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -8,9 +8,12 @@
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */
-$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji';
+$font-family: 'Nunito', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
-$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji';
+$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
+
+// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
+$system-light: #F4F6FA;
// unified palette
// try to use these colors when possible
@@ -178,8 +181,8 @@ $eventtile-meta-color: $roomtopic-color;
$composer-e2e-icon-color: #91a1c0;
$header-divider-color: #91a1c0;
-// this probably shouldn't have it's own colour
-$voipcall-plinth-color: #F4F6FA;
+$toast-bg-color: $system-light;
+$voipcall-plinth-color: $system-light;
// ********************
@@ -331,7 +334,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #E3E8F0;
$message-body-panel-icon-fg-color: $secondary-fg-color;
-$message-body-panel-icon-bg-color: #F4F6FA;
+$message-body-panel-icon-bg-color: $system-light;
// See non-legacy _light for variable information
$voice-record-stop-symbol-color: #ff4b55;
@@ -348,9 +351,9 @@ $appearance-tab-border-color: $input-darker-bg-color;
$composer-shadow-color: tranparent;
// Bubble tiles
-$eventbubble-self-bg: #F8FDFC;
-$eventbubble-others-bg: #F7F8F9;
-$eventbubble-bg-hover: rgb(242, 242, 242);
+$eventbubble-self-bg: #F0FBF8;
+$eventbubble-others-bg: $system-light;
+$eventbubble-bg-hover: #FAFBFD;
$eventbubble-avatar-outline: #fff;
$eventbubble-reply-color: #C1C6CD;
@@ -390,7 +393,7 @@ $eventbubble-reply-color: #C1C6CD;
@define-mixin mx_DialogButton_secondary {
// flip colours for the secondary ones
font-weight: 600;
- border: 1px solid $accent-color ! important;
+ border: 1px solid $accent-color !important;
color: $accent-color;
background-color: $button-secondary-bg-color;
}
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 32722515d8..eff9abe5af 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -8,9 +8,12 @@
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */
-$font-family: Inter, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji';
+$font-family: 'Inter', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Arial', 'Helvetica', sans-serif, 'Noto Color Emoji';
-$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji';
+$monospace-font-family: 'Inconsolata', 'Twemoji', 'Apple Color Emoji', 'Segoe UI Emoji', 'Courier', monospace, 'Noto Color Emoji';
+
+// Colors from Figma Compound https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed/Compound?node-id=557%3A0
+$system-light: #F4F6FA;
// unified palette
// try to use these colors when possible
@@ -138,7 +141,7 @@ $blockquote-bar-color: #ddd;
$blockquote-fg-color: #777;
$settings-grey-fg-color: #a2a2a2;
-$settings-profile-placeholder-bg-color: #f4f6fa;
+$settings-profile-placeholder-bg-color: $system-light;
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
$settings-profile-button-bg-color: #e7e7e7;
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
@@ -167,8 +170,8 @@ $eventtile-meta-color: $roomtopic-color;
$composer-e2e-icon-color: #91A1C0;
$header-divider-color: #91A1C0;
-// this probably shouldn't have it's own colour
-$voipcall-plinth-color: #F4F6FA;
+$toast-bg-color: $system-light;
+$voipcall-plinth-color: $system-light;
// ********************
@@ -327,7 +330,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #E3E8F0; // "Separator"
$message-body-panel-icon-fg-color: $secondary-fg-color;
-$message-body-panel-icon-bg-color: #F4F6FA;
+$message-body-panel-icon-bg-color: $system-light;
// These two don't change between themes. They are the $warning-color, but we don't
// want custom themes to affect them by accident.
@@ -350,9 +353,9 @@ $groupFilterPanel-background-blur-amount: 20px;
$composer-shadow-color: rgba(0, 0, 0, 0.04);
// Bubble tiles
-$eventbubble-self-bg: #F8FDFC;
-$eventbubble-others-bg: #F7F8F9;
-$eventbubble-bg-hover: #FEFCF5;
+$eventbubble-self-bg: #F0FBF8;
+$eventbubble-others-bg: $system-light;
+$eventbubble-bg-hover: #FAFBFD;
$eventbubble-avatar-outline: $primary-bg-color;
$eventbubble-reply-color: #C1C6CD;
@@ -392,7 +395,7 @@ $eventbubble-reply-color: #C1C6CD;
@define-mixin mx_DialogButton_secondary {
// flip colours for the secondary ones
font-weight: 600;
- border: 1px solid $accent-color ! important;
+ border: 1px solid $accent-color !important;
color: $accent-color;
background-color: $button-secondary-bg-color;
}
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index e7c1dda54f..77569711df 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -1,7 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017, 2018 New Vector Ltd
-Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
+Copyright 2019 - 2021 The Matrix.org Foundation C.I.C.
+Copyright 2021 ล imon Brandner
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -60,7 +61,6 @@ import Modal from './Modal';
import { _t } from './languageHandler';
import dis from './dispatcher/dispatcher';
import WidgetUtils from './utils/WidgetUtils';
-import WidgetEchoStore from './stores/WidgetEchoStore';
import SettingsStore from './settings/SettingsStore';
import { Jitsi } from "./widgets/Jitsi";
import { WidgetType } from "./widgets/WidgetType";
@@ -86,6 +86,12 @@ import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/
import EventEmitter from 'events';
import SdkConfig from './SdkConfig';
import { ensureDMExists, findDMForUser } from './createRoom';
+import { IPushRule, RuleId, TweakName, Tweaks } from "matrix-js-sdk/src/@types/PushRules";
+import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
+import { WidgetLayoutStore, Container } from './stores/widgets/WidgetLayoutStore';
+import { getIncomingCallToastKey } from './toasts/IncomingCallToast';
+import ToastStore from './stores/ToastStore';
+import IncomingCallToast from "./toasts/IncomingCallToast";
export const PROTOCOL_PSTN = 'm.protocol.pstn';
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
@@ -476,14 +482,28 @@ export default class CallHandler extends EventEmitter {
}
switch (newState) {
- case CallState.Ringing:
- this.play(AudioID.Ring);
+ case CallState.Ringing: {
+ const incomingCallPushRule = (
+ new PushProcessor(MatrixClientPeg.get()).getPushRuleById(RuleId.IncomingCall) as IPushRule
+ );
+ const pushRuleEnabled = incomingCallPushRule?.enabled;
+ const tweakSetToRing = incomingCallPushRule?.actions.some((action: Tweaks) => (
+ action.set_tweak === TweakName.Sound &&
+ action.value === "ring"
+ ));
+
+ if (pushRuleEnabled && tweakSetToRing) {
+ this.play(AudioID.Ring);
+ } else {
+ this.silenceCall(call.callId);
+ }
break;
- case CallState.InviteSent:
+ }
+ case CallState.InviteSent: {
this.play(AudioID.Ringback);
break;
- case CallState.Ended:
- {
+ }
+ case CallState.Ended: {
const hangupReason = call.hangupReason;
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
this.removeCallForRoom(mappedRoomId);
@@ -624,6 +644,19 @@ export default class CallHandler extends EventEmitter {
`Call state in ${mappedRoomId} changed to ${status}`,
);
+ const toastKey = getIncomingCallToastKey(call.callId);
+ if (status === CallState.Ringing) {
+ ToastStore.sharedInstance().addOrReplaceToast({
+ key: toastKey,
+ priority: 100,
+ component: IncomingCallToast,
+ bodyClassName: "mx_IncomingCallToast",
+ props: { call },
+ });
+ } else {
+ ToastStore.sharedInstance().dismissToast(toastKey);
+ }
+
dis.dispatch({
action: 'call_state',
room_id: mappedRoomId,
@@ -914,6 +947,8 @@ export default class CallHandler extends EventEmitter {
action: 'view_room',
room_id: roomId,
});
+
+ await this.placeCall(roomId, PlaceCallType.Voice, null);
}
private async startTransferToPhoneNumber(call: MatrixCall, destination: string, consultFirst: boolean) {
@@ -993,14 +1028,10 @@ export default class CallHandler extends EventEmitter {
// 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!'),
- });
+ const jitsiWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type));
+ if (jitsiWidget) {
+ // If there already is a Jitsi widget pin it
+ WidgetLayoutStore.instance.moveToContainer(room, jitsiWidget, Container.Top);
return;
}
diff --git a/src/ContentMessages.tsx b/src/ContentMessages.tsx
index c5bcb226ff..14a0c1ed51 100644
--- a/src/ContentMessages.tsx
+++ b/src/ContentMessages.tsx
@@ -209,6 +209,14 @@ async function loadImageElement(imageFile: File) {
return { width, height, img };
}
+// Minimum size for image files before we generate a thumbnail for them.
+const IMAGE_SIZE_THRESHOLD_THUMBNAIL = 1 << 15; // 32KB
+// Minimum size improvement for image thumbnails, if both are not met then don't bother uploading thumbnail.
+const IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE = 1 << 16; // 1MB
+const IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT = 0.1; // 10%
+// We don't apply these thresholds to video thumbnails as a poster image is always useful
+// and videos tend to be much larger.
+
/**
* Read the metadata for an image file and create and upload a thumbnail of the image.
*
@@ -217,23 +225,33 @@ async function loadImageElement(imageFile: File) {
* @param {File} imageFile The image to read and thumbnail.
* @return {Promise} A promise that resolves with the attachment info.
*/
-function infoForImageFile(matrixClient, roomId, imageFile) {
+async function infoForImageFile(matrixClient: MatrixClient, roomId: string, imageFile: File) {
let thumbnailType = "image/png";
if (imageFile.type === "image/jpeg") {
thumbnailType = "image/jpeg";
}
- let imageInfo;
- return loadImageElement(imageFile).then((r) => {
- return createThumbnail(r.img, r.width, r.height, thumbnailType);
- }).then((result) => {
- imageInfo = result.info;
- return uploadFile(matrixClient, roomId, result.thumbnail);
- }).then((result) => {
- imageInfo.thumbnail_url = result.url;
- imageInfo.thumbnail_file = result.file;
+ const imageElement = await loadImageElement(imageFile);
+
+ const result = await createThumbnail(imageElement.img, imageElement.width, imageElement.height, thumbnailType);
+ const imageInfo = result.info;
+
+ // we do all sizing checks here because we still rely on thumbnail generation for making a blurhash from.
+ const sizeDifference = imageFile.size - imageInfo.thumbnail_info.size;
+ if (
+ imageFile.size <= IMAGE_SIZE_THRESHOLD_THUMBNAIL || // image is small enough already
+ (sizeDifference <= IMAGE_THUMBNAIL_MIN_REDUCTION_SIZE && // thumbnail is not sufficiently smaller than original
+ sizeDifference <= (imageFile.size * IMAGE_THUMBNAIL_MIN_REDUCTION_PERCENT))
+ ) {
+ delete imageInfo["thumbnail_info"];
return imageInfo;
- });
+ }
+
+ const uploadResult = await uploadFile(matrixClient, roomId, result.thumbnail);
+
+ imageInfo["thumbnail_url"] = uploadResult.url;
+ imageInfo["thumbnail_file"] = uploadResult.file;
+ return imageInfo;
}
/**
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index af5d2b3019..2eee5214af 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -57,7 +57,33 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
-export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix'];
+export const PERMITTED_URL_SCHEMES = [
+ "bitcoin",
+ "ftp",
+ "geo",
+ "http",
+ "https",
+ "im",
+ "irc",
+ "ircs",
+ "magnet",
+ "mailto",
+ "matrix",
+ "mms",
+ "news",
+ "nntp",
+ "openpgp4fpr",
+ "sip",
+ "sftp",
+ "sms",
+ "smsto",
+ "ssh",
+ "tel",
+ "urn",
+ "webcal",
+ "wtai",
+ "xmpp",
+];
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js
index e91e1d72cf..ffece510de 100644
--- a/src/IdentityAuthClient.js
+++ b/src/IdentityAuthClient.js
@@ -146,23 +146,23 @@ export default class IdentityAuthClient {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('Default identity server terms warning', '',
QuestionDialog, {
- title: _t("Identity server has no terms of service"),
- description: (
-
-
{ _t(
- "This action requires accessing the default identity server " +
+ title: _t("Identity server has no terms of service"),
+ description: (
+
+
{ _t(
+ "This action requires accessing the default identity server " +
" to validate an email address or phone number, " +
"but the server does not have any terms of service.", {},
- {
- server: () => { abbreviateUrl(identityServerUrl) },
- },
- ) }
-
{ _t(
- "Only continue if you trust the owner of the server.",
- ) }
{ _t(
+ "Only continue if you trust the owner of the server.",
+ ) }
+
+ ),
+ button: _t("Trust"),
});
const [confirmed] = await finished;
if (confirmed) {
diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts
index 410124a637..e48fd52cb1 100644
--- a/src/Lifecycle.ts
+++ b/src/Lifecycle.ts
@@ -48,6 +48,7 @@ import { Jitsi } from "./widgets/Jitsi";
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform";
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
import CountlyAnalytics from "./CountlyAnalytics";
+import { PosthogAnalytics } from "./PosthogAnalytics";
import CallHandler from './CallHandler';
import LifecycleCustomisations from "./customisations/Lifecycle";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
@@ -573,6 +574,8 @@ async function doSetLoggedIn(
await abortLogin();
}
+ PosthogAnalytics.instance.updateAnonymityFromSettings(credentials.userId);
+
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
MatrixClientPeg.replaceUsingCreds(credentials);
@@ -700,6 +703,8 @@ export function logout(): void {
CountlyAnalytics.instance.enable(/* anonymous = */ true);
}
+ PosthogAnalytics.instance.logout();
+
if (MatrixClientPeg.get().isGuest()) {
// logout doesn't work for guest sessions
// Also we sometimes want to re-log in a guest session if we abort the login.
diff --git a/src/PosthogAnalytics.ts b/src/PosthogAnalytics.ts
new file mode 100644
index 0000000000..860a155aff
--- /dev/null
+++ b/src/PosthogAnalytics.ts
@@ -0,0 +1,355 @@
+/*
+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 posthog, { PostHog } from 'posthog-js';
+import PlatformPeg from './PlatformPeg';
+import SdkConfig from './SdkConfig';
+import SettingsStore from './settings/SettingsStore';
+
+/* Posthog analytics tracking.
+ *
+ * Anonymity behaviour is as follows:
+ *
+ * - If Posthog isn't configured in `config.json`, events are not sent.
+ * - If [Do Not Track](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is
+ * enabled, events are not sent (this detection is built into posthog and turned on via the
+ * `respect_dnt` flag being passed to `posthog.init`).
+ * - If the `feature_pseudonymous_analytics_opt_in` labs flag is `true`, track pseudonomously, i.e.
+ * hash all matrix identifiers in tracking events (user IDs, room IDs etc) using SHA-256.
+ * - Otherwise, if the existing `analyticsOptIn` flag is `true`, track anonymously, i.e.
+ * redact all matrix identifiers in tracking events.
+ * - If both flags are false or not set, events are not sent.
+ */
+
+interface IEvent {
+ // The event name that will be used by PostHog. Event names should use snake_case.
+ eventName: string;
+
+ // The properties of the event that will be stored in PostHog. This is just a placeholder,
+ // extending interfaces must override this with a concrete definition to do type validation.
+ properties: {};
+}
+
+export enum Anonymity {
+ Disabled,
+ Anonymous,
+ Pseudonymous
+}
+
+// If an event extends IPseudonymousEvent, the event contains pseudonymous data
+// that won't be sent unless the user has explicitly consented to pseudonymous tracking.
+// For example, it might contain hashed user IDs or room IDs.
+// Such events will be automatically dropped if PosthogAnalytics.anonymity isn't set to Pseudonymous.
+export interface IPseudonymousEvent extends IEvent {}
+
+// If an event extends IAnonymousEvent, the event strictly contains *only* anonymous data;
+// i.e. no identifiers that can be associated with the user.
+export interface IAnonymousEvent extends IEvent {}
+
+export interface IRoomEvent extends IPseudonymousEvent {
+ hashedRoomId: string;
+}
+
+interface IPageView extends IAnonymousEvent {
+ eventName: "$pageview";
+ properties: {
+ durationMs?: number;
+ screen?: string;
+ };
+}
+
+const hashHex = async (input: string): Promise => {
+ const buf = new TextEncoder().encode(input);
+ const digestBuf = await window.crypto.subtle.digest("sha-256", buf);
+ return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join("");
+};
+
+const whitelistedScreens = new Set([
+ "register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
+ "start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
+]);
+
+export async function getRedactedCurrentLocation(
+ origin: string,
+ hash: string,
+ pathname: string,
+ anonymity: Anonymity,
+): Promise {
+ // Redact PII from the current location.
+ // If anonymous is true, redact entirely, if false, substitute it with a hash.
+ // For known screens, assumes a URL structure of //might/be/pii
+ if (origin.startsWith('file://')) {
+ pathname = "//";
+ }
+
+ let hashStr;
+ if (hash == "") {
+ hashStr = "";
+ } else {
+ let [beforeFirstSlash, screen, ...parts] = hash.split("/");
+
+ if (!whitelistedScreens.has(screen)) {
+ screen = "";
+ }
+
+ for (let i = 0; i < parts.length; i++) {
+ parts[i] = anonymity === Anonymity.Anonymous ? `` : await hashHex(parts[i]);
+ }
+
+ hashStr = `${beforeFirstSlash}/${screen}/${parts.join("/")}`;
+ }
+ return origin + pathname + hashStr;
+}
+
+interface PlatformProperties {
+ appVersion: string;
+ appPlatform: string;
+}
+
+export class PosthogAnalytics {
+ /* Wrapper for Posthog analytics.
+ * 3 modes of anonymity are supported, governed by this.anonymity
+ * - Anonymity.Disabled means *no data* is passed to posthog
+ * - Anonymity.Anonymous means all identifers will be redacted before being passed to posthog
+ * - Anonymity.Pseudonymous means all identifiers will be hashed via SHA-256 before being passed
+ * to Posthog
+ *
+ * To update anonymity, call updateAnonymityFromSettings() or you can set it directly via setAnonymity().
+ *
+ * To pass an event to Posthog:
+ *
+ * 1. Declare a type for the event, extending IAnonymousEvent, IPseudonymousEvent or IRoomEvent.
+ * 2. Call the appropriate track*() method. Pseudonymous events will be dropped when anonymity is
+ * Anonymous or Disabled; Anonymous events will be dropped when anonymity is Disabled.
+ */
+
+ private anonymity = Anonymity.Disabled;
+ // set true during the constructor if posthog config is present, otherwise false
+ private enabled = false;
+ private static _instance = null;
+ private platformSuperProperties = {};
+
+ public static get instance(): PosthogAnalytics {
+ if (!this._instance) {
+ this._instance = new PosthogAnalytics(posthog);
+ }
+ return this._instance;
+ }
+
+ constructor(private readonly posthog: PostHog) {
+ const posthogConfig = SdkConfig.get()["posthog"];
+ if (posthogConfig) {
+ this.posthog.init(posthogConfig.projectApiKey, {
+ api_host: posthogConfig.apiHost,
+ autocapture: false,
+ mask_all_text: true,
+ mask_all_element_attributes: true,
+ // This only triggers on page load, which for our SPA isn't particularly useful.
+ // Plus, the .capture call originating from somewhere in posthog makes it hard
+ // to redact URLs, which requires async code.
+ //
+ // To raise this manually, just call .capture("$pageview") or posthog.capture_pageview.
+ capture_pageview: false,
+ sanitize_properties: this.sanitizeProperties,
+ respect_dnt: true,
+ });
+ this.enabled = true;
+ } else {
+ this.enabled = false;
+ }
+ }
+
+ private sanitizeProperties = (properties: posthog.Properties): posthog.Properties => {
+ // Callback from posthog to sanitize properties before sending them to the server.
+ //
+ // Here we sanitize posthog's built in properties which leak PII e.g. url reporting.
+ // See utils.js _.info.properties in posthog-js.
+
+ // Replace the $current_url with a redacted version.
+ // $redacted_current_url is injected by this class earlier in capture(), as its generation
+ // is async and can't be done in this non-async callback.
+ if (!properties['$redacted_current_url']) {
+ console.log("$redacted_current_url not set in sanitizeProperties, will drop $current_url entirely");
+ }
+ properties['$current_url'] = properties['$redacted_current_url'];
+ delete properties['$redacted_current_url'];
+
+ if (this.anonymity == Anonymity.Anonymous) {
+ // drop referrer information for anonymous users
+ properties['$referrer'] = null;
+ properties['$referring_domain'] = null;
+ properties['$initial_referrer'] = null;
+ properties['$initial_referring_domain'] = null;
+
+ // drop device ID, which is a UUID persisted in local storage
+ properties['$device_id'] = null;
+ }
+
+ return properties;
+ };
+
+ private static getAnonymityFromSettings(): Anonymity {
+ // determine the current anonymity level based on current user settings
+
+ // "Send anonymous usage data which helps us improve Element. This will use a cookie."
+ const analyticsOptIn = SettingsStore.getValue("analyticsOptIn", null, true);
+
+ // (proposed wording) "Send pseudonymous usage data which helps us improve Element. This will use a cookie."
+ //
+ // TODO: Currently, this is only a labs flag, for testing purposes.
+ const pseudonumousOptIn = SettingsStore.getValue("feature_pseudonymous_analytics_opt_in", null, true);
+
+ let anonymity;
+ if (pseudonumousOptIn) {
+ anonymity = Anonymity.Pseudonymous;
+ } else if (analyticsOptIn) {
+ anonymity = Anonymity.Anonymous;
+ } else {
+ anonymity = Anonymity.Disabled;
+ }
+
+ return anonymity;
+ }
+
+ private registerSuperProperties(properties: posthog.Properties) {
+ if (this.enabled) {
+ this.posthog.register(properties);
+ }
+ }
+
+ private static async getPlatformProperties(): Promise {
+ const platform = PlatformPeg.get();
+ let appVersion;
+ try {
+ appVersion = await platform.getAppVersion();
+ } catch (e) {
+ // this happens if no version is set i.e. in dev
+ appVersion = "unknown";
+ }
+
+ return {
+ appVersion,
+ appPlatform: platform.getHumanReadableName(),
+ };
+ }
+
+ private async capture(eventName: string, properties: posthog.Properties) {
+ if (!this.enabled) {
+ return;
+ }
+ const { origin, hash, pathname } = window.location;
+ properties['$redacted_current_url'] = await getRedactedCurrentLocation(
+ origin, hash, pathname, this.anonymity);
+ this.posthog.capture(eventName, properties);
+ }
+
+ public isEnabled(): boolean {
+ return this.enabled;
+ }
+
+ public setAnonymity(anonymity: Anonymity): void {
+ // Update this.anonymity.
+ // This is public for testing purposes, typically you want to call updateAnonymityFromSettings
+ // to ensure this value is in step with the user's settings.
+ if (this.enabled && (anonymity == Anonymity.Disabled || anonymity == Anonymity.Anonymous)) {
+ // when transitioning to Disabled or Anonymous ensure we clear out any prior state
+ // set in posthog e.g. distinct ID
+ this.posthog.reset();
+ // Restore any previously set platform super properties
+ this.registerSuperProperties(this.platformSuperProperties);
+ }
+ this.anonymity = anonymity;
+ }
+
+ public async identifyUser(userId: string): Promise {
+ if (this.anonymity == Anonymity.Pseudonymous) {
+ this.posthog.identify(await hashHex(userId));
+ }
+ }
+
+ public getAnonymity(): Anonymity {
+ return this.anonymity;
+ }
+
+ public logout(): void {
+ if (this.enabled) {
+ this.posthog.reset();
+ }
+ this.setAnonymity(Anonymity.Anonymous);
+ }
+
+ public async trackPseudonymousEvent(
+ eventName: E["eventName"],
+ properties: E["properties"] = {},
+ ) {
+ if (this.anonymity == Anonymity.Anonymous || this.anonymity == Anonymity.Disabled) return;
+ await this.capture(eventName, properties);
+ }
+
+ public async trackAnonymousEvent(
+ eventName: E["eventName"],
+ properties: E["properties"] = {},
+ ): Promise {
+ if (this.anonymity == Anonymity.Disabled) return;
+ await this.capture(eventName, properties);
+ }
+
+ public async trackRoomEvent(
+ eventName: E["eventName"],
+ roomId: string,
+ properties: Omit,
+ ): Promise {
+ const updatedProperties = {
+ ...properties,
+ hashedRoomId: roomId ? await hashHex(roomId) : null,
+ };
+ await this.trackPseudonymousEvent(eventName, updatedProperties);
+ }
+
+ public async trackPageView(durationMs: number): Promise {
+ const hash = window.location.hash;
+
+ let screen = null;
+ const split = hash.split("/");
+ if (split.length >= 2) {
+ screen = split[1];
+ }
+
+ await this.trackAnonymousEvent("$pageview", {
+ durationMs,
+ screen,
+ });
+ }
+
+ public async updatePlatformSuperProperties(): Promise {
+ // Update super properties in posthog with our platform (app version, platform).
+ // These properties will be subsequently passed in every event.
+ //
+ // This only needs to be done once per page lifetime. Note that getPlatformProperties
+ // is async and can involve a network request if we are running in a browser.
+ this.platformSuperProperties = await PosthogAnalytics.getPlatformProperties();
+ this.registerSuperProperties(this.platformSuperProperties);
+ }
+
+ public async updateAnonymityFromSettings(userId?: string): Promise {
+ // Update this.anonymity based on the user's analytics opt-in settings
+ // Identify the user (via hashed user ID) to posthog if anonymity is pseudonmyous
+ this.setAnonymity(PosthogAnalytics.getAnonymityFromSettings());
+ if (userId && this.getAnonymity() == Anonymity.Pseudonymous) {
+ await this.identifyUser(userId);
+ }
+ }
+}
diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index 7bad8eb50e..b9295be3ed 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -25,11 +25,44 @@ import { Action } from './dispatcher/actions';
import defaultDispatcher from './dispatcher/dispatcher';
import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { MatrixClientPeg } from "./MatrixClientPeg";
// These functions are frequently used just to check whether an event has
// any text to display at all. For this reason they return deferred values
// to avoid the expense of looking up translations when they're not needed.
+function textForCallInviteEvent(event: MatrixEvent): () => string | null {
+ const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
+ // FIXME: Find a better way to determine this from the event?
+ let isVoice = true;
+ if (event.getContent().offer && event.getContent().offer.sdp &&
+ event.getContent().offer.sdp.indexOf('m=video') !== -1) {
+ isVoice = false;
+ }
+ const isSupported = MatrixClientPeg.get().supportsVoip();
+
+ // This ladder could be reduced down to a couple string variables, however other languages
+ // can have a hard time translating those strings. In an effort to make translations easier
+ // and more accurate, we break out the string-based variables to a couple booleans.
+ if (isVoice && isSupported) {
+ return () => _t("%(senderName)s placed a voice call.", {
+ senderName: getSenderName(),
+ });
+ } else if (isVoice && !isSupported) {
+ return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
+ senderName: getSenderName(),
+ });
+ } else if (!isVoice && isSupported) {
+ return () => _t("%(senderName)s placed a video call.", {
+ senderName: getSenderName(),
+ });
+ } else if (!isVoice && !isSupported) {
+ return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
+ senderName: getSenderName(),
+ });
+ }
+}
+
function textForMemberEvent(ev: MatrixEvent, allowJSX: boolean, showHiddenEvents?: boolean): () => string | null {
// XXX: SYJS-16 "sender is sometimes null for join messages"
const senderName = ev.sender ? ev.sender.name : ev.getSender();
@@ -567,6 +600,7 @@ interface IHandlers {
const handlers: IHandlers = {
'm.room.message': textForMessageEvent,
+ 'm.call.invite': textForCallInviteEvent,
};
const stateHandlers: IHandlers = {
diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx
index 9cc7b60c99..c66984191f 100644
--- a/src/accessibility/KeyboardShortcuts.tsx
+++ b/src/accessibility/KeyboardShortcuts.tsx
@@ -163,7 +163,7 @@ const shortcuts: Record = {
modifiers: [Modifiers.SHIFT],
key: Key.PAGE_UP,
}],
- description: _td("Jump to oldest unread message"),
+ description: _td("Jump to oldest unread message"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL, Modifiers.SHIFT],
diff --git a/src/audio/Playback.ts b/src/audio/Playback.ts
index 33d346629a..9dad828a79 100644
--- a/src/audio/Playback.ts
+++ b/src/audio/Playback.ts
@@ -38,17 +38,9 @@ function makePlaybackWaveform(input: number[]): number[] {
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
const noiseWaveform = input.map(v => Math.abs(v));
- // Next, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
- // We also rescale the waveform to be 0-1 for the remaining function logic.
- const resampled = arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
-
- // Then, we'll do a high and low pass filter to isolate actual speaking volumes within the rescaled
- // waveform. Most speech happens below the 0.5 mark.
- const filtered = resampled.map(v => clamp(v, 0.1, 0.5));
-
- // Finally, we'll rescale the filtered waveform (0.1-0.5 becomes 0-1 again) so the user sees something
- // sensible. This is what we return to keep our contract of "values between zero and one".
- return arrayRescale(filtered, 0, 1);
+ // Then, we'll resample the waveform using a smoothing approach so we can keep the same rough shape.
+ // We also rescale the waveform to be 0-1 so we end up with a clamped waveform to rely upon.
+ return arrayRescale(arraySmoothingResample(noiseWaveform, PLAYBACK_WAVEFORM_SAMPLES), 0, 1);
}
export class Playback extends EventEmitter implements IDestroyable {
diff --git a/src/audio/VoiceRecording.ts b/src/audio/VoiceRecording.ts
index efd616e5ae..67b2acda0c 100644
--- a/src/audio/VoiceRecording.ts
+++ b/src/audio/VoiceRecording.ts
@@ -30,6 +30,7 @@ import { IEncryptedFile } from "matrix-js-sdk/src/@types/event";
import { uploadFile } from "../ContentMessages";
import { FixedRollingArray } from "../utils/FixedRollingArray";
import { clamp } from "../utils/numbers";
+import mxRecorderWorkletPath from "./RecorderWorklet";
const CHANNELS = 1; // stereo isn't important
export const SAMPLE_RATE = 48000; // 48khz is what WebRTC uses. 12khz is where we lose quality.
@@ -113,16 +114,10 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
});
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
- // Set up our worklet. We use this for timing information and waveform analysis: the
- // web audio API prefers this be done async to avoid holding the main thread with math.
- const mxRecorderWorkletPath = document.body.dataset.vectorRecorderWorkletScript;
- if (!mxRecorderWorkletPath) {
- // noinspection ExceptionCaughtLocallyJS
- throw new Error("Unable to create recorder: no worklet script registered");
- }
-
// Connect our inputs and outputs
if (this.recorderContext.audioWorklet) {
+ // Set up our worklet. We use this for timing information and waveform analysis: the
+ // web audio API prefers this be done async to avoid holding the main thread with math.
await this.recorderContext.audioWorklet.addModule(mxRecorderWorkletPath);
this.recorderWorklet = new AudioWorkletNode(this.recorderContext, WORKLET_NAME);
this.recorderSource.connect(this.recorderWorklet);
diff --git a/src/components/structures/ContextMenu.tsx b/src/components/structures/ContextMenu.tsx
index 407dc6f04c..332b6cd318 100644
--- a/src/components/structures/ContextMenu.tsx
+++ b/src/components/structures/ContextMenu.tsx
@@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import React, { CSSProperties, RefObject, useRef, useState } from "react";
+import React, { CSSProperties, RefObject, SyntheticEvent, useRef, useState } from "react";
import ReactDOM from "react-dom";
import classNames from "classnames";
@@ -80,6 +80,10 @@ export interface IProps extends IPosition {
managed?: boolean;
wrapperClassName?: string;
+ // If true, this context menu will be mounted as a child to the parent container. Otherwise
+ // it will be mounted to a container at the root of the DOM.
+ mountAsChild?: boolean;
+
// Function to be called on menu close
onFinished();
// on resize callback
@@ -390,7 +394,13 @@ export class ContextMenu extends React.PureComponent {
}
render(): React.ReactChild {
- return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
+ if (this.props.mountAsChild) {
+ // Render as a child of the current parent
+ return this.renderMenu();
+ } else {
+ // Render as a child of a container at the root of the DOM
+ return ReactDOM.createPortal(this.renderMenu(), getOrCreateContainer());
+ }
}
}
@@ -461,10 +471,14 @@ type ContextMenuTuple = [boolean, RefObject, () => void, () => void, (val:
export const useContextMenu = (): ContextMenuTuple => {
const button = useRef(null);
const [isOpen, setIsOpen] = useState(false);
- const open = () => {
+ const open = (ev?: SyntheticEvent) => {
+ ev?.preventDefault();
+ ev?.stopPropagation();
setIsOpen(true);
};
- const close = () => {
+ const close = (ev?: SyntheticEvent) => {
+ ev?.preventDefault();
+ ev?.stopPropagation();
setIsOpen(false);
};
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 8cfe35c4cf..60c78b5f9e 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -107,6 +107,7 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore";
import SoftLogout from './auth/SoftLogout';
import { makeRoomPermalink } from "../../utils/permalinks/Permalinks";
import { copyPlaintext } from "../../utils/strings";
+import { PosthogAnalytics } from '../../PosthogAnalytics';
/** constants for MatrixChat.state.view */
export enum Views {
@@ -387,6 +388,10 @@ export default class MatrixChat extends React.PureComponent {
if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable();
}
+
+ PosthogAnalytics.instance.updateAnonymityFromSettings();
+ PosthogAnalytics.instance.updatePlatformSuperProperties();
+
CountlyAnalytics.instance.enable(/* anonymous = */ true);
}
@@ -443,6 +448,7 @@ export default class MatrixChat extends React.PureComponent {
const durationMs = this.stopPageChangeTimer();
Analytics.trackPageChange(durationMs);
CountlyAnalytics.instance.trackPageChange(durationMs);
+ PosthogAnalytics.instance.trackPageView(durationMs);
}
if (this.focusComposer) {
dis.fire(Action.FocusSendMessageComposer);
diff --git a/src/components/structures/SpaceRoomDirectory.tsx b/src/components/structures/SpaceRoomDirectory.tsx
index 038c1df514..d8cc9593f0 100644
--- a/src/components/structures/SpaceRoomDirectory.tsx
+++ b/src/components/structures/SpaceRoomDirectory.tsx
@@ -16,7 +16,6 @@ limitations under the License.
import React, { ReactNode, useMemo, useState } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
-import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import { ISpaceSummaryRoom, ISpaceSummaryEvent } from "matrix-js-sdk/src/@types/spaces";
import classNames from "classnames";
@@ -44,11 +43,13 @@ import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { linkifyElement } from "../../HtmlUtils";
import { getDisplayAliasForAliasSet } from "../../Rooms";
+import { useDispatcher } from "../../hooks/useDispatcher";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+import { Action } from "../../dispatcher/actions";
interface IHierarchyProps {
space: Room;
initialText?: string;
- refreshToken?: any;
additionalButtons?: ReactNode;
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
}
@@ -315,18 +316,25 @@ export const HierarchyLevel = ({
;
};
-// mutate argument refreshToken to force a reload
-export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
+export const useSpaceSummary = (space: Room): [
null,
ISpaceSummaryRoom[],
Map>?,
Map>?,
Map>?,
] | [Error] => {
+ // crude temporary refresh token approach until we have pagination and rework the data flow here
+ const [refreshToken, setRefreshToken] = useState(0);
+ useDispatcher(defaultDispatcher, (payload => {
+ if (payload.action === Action.UpdateSpaceHierarchy) {
+ setRefreshToken(t => t + 1);
+ }
+ }));
+
// TODO pagination
return useAsyncMemo(async () => {
try {
- const data = await cli.getSpaceSummary(space.roomId);
+ const data = await space.client.getSpaceSummary(space.roomId);
const parentChildRelations = new EnhancedMap>();
const childParentRelations = new EnhancedMap>();
@@ -354,7 +362,6 @@ export const SpaceHierarchy: React.FC = ({
space,
initialText = "",
showRoom,
- refreshToken,
additionalButtons,
children,
}) => {
@@ -364,7 +371,7 @@ export const SpaceHierarchy: React.FC = ({
const [selected, setSelected] = useState(new Map>()); // Map>
- const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
+ const [summaryError, rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(space);
const roomsMap = useMemo(() => {
if (!rooms) return null;
diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx
index a077fddadf..6f63ea090c 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -47,13 +47,23 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload";
import { useStateArray } from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare";
-import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space";
+import {
+ shouldShowSpaceSettings,
+ showAddExistingRooms,
+ showCreateNewRoom,
+ showCreateNewSubspace,
+ showSpaceSettings,
+} from "../../utils/space";
import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory";
import MemberAvatar from "../views/avatars/MemberAvatar";
-import { useStateToggle } from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile";
-import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog";
+import {
+ AddExistingToSpace,
+ defaultDmsRenderer,
+ defaultRoomsRenderer,
+ defaultSpacesRenderer,
+} from "../views/dialogs/AddExistingToSpaceDialog";
import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOption,
@@ -62,10 +72,8 @@ import IconizedContextMenu, {
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { BetaPill } from "../views/beta/BetaCard";
import { UserTab } from "../views/dialogs/UserSettingsDialog";
-import Modal from "../../Modal";
-import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
-import SdkConfig from "../../SdkConfig";
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
+import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
interface IProps {
space: Room;
@@ -92,28 +100,6 @@ enum Phase {
PrivateExistingRooms,
}
-// XXX: Temporary for the Spaces Beta only
-export const SpaceFeedbackPrompt = ({ onClick }: { onClick?: () => void }) => {
- if (!SdkConfig.get().bug_report_endpoint_url) return null;
-
- return
);
+ );
containerClasses = classNames("mx_ToastContainer", {
"mx_ToastContainer_stacked": isStacked,
diff --git a/src/components/views/audio_messages/LiveRecordingWaveform.tsx b/src/components/views/audio_messages/LiveRecordingWaveform.tsx
index 9c33889884..73e18626fe 100644
--- a/src/components/views/audio_messages/LiveRecordingWaveform.tsx
+++ b/src/components/views/audio_messages/LiveRecordingWaveform.tsx
@@ -17,8 +17,7 @@ limitations under the License.
import React from "react";
import { IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording } from "../../../audio/VoiceRecording";
import { replaceableComponent } from "../../../utils/replaceableComponent";
-import { arrayFastResample } from "../../../utils/arrays";
-import { percentageOf } from "../../../utils/numbers";
+import { arrayFastResample, arraySeed } from "../../../utils/arrays";
import Waveform from "./Waveform";
import { MarkedExecution } from "../../../utils/MarkedExecution";
@@ -48,18 +47,14 @@ export default class LiveRecordingWaveform extends React.PureComponent {
- const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
- // The incoming data is between zero and one, but typically even screaming into a
- // microphone won't send you over 0.6, so we artificially adjust the gain for the
- // waveform. This results in a slightly more cinematic/animated waveform for the
- // user.
- this.waveform = bars.map(b => percentageOf(b, 0, 0.50));
+ // The incoming data is between zero and one, so we don't need to clamp/rescale it.
+ this.waveform = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
this.scheduledUpdate.mark();
});
}
diff --git a/src/components/views/auth/CaptchaForm.tsx b/src/components/views/auth/CaptchaForm.tsx
index b1c09f2b22..97f45167a8 100644
--- a/src/components/views/auth/CaptchaForm.tsx
+++ b/src/components/views/auth/CaptchaForm.tsx
@@ -103,8 +103,8 @@ export default class CaptchaForm extends React.Component
this.props.onFinished();
};
+ onKeyDown = (ev) => {
+ // Prevent Backspace and Delete keys from functioning in the entry field
+ if (ev.code === "Backspace" || ev.code === "Delete") {
+ ev.preventDefault();
+ }
+ };
+
onChange = (ev) => {
this.setState({ value: ev.target.value });
};
@@ -64,6 +71,7 @@ export default class DialpadContextMenu extends React.Component
className="mx_DialPadContextMenu_dialled"
value={this.state.value}
autoFocus={true}
+ onKeyDown={this.onKeyDown}
onChange={this.onChange}
/>
diff --git a/src/components/views/context_menus/IconizedContextMenu.tsx b/src/components/views/context_menus/IconizedContextMenu.tsx
index 1d822fd246..571b0b39bf 100644
--- a/src/components/views/context_menus/IconizedContextMenu.tsx
+++ b/src/components/views/context_menus/IconizedContextMenu.tsx
@@ -86,14 +86,18 @@ export const IconizedContextMenuCheckbox: React.FC = ({
>
{ label }
- { active && }
+
;
};
-export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, ...props }) => {
+export const IconizedContextMenuOption: React.FC = ({ label, iconClassName, children, ...props }) => {
return ;
};
diff --git a/src/components/views/context_menus/SpaceContextMenu.tsx b/src/components/views/context_menus/SpaceContextMenu.tsx
new file mode 100644
index 0000000000..3da00e71aa
--- /dev/null
+++ b/src/components/views/context_menus/SpaceContextMenu.tsx
@@ -0,0 +1,216 @@
+/*
+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 React, { useContext } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+import { EventType } from "matrix-js-sdk/src/@types/event";
+
+import {
+ IProps as IContextMenuProps,
+} from "../../structures/ContextMenu";
+import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu";
+import { _t } from "../../../languageHandler";
+import {
+ leaveSpace,
+ shouldShowSpaceSettings,
+ showAddExistingRooms,
+ showCreateNewRoom,
+ showCreateNewSubspace,
+ showSpaceInvite,
+ showSpaceSettings,
+} from "../../../utils/space";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import { ButtonEvent } from "../elements/AccessibleButton";
+import defaultDispatcher from "../../../dispatcher/dispatcher";
+import RoomViewStore from "../../../stores/RoomViewStore";
+import { SetRightPanelPhasePayload } from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
+import { Action } from "../../../dispatcher/actions";
+import { RightPanelPhases } from "../../../stores/RightPanelStorePhases";
+import { BetaPill } from "../beta/BetaCard";
+
+interface IProps extends IContextMenuProps {
+ space: Room;
+}
+
+const SpaceContextMenu = ({ space, onFinished, ...props }: IProps) => {
+ const cli = useContext(MatrixClientContext);
+ const userId = cli.getUserId();
+
+ let inviteOption;
+ if (space.getJoinRule() === "public" || space.canInvite(userId)) {
+ const onInviteClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ showSpaceInvite(space);
+ onFinished();
+ };
+
+ inviteOption = (
+
+ );
+ }
+
+ let settingsOption;
+ let leaveSection;
+ if (shouldShowSpaceSettings(space)) {
+ const onSettingsClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ showSpaceSettings(space);
+ onFinished();
+ };
+
+ settingsOption = (
+
+ );
+ } else {
+ const onLeaveClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ leaveSpace(space);
+ onFinished();
+ };
+
+ leaveSection =
+
+ ;
+ }
+
+ const canAddRooms = space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
+
+ let newRoomSection;
+ if (space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
+ const onNewRoomClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ showCreateNewRoom(space);
+ onFinished();
+ };
+
+ const onAddExistingRoomClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ showAddExistingRooms(space);
+ onFinished();
+ };
+
+ const onNewSubspaceClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ showCreateNewSubspace(space);
+ onFinished();
+ };
+
+ newRoomSection =
+
+
+
+
+
+ ;
+ }
+
+ const onMembersClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ if (!RoomViewStore.getRoomId()) {
+ defaultDispatcher.dispatch({
+ action: "view_room",
+ room_id: space.roomId,
+ }, true);
+ }
+
+ defaultDispatcher.dispatch({
+ action: Action.SetRightPanelPhase,
+ phase: RightPanelPhases.SpaceMemberList,
+ refireParams: { space: space },
+ });
+ onFinished();
+ };
+
+ const onExploreRoomsClick = (ev: ButtonEvent) => {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ defaultDispatcher.dispatch({
+ action: "view_room",
+ room_id: space.roomId,
+ });
+ onFinished();
+ };
+
+ return
+
+ { space.name }
+
+
+ { inviteOption }
+
+ { settingsOption }
+
+
+ { newRoomSection }
+ { leaveSection }
+ ;
+};
+
+export default SpaceContextMenu;
+
diff --git a/src/components/views/dialogs/AddExistingSubspaceDialog.tsx b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx
new file mode 100644
index 0000000000..7fef2c2d9d
--- /dev/null
+++ b/src/components/views/dialogs/AddExistingSubspaceDialog.tsx
@@ -0,0 +1,67 @@
+/*
+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 React, { useState } from "react";
+import { Room } from "matrix-js-sdk/src/models/room";
+
+import { _t } from '../../../languageHandler';
+import BaseDialog from "./BaseDialog";
+import AccessibleButton from "../elements/AccessibleButton";
+import MatrixClientContext from "../../../contexts/MatrixClientContext";
+import { AddExistingToSpace, defaultSpacesRenderer, SubspaceSelector } from "./AddExistingToSpaceDialog";
+
+interface IProps {
+ space: Room;
+ onCreateSubspaceClick(): void;
+ onFinished(added?: boolean): void;
+}
+
+const AddExistingSubspaceDialog: React.FC = ({ space, onCreateSubspaceClick, onFinished }) => {
+ const [selectedSpace, setSelectedSpace] = useState(space);
+
+ return
+ )}
+ className="mx_AddExistingToSpaceDialog"
+ contentId="mx_AddExistingToSpace"
+ onFinished={onFinished}
+ fixedWidth={false}
+ >
+
+
+
{ _t("Want to add a new space instead?") }
+
+ { _t("Create a new space") }
+
+ >}
+ filterPlaceholder={_t("Search for spaces")}
+ spacesRenderer={defaultSpacesRenderer}
+ />
+
+ ;
+};
+
+export default AddExistingSubspaceDialog;
+
diff --git a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
index 3ef86e438d..cf4f369d09 100644
--- a/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
+++ b/src/components/views/dialogs/AddExistingToSpaceDialog.tsx
@@ -18,9 +18,9 @@ import React, { ReactNode, useContext, useMemo, useState } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import { sleep } from "matrix-js-sdk/src/utils";
+import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t } from '../../../languageHandler';
-import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox";
@@ -35,19 +35,20 @@ import StyledCheckbox from "../elements/StyledCheckbox";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import ProgressBar from "../elements/ProgressBar";
-import { SpaceFeedbackPrompt } from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
-interface IProps extends IDialogProps {
+interface IProps {
space: Room;
- onCreateRoomClick(space: Room): void;
+ onCreateRoomClick(): void;
+ onAddSubspaceClick(): void;
+ onFinished(added?: boolean): void;
}
-const Entry = ({ room, checked, onChange }) => {
+export const Entry = ({ room, checked, onChange }) => {
return
{ _t(
"Anyone will be able to find and join this room, not just members of .", {}, {
@@ -260,6 +260,12 @@ export default class CreateRoomDialog extends React.Component {
{ _t("You can change this at any time from room settings.") }
;
+};
+
+interface IProps {
+ space: Room;
+ onFinished(leave: boolean, rooms?: Room[]): void;
+}
+
+const isOnlyAdmin = (room: Room): boolean => {
+ return !room.getJoinedMembers().some(member => {
+ return member.userId !== room.client.credentials.userId && member.powerLevelNorm === 100;
+ });
+};
+
+const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => {
+ const spaceChildren = useMemo(() => SpaceStore.instance.getChildren(space.roomId), [space.roomId]);
+ const [roomsToLeave, setRoomsToLeave] = useState([]);
+
+ let rejoinWarning;
+ if (space.getJoinRule() !== JoinRule.Public) {
+ rejoinWarning = _t("You won't be able to rejoin unless you are re-invited.");
+ }
+
+ let onlyAdminWarning;
+ if (isOnlyAdmin(space)) {
+ onlyAdminWarning = _t("You're the only admin of this space. " +
+ "Leaving it will mean no one has control over it.");
+ } else {
+ const numChildrenOnlyAdminIn = roomsToLeave.filter(isOnlyAdmin).length;
+ if (numChildrenOnlyAdminIn > 0) {
+ onlyAdminWarning = _t("You're the only admin of some of the rooms or spaces you wish to leave. " +
+ "Leaving them will leave them without any admins.");
+ }
+ }
+
+ return onFinished(false)}
+ fixedWidth={false}
+ >
+
+
+ { _t("Are you sure you want to leave ?", {}, {
+ spaceName: () => { space.name },
+ }) }
+
+ { rejoinWarning }
+
+ onFinished(true, roomsToLeave)}
+ hasCancel={true}
+ onCancel={onFinished}
+ />
+ ;
+};
+
+export default LeaveSpaceDialog;
diff --git a/src/components/views/dialogs/LogoutDialog.js b/src/components/views/dialogs/LogoutDialog.tsx
similarity index 82%
rename from src/components/views/dialogs/LogoutDialog.js
rename to src/components/views/dialogs/LogoutDialog.tsx
index 469cd48093..8c035dcbba 100644
--- a/src/components/views/dialogs/LogoutDialog.js
+++ b/src/components/views/dialogs/LogoutDialog.tsx
@@ -16,6 +16,7 @@ limitations under the License.
*/
import React from 'react';
+import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher';
@@ -24,19 +25,25 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import RestoreKeyBackupDialog from './security/RestoreKeyBackupDialog';
import { replaceableComponent } from "../../../utils/replaceableComponent";
+interface IProps {
+ onFinished: (success: boolean) => void;
+}
+
+interface IState {
+ shouldLoadBackupStatus: boolean;
+ loading: boolean;
+ backupInfo: IKeyBackupInfo;
+ error?: string;
+}
+
@replaceableComponent("views.dialogs.LogoutDialog")
-export default class LogoutDialog extends React.Component {
- defaultProps = {
+export default class LogoutDialog extends React.Component {
+ static defaultProps = {
onFinished: function() {},
};
- constructor() {
- super();
- this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this);
- this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this);
- this._onFinished = this._onFinished.bind(this);
- this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
- this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
+ constructor(props) {
+ super(props);
const cli = MatrixClientPeg.get();
const shouldLoadBackupStatus = cli.isCryptoEnabled() && !cli.getKeyBackupEnabled();
@@ -49,11 +56,11 @@ export default class LogoutDialog extends React.Component {
};
if (shouldLoadBackupStatus) {
- this._loadBackupStatus();
+ this.loadBackupStatus();
}
}
- async _loadBackupStatus() {
+ private async loadBackupStatus() {
try {
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
this.setState({
@@ -69,29 +76,29 @@ export default class LogoutDialog extends React.Component {
}
}
- _onSettingsLinkClick() {
+ private onSettingsLinkClick = (): void => {
// close dialog
- this.props.onFinished();
- }
+ this.props.onFinished(true);
+ };
- _onExportE2eKeysClicked() {
+ private onExportE2eKeysClicked = (): void => {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/security/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
);
- }
+ };
- _onFinished(confirmed) {
+ private onFinished = (confirmed: boolean): void => {
if (confirmed) {
dis.dispatch({ action: 'logout' });
}
// close dialog
- this.props.onFinished();
- }
+ this.props.onFinished(confirmed);
+ };
- _onSetRecoveryMethodClick() {
+ private onSetRecoveryMethodClick = (): void => {
if (this.state.backupInfo) {
// A key backup exists for this account, but the creating device is not
// verified, so restore the backup which will give us the keys from it and
@@ -108,15 +115,15 @@ export default class LogoutDialog extends React.Component {
}
// close dialog
- this.props.onFinished();
- }
+ this.props.onFinished(true);
+ };
- _onLogoutConfirm() {
+ private onLogoutConfirm = (): void => {
dis.dispatch({ action: 'logout' });
// close dialog
- this.props.onFinished();
- }
+ this.props.onFinished(true);
+ };
render() {
if (this.state.shouldLoadBackupStatus) {
@@ -152,16 +159,16 @@ export default class LogoutDialog extends React.Component {
- { _t("Advanced") }
-
+
{ _t("Manually export keys") }
@@ -174,7 +181,7 @@ export default class LogoutDialog extends React.Component {
title={_t("You'll lose access to your encrypted messages")}
contentId='mx_Dialog_content'
hasCancel={true}
- onFinished={this._onFinished}
+ onFinished={this.onFinished}
>
{ dialogContent }
);
@@ -187,7 +194,7 @@ export default class LogoutDialog extends React.Component {
"Are you sure you want to sign out?",
)}
button={_t("Sign out")}
- onFinished={this._onFinished}
+ onFinished={this.onFinished}
/>);
}
}
diff --git a/src/components/views/elements/JoinRuleDropdown.tsx b/src/components/views/elements/JoinRuleDropdown.tsx
new file mode 100644
index 0000000000..e2d9b6d872
--- /dev/null
+++ b/src/components/views/elements/JoinRuleDropdown.tsx
@@ -0,0 +1,68 @@
+/*
+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 React from 'react';
+import { JoinRule } from 'matrix-js-sdk/src/@types/partials';
+
+import Dropdown from "./Dropdown";
+
+interface IProps {
+ value: JoinRule;
+ label: string;
+ width?: number;
+ labelInvite: string;
+ labelPublic: string;
+ labelRestricted?: string; // if omitted then this option will be hidden, e.g if unsupported
+ onChange(value: JoinRule): void;
+}
+
+const JoinRuleDropdown = ({
+ label,
+ labelInvite,
+ labelPublic,
+ labelRestricted,
+ value,
+ width = 448,
+ onChange,
+}: IProps) => {
+ const options = [
+
+ { labelInvite }
+
,
+
+ { labelPublic }
+
,
+ ];
+
+ if (labelRestricted) {
+ options.unshift(
}
);
}
- // When the iframe loads we tell it to render a download link
- const onIframeLoad = (ev) => {
- ev.target.contentWindow.postMessage({
- imgSrc: DOWNLOAD_ICON_URL,
- imgStyle: null, // it handles this internally for us. Useful if a downstream changes the icon.
- style: computedStyle(this.dummyLink.current),
- blob: this.state.decryptedBlob,
- // Set a download attribute for encrypted files so that the file
- // will have the correct name when the user tries to download it.
- // We can't provide a Content-Disposition header like we would for HTTP.
- download: fileName,
- textContent: _t("Download %(text)s", { text: text }),
- // only auto-download if a user triggered this iframe explicitly
- auto: this.userDidClick,
- }, "*");
- };
-
const url = "usercontent/"; // XXX: this path should probably be passed from the skin
// If the attachment is encrypted then put the link inside an iframe.
@@ -218,9 +248,16 @@ export default class MFileBody extends React.Component {
*/ }
+ { /*
+ TODO: Move iframe (and dummy link) into FileDownloader.
+ We currently have it set up this way because of styles applied to the iframe
+ itself which cannot be easily handled/overridden by the FileDownloader. In
+ future, the download link may disappear entirely at which point it could also
+ be suitable to just remove this bit of code.
+ */ }