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/_common.scss b/res/css/_common.scss
index b128a82442..6b4e109b3a 100644
--- a/res/css/_common.scss
+++ b/res/css/_common.scss
@@ -104,8 +104,8 @@ a:visited {
input[type=text],
input[type=search],
input[type=password] {
+ font-family: inherit;
padding: 9px;
- font-family: $font-family;
font-size: $font-14px;
font-weight: 600;
min-width: 0;
@@ -146,7 +146,6 @@ input[type=text], input[type=password], textarea {
/* Required by Firefox */
textarea {
- font-family: $font-family;
color: $primary-fg-color;
}
diff --git a/res/css/_components.scss b/res/css/_components.scss
index f9e3ab1160..af161c92c6 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";
@@ -237,6 +240,7 @@
@import "./views/settings/_E2eAdvancedPanel.scss";
@import "./views/settings/_EmailAddresses.scss";
@import "./views/settings/_IntegrationManager.scss";
+@import "./views/settings/_LayoutSwitcher.scss";
@import "./views/settings/_Notifications.scss";
@import "./views/settings/_PhoneNumbers.scss";
@import "./views/settings/_ProfileSettings.scss";
@@ -263,12 +267,15 @@
@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";
@import "./views/voip/_CallPreview.scss";
@import "./views/voip/_CallView.scss";
@import "./views/voip/_CallViewForRoom.scss";
+@import "./views/voip/_CallViewHeader.scss";
+@import "./views/voip/_CallViewSidebar.scss";
@import "./views/voip/_DialPad.scss";
@import "./views/voip/_DialPadContextMenu.scss";
@import "./views/voip/_DialPadModal.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 7925686bf1..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 {
@@ -190,7 +191,6 @@ limitations under the License.
position: relative;
padding: 8px 16px;
border-radius: 8px;
- min-height: 56px;
box-sizing: border-box;
display: grid;
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/auth/_InteractiveAuthEntryComponents.scss b/res/css/views/auth/_InteractiveAuthEntryComponents.scss
index ffaad3cd7a..ec07b765fd 100644
--- a/res/css/views/auth/_InteractiveAuthEntryComponents.scss
+++ b/res/css/views/auth/_InteractiveAuthEntryComponents.scss
@@ -85,7 +85,7 @@ limitations under the License.
.mx_InteractiveAuthEntryComponents_termsPolicy {
display: flex;
flex-direction: row;
- justify-content: start;
+ justify-content: flex-start;
align-items: center;
}
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/_AddressPickerDialog.scss b/res/css/views/dialogs/_AddressPickerDialog.scss
index 136e497994..a1147e6fbc 100644
--- a/res/css/views/dialogs/_AddressPickerDialog.scss
+++ b/res/css/views/dialogs/_AddressPickerDialog.scss
@@ -29,7 +29,6 @@ limitations under the License.
.mx_AddressPickerDialog_input:focus {
height: 26px;
font-size: $font-14px;
- font-family: $font-family;
padding-left: 12px;
padding-right: 12px;
margin: 0 !important;
diff --git a/res/css/views/dialogs/_ConfirmUserActionDialog.scss b/res/css/views/dialogs/_ConfirmUserActionDialog.scss
index 823f4d1e28..284c171f4e 100644
--- a/res/css/views/dialogs/_ConfirmUserActionDialog.scss
+++ b/res/css/views/dialogs/_ConfirmUserActionDialog.scss
@@ -34,7 +34,6 @@ limitations under the License.
}
.mx_ConfirmUserActionDialog_reasonField {
- font-family: $font-family;
font-size: $font-14px;
color: $primary-fg-color;
background-color: $primary-bg-color;
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/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss
index 8fee740016..4d35e8d569 100644
--- a/res/css/views/dialogs/_DevtoolsDialog.scss
+++ b/res/css/views/dialogs/_DevtoolsDialog.scss
@@ -55,22 +55,6 @@ limitations under the License.
padding-right: 24px;
}
-.mx_DevTools_inputCell {
- display: table-cell;
- width: 240px;
-}
-
-.mx_DevTools_inputCell input {
- display: inline-block;
- border: 0;
- border-bottom: 1px solid $input-underline-color;
- padding: 0;
- width: 240px;
- color: $input-fg-color;
- font-family: $font-family;
- font-size: $font-16px;
-}
-
.mx_DevTools_textarea {
font-size: $font-12px;
max-width: 684px;
@@ -139,7 +123,6 @@ limitations under the License.
+ .mx_DevTools_tgl-btn {
padding: 2px;
transition: all .2s ease;
- font-family: sans-serif;
perspective: 100px;
&::after,
&::before {
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 69dde5925e..b4a2c69b86 100644
--- a/res/css/views/elements/_DesktopCapturerSourcePicker.scss
+++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss
@@ -16,57 +16,41 @@ limitations under the License.
.mx_desktopCapturerSourcePicker {
overflow: hidden;
-}
-.mx_desktopCapturerSourcePicker_tabLabels {
- display: flex;
- padding: 0 0 8px 0;
-}
+ .mx_desktopCapturerSourcePicker_tab {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: center;
+ align-items: flex-start;
+ height: 500px;
+ overflow: overlay;
-.mx_desktopCapturerSourcePicker_tabLabel,
-.mx_desktopCapturerSourcePicker_tabLabel_selected {
- width: 100%;
- text-align: center;
- border-radius: 8px;
- padding: 8px 0;
- font-size: $font-13px;
-}
+ .mx_desktopCapturerSourcePicker_source {
+ width: 50%;
+ display: flex;
+ flex-direction: column;
-.mx_desktopCapturerSourcePicker_tabLabel_selected {
- background-color: $tab-label-active-bg-color;
- color: $tab-label-active-fg-color;
-}
+ .mx_desktopCapturerSourcePicker_source_thumbnail {
+ margin: 4px;
+ padding: 4px;
+ border-width: 2px;
+ border-radius: 8px;
+ border-style: solid;
+ border-color: transparent;
-.mx_desktopCapturerSourcePicker_panel {
- display: flex;
- flex-wrap: wrap;
- justify-content: center;
- align-items: flex-start;
- height: 500px;
- overflow: overlay;
-}
+ &.mx_desktopCapturerSourcePicker_source_thumbnail_selected,
+ &:hover,
+ &:focus {
+ border-color: $accent-color;
+ }
+ }
-.mx_desktopCapturerSourcePicker_stream_button {
- display: flex;
- flex-direction: column;
- margin: 8px;
- border-radius: 4px;
-}
-
-.mx_desktopCapturerSourcePicker_stream_button:hover,
-.mx_desktopCapturerSourcePicker_stream_button:focus {
- background: $roomtile-selected-bg-color;
-}
-
-.mx_desktopCapturerSourcePicker_stream_thumbnail {
- margin: 4px;
- width: 312px;
-}
-
-.mx_desktopCapturerSourcePicker_stream_name {
- margin: 0 4px;
- white-space: nowrap;
- text-overflow: ellipsis;
- overflow: hidden;
- width: 312px;
+ .mx_desktopCapturerSourcePicker_source_name {
+ margin: 0 4px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ }
+ }
+ }
}
diff --git a/res/css/views/elements/_Field.scss b/res/css/views/elements/_Field.scss
index f67da6477b..cae81dcc97 100644
--- a/res/css/views/elements/_Field.scss
+++ b/res/css/views/elements/_Field.scss
@@ -39,7 +39,6 @@ limitations under the License.
.mx_Field select,
.mx_Field textarea {
font-weight: normal;
- font-family: $font-family;
font-size: $font-14px;
border: none;
// Even without a border here, we still need this avoid overlapping the rounded
diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss
index 54c7df3e0b..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;
@@ -43,6 +43,14 @@ limitations under the License.
}
}
+ &.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
+ mask-image: url('$(res)/img/voip/missed-voice.svg');
+ }
+
+ &.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
+ mask-image: url('$(res)/img/voip/missed-video.svg');
+ }
+
.mx_CallEvent_info {
display: flex;
flex-direction: row;
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 8629682693..c6170bf7c0 100644
--- a/res/css/views/rooms/_EventBubbleTile.scss
+++ b/res/css/views/rooms/_EventBubbleTile.scss
@@ -15,7 +15,7 @@ limitations under the License.
*/
.mx_EventTile[data-layout=bubble],
-.mx_EventTile[data-layout=bubble] ~ .mx_EventListSummary {
+.mx_EventListSummary[data-layout=bubble] {
--avatarSize: 32px;
--gutterSize: 11px;
--cornerRadius: 12px;
@@ -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 {
@@ -155,12 +159,20 @@ limitations under the License.
.mx_EventTile_avatar {
position: absolute;
top: 0;
+ line-height: 1;
+ z-index: 9;
img {
box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
border-radius: 50%;
}
}
+ &.mx_EventTile_noSender {
+ .mx_EventTile_avatar {
+ top: -19px;
+ }
+ }
+
.mx_BaseAvatar,
.mx_EventTile_avatar {
line-height: 1;
@@ -216,90 +228,6 @@ limitations under the License.
border-left-color: $eventbubble-reply-color;
}
- &.mx_EventTile_bubbleContainer,
- &.mx_EventTile_info,
- & ~ .mx_EventListSummary[data-expanded=false] {
- --backgroundColor: transparent;
- --gutterSize: 0;
-
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 5px 0;
-
- .mx_EventTile_avatar {
- position: static;
- order: -1;
- margin-right: 5px;
- }
- }
-
- & ~ .mx_EventListSummary {
- --maxWidth: 80%;
- margin-left: calc(var(--avatarSize) + var(--gutterSize));
- margin-right: calc(var(--gutterSize) + var(--avatarSize));
- .mx_EventListSummary_toggle {
- float: none;
- margin: 0;
- order: 9;
- margin-left: 5px;
- }
- .mx_EventListSummary_avatars {
- padding-top: 0;
- }
-
- &::after {
- content: "";
- clear: both;
- }
-
- .mx_EventTile {
- margin: 0 6px;
- }
-
- .mx_EventTile_line {
- margin: 0 5px;
- > a {
- left: auto;
- right: 0;
- transform: translateX(calc(100% + 5px));
- }
- }
-
- .mx_MessageActionBar {
- transform: translate3d(90%, 0, 0);
- }
- }
-
- & ~ .mx_EventListSummary[data-expanded=false] {
- padding: 0 34px;
- }
-
- /* events that do not require bubble layout */
- & ~ .mx_EventListSummary,
- &.mx_EventTile_bad {
- .mx_EventTile_line {
- background: transparent;
- }
-
- &:hover {
- &::before {
- background: transparent;
- }
- }
- }
-
- & + .mx_EventListSummary {
- .mx_EventTile {
- margin-top: 0;
- padding: 2px 0;
- }
- }
-
- .mx_EventListSummary_toggle {
- margin-right: 55px;
- }
-
/* Special layout scenario for "Unable To Decrypt (UTD)" events */
&.mx_EventTile_bad > .mx_EventTile_line {
display: grid;
@@ -334,3 +262,93 @@ limitations under the License.
max-width: 100%;
}
}
+
+.mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble],
+.mx_EventTile.mx_EventTile_info[data-layout=bubble],
+.mx_EventListSummary[data-layout=bubble][data-expanded=false] {
+ --backgroundColor: transparent;
+ --gutterSize: 0;
+
+ display: flex;
+ align-items: center;
+ justify-content: flex-start;
+ padding: 5px 0;
+
+ .mx_EventTile_avatar {
+ position: static;
+ 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: 70%;
+ margin-left: calc(var(--avatarSize) + var(--gutterSize));
+ margin-right: 94px;
+ .mx_EventListSummary_toggle {
+ float: none;
+ margin: 0;
+ order: 9;
+ margin-left: 5px;
+ margin-right: 55px;
+ }
+ .mx_EventListSummary_avatars {
+ padding-top: 0;
+ }
+
+ &::after {
+ content: "";
+ clear: both;
+ }
+
+ .mx_EventTile {
+ margin: 0 6px;
+ padding: 2px 0;
+ }
+
+ .mx_EventTile_line {
+ margin: 0 5px;
+ > a {
+ left: auto;
+ right: 0;
+ transform: translateX(calc(100% + 5px));
+ }
+ }
+
+ .mx_MessageActionBar {
+ transform: translate3d(90%, 0, 0);
+ }
+}
+
+.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
+ padding: 0 34px;
+}
+
+/* events that do not require bubble layout */
+.mx_EventListSummary[data-layout=bubble],
+.mx_EventTile.mx_EventTile_bad[data-layout=bubble] {
+ .mx_EventTile_line {
+ background: transparent;
+ }
+
+ &:hover {
+ &::before {
+ background: transparent;
+ }
+ }
+}
diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index bda4a15f45..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 .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 {
@@ -573,6 +580,12 @@ $hover-select-border: 4px;
color: $accent-color-alt;
}
+.mx_EventTile_content .markdown-body blockquote {
+ border-left: 2px solid $blockquote-bar-color;
+ border-radius: 2px;
+ padding: 0 10px;
+}
+
.mx_EventTile_content .markdown-body .hljs {
display: inline !important;
}
diff --git a/res/css/views/rooms/_IRCLayout.scss b/res/css/views/rooms/_IRCLayout.scss
index 97190807ca..578c0325d2 100644
--- a/res/css/views/rooms/_IRCLayout.scss
+++ b/res/css/views/rooms/_IRCLayout.scss
@@ -116,6 +116,11 @@ $irc-line-height: $font-18px;
.mx_EditMessageComposer_buttons {
position: relative;
}
+
+ .mx_ReactionsRow {
+ padding-left: 0;
+ padding-right: 0;
+ }
}
.mx_EventTile_emote {
diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss
index e6c0cc3f46..5e2eff4047 100644
--- a/res/css/views/rooms/_MessageComposer.scss
+++ b/res/css/views/rooms/_MessageComposer.scss
@@ -165,8 +165,6 @@ limitations under the License.
font-size: $font-14px;
max-height: 120px;
overflow: auto;
- /* needed for FF */
- font-family: $font-family;
}
/* hack for FF as vertical alignment of custom placeholder text is broken */
diff --git a/res/css/views/rooms/_ReplyTile.scss b/res/css/views/rooms/_ReplyTile.scss
index f3e204e415..fd21e5f348 100644
--- a/res/css/views/rooms/_ReplyTile.scss
+++ b/res/css/views/rooms/_ReplyTile.scss
@@ -60,8 +60,6 @@ limitations under the License.
$reply-lines: 2;
$line-height: $font-22px;
- pointer-events: none;
-
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss
index 5501ab343e..8196d5c67a 100644
--- a/res/css/views/rooms/_VoiceRecordComposerTile.scss
+++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss
@@ -20,7 +20,7 @@ limitations under the License.
height: 28px;
border: 2px solid $voice-record-stop-border-color;
border-radius: 32px;
- margin-right: 16px; // between us and the send button
+ margin-right: 8px; // between us and the waveform component
position: relative;
&::after {
@@ -46,9 +46,28 @@ 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.
+ // fixed height to reduce layout jumps with the play button appearing
+ // https://github.com/vector-im/element-web/issues/18431
+ height: 32px;
+
margin: 6px; // force the composer area to put a gutter around us
margin-right: 12px; // isolate from stop/send button
@@ -68,7 +87,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: 17px; // vertically center (middle align with clock)
border-radius: 10px;
}
}
diff --git a/res/css/views/settings/_LayoutSwitcher.scss b/res/css/views/settings/_LayoutSwitcher.scss
new file mode 100644
index 0000000000..924fe5ae1b
--- /dev/null
+++ b/res/css/views/settings/_LayoutSwitcher.scss
@@ -0,0 +1,91 @@
+/*
+Copyright 2020 - 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.
+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_LayoutSwitcher {
+ .mx_LayoutSwitcher_RadioButtons {
+ display: flex;
+ flex-direction: row;
+ gap: 24px;
+
+ color: $primary-fg-color;
+
+ > .mx_LayoutSwitcher_RadioButton {
+ flex-grow: 0;
+ flex-shrink: 1;
+ display: flex;
+ flex-direction: column;
+
+ width: 300px;
+
+ border: 1px solid $appearance-tab-border-color;
+ border-radius: 10px;
+
+ .mx_EventTile_msgOption,
+ .mx_MessageActionBar {
+ display: none;
+ }
+
+ .mx_LayoutSwitcher_RadioButton_preview {
+ flex-grow: 1;
+ display: flex;
+ align-items: center;
+ padding: 10px;
+ pointer-events: none;
+ }
+
+ .mx_RadioButton {
+ flex-grow: 0;
+ padding: 10px;
+ }
+
+ .mx_EventTile_content {
+ margin-right: 0;
+ }
+
+ &.mx_LayoutSwitcher_RadioButton_selected {
+ border-color: $accent-color;
+ }
+ }
+
+ .mx_RadioButton {
+ border-top: 1px solid $appearance-tab-border-color;
+
+ > input + div {
+ border-color: rgba($muted-fg-color, 0.2);
+ }
+ }
+
+ .mx_RadioButton_checked {
+ background-color: rgba($accent-color, 0.08);
+ }
+
+ .mx_EventTile {
+ margin: 0;
+ &[data-layout=bubble] {
+ margin-right: 40px;
+ }
+ &[data-layout=irc] {
+ > a {
+ display: none;
+ }
+ }
+ .mx_EventTile_line {
+ max-width: 90%;
+ }
+ }
+ }
+}
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 0d679af4e5..9f40372690 100644
--- a/res/css/views/settings/tabs/_SettingsTab.scss
+++ b/res/css/views/settings/tabs/_SettingsTab.scss
@@ -36,7 +36,6 @@ limitations under the License.
.mx_SettingsTab_subheading {
font-size: $font-16px;
display: block;
- font-family: $font-family;
font-weight: 600;
color: $primary-fg-color;
margin-bottom: 10px;
@@ -73,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/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
index ca5a6f0a66..d8e617a40d 100644
--- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
@@ -1,5 +1,5 @@
/*
-Copyright 2020 The Matrix.org Foundation C.I.C.
+Copyright 2020 - 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.
@@ -155,79 +155,6 @@ limitations under the License.
margin-left: calc($font-16px + 10px);
}
-.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
- display: flex;
- flex-direction: row;
- gap: 24px;
-
- color: $primary-fg-color;
-
- > .mx_AppearanceUserSettingsTab_Layout_RadioButton {
- flex-grow: 0;
- flex-shrink: 1;
- display: flex;
- flex-direction: column;
-
- width: 300px;
-
- border: 1px solid $appearance-tab-border-color;
- border-radius: 10px;
-
- .mx_EventTile_msgOption,
- .mx_MessageActionBar {
- display: none;
- }
-
- .mx_AppearanceUserSettingsTab_Layout_RadioButton_preview {
- flex-grow: 1;
- display: flex;
- align-items: center;
- padding: 10px;
- pointer-events: none;
- }
-
- .mx_RadioButton {
- flex-grow: 0;
- padding: 10px;
- }
-
- .mx_EventTile_content {
- margin-right: 0;
- }
-
- &.mx_AppearanceUserSettingsTab_Layout_RadioButton_selected {
- border-color: $accent-color;
- }
- }
-
- .mx_RadioButton {
- border-top: 1px solid $appearance-tab-border-color;
-
- > input + div {
- border-color: rgba($muted-fg-color, 0.2);
- }
- }
-
- .mx_RadioButton_checked {
- background-color: rgba($accent-color, 0.08);
- }
-
- .mx_EventTile {
- margin: 0;
- &[data-layout=bubble] {
- margin-right: 40px;
- }
- &[data-layout=irc] {
- > a {
- display: none;
- }
- }
- .mx_EventTile_line {
- max-width: 90%;
- }
- }
-}
-
.mx_AppearanceUserSettingsTab_Advanced {
color: $primary-fg-color;
diff --git a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss
index 0f879d209e..fbbe9909e7 100644
--- a/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_HelpUserSettingsTab.scss
@@ -28,28 +28,32 @@ limitations under the License.
user-select: all;
}
-.mx_HelpUserSettingsTab_accessToken {
+.mx_HelpUserSettingsTab_copy {
display: flex;
- justify-content: space-between;
border-radius: 5px;
border: solid 1px $light-fg-color;
margin-bottom: 10px;
margin-top: 10px;
padding: 10px;
-}
+ width: max-content;
-.mx_HelpUserSettingsTab_accessToken_copy {
- flex-shrink: 0;
- cursor: pointer;
- margin-left: 20px;
- display: inherit;
-}
+ .mx_HelpUserSettingsTab_copyButton {
+ flex-shrink: 0;
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+ margin-left: 20px;
+ display: block;
-.mx_HelpUserSettingsTab_accessToken_copy > div {
- mask-image: url($copy-button-url);
- background-color: $message-action-bar-fg-color;
- margin-left: 5px;
- width: 20px;
- height: 20px;
- background-repeat: no-repeat;
+ &::before {
+ content: "";
+
+ mask-image: url($copy-button-url);
+ background-color: $message-action-bar-fg-color;
+ width: 20px;
+ height: 20px;
+ display: block;
+ background-repeat: no-repeat;
+ }
+ }
}
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 205d431752..7752edddfa 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;
@@ -67,7 +67,30 @@ limitations under the License.
.mx_CallView_content {
position: relative;
display: flex;
+ justify-content: center;
border-radius: 8px;
+
+ > .mx_VideoFeed {
+ width: 100%;
+ height: 100%;
+
+ &.mx_VideoFeed_voice {
+ background-color: $inverted-bg-color;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ .mx_VideoFeed_video {
+ height: 100%;
+ background-color: #000;
+ }
+
+ .mx_VideoFeed_mic {
+ left: 10px;
+ bottom: 10px;
+ }
+ }
}
.mx_CallView_voice {
@@ -176,120 +199,14 @@ limitations under the License.
}
}
-.mx_CallView_header {
- height: 44px;
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: left;
- flex-shrink: 0;
-}
-
-.mx_CallView_header_callType {
- font-size: 1.2rem;
- font-weight: bold;
- vertical-align: middle;
-}
-
-.mx_CallView_header_secondaryCallInfo {
- &::before {
- content: '·';
- margin-left: 6px;
- margin-right: 6px;
- }
-}
-
-.mx_CallView_header_controls {
- margin-left: auto;
-}
-
-.mx_CallView_header_button {
- display: inline-block;
- vertical-align: middle;
- cursor: pointer;
-
- &::before {
- content: '';
- display: inline-block;
- height: 20px;
- width: 20px;
- vertical-align: middle;
- background-color: $secondary-fg-color;
- mask-repeat: no-repeat;
- mask-size: contain;
- mask-position: center;
- }
-}
-
-.mx_CallView_header_button_fullscreen {
- &::before {
- mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
- }
-}
-
-.mx_CallView_header_button_expand {
- &::before {
- mask-image: url('$(res)/img/element-icons/call/expand.svg');
- }
-}
-
-.mx_CallView_header_callInfo {
- margin-left: 12px;
- margin-right: 16px;
-}
-
-.mx_CallView_header_roomName {
- font-weight: bold;
- font-size: 12px;
- line-height: initial;
- height: 15px;
-}
-
-.mx_CallView_secondaryCall_roomName {
- margin-left: 4px;
-}
-
-.mx_CallView_header_callTypeSmall {
- font-size: 12px;
- color: $secondary-fg-color;
- line-height: initial;
- height: 15px;
- overflow: hidden;
- white-space: nowrap;
- text-overflow: ellipsis;
- max-width: 240px;
-}
-
-.mx_CallView_header_phoneIcon {
- display: inline-block;
- margin-right: 6px;
- height: 16px;
- width: 16px;
- vertical-align: middle;
-
- &::before {
- content: '';
- display: inline-block;
- vertical-align: top;
-
- height: 16px;
- width: 16px;
- background-color: $warning-color;
- mask-repeat: no-repeat;
- mask-size: contain;
- mask-position: center;
- mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
- }
-}
-
.mx_CallView_callControls {
position: absolute;
display: flex;
justify-content: center;
bottom: 5px;
- width: 100%;
opacity: 1;
transition: opacity 0.5s;
+ z-index: 200; // To be above _all_ feeds
}
.mx_CallView_callControls_hidden {
@@ -297,10 +214,29 @@ limitations under the License.
pointer-events: none;
}
+.mx_CallView_presenting {
+ opacity: 1;
+ transition: opacity 0.5s;
+
+ position: absolute;
+ margin-top: 18px;
+ padding: 4px 8px;
+ border-radius: 4px;
+
+ // Same on both themes
+ color: white;
+ background-color: #17191c;
+}
+
+.mx_CallView_presenting_hidden {
+ opacity: 0.001; // opacity 0 can cause a re-layout
+ pointer-events: none;
+}
+
.mx_CallView_callControls_button {
cursor: pointer;
- margin-left: 8px;
- margin-right: 8px;
+ margin-left: 2px;
+ margin-right: 2px;
&::before {
@@ -317,17 +253,11 @@ limitations under the License.
}
.mx_CallView_callControls_dialpad {
- margin-right: auto;
&::before {
background-image: url('$(res)/img/voip/dialpad.svg');
}
}
-.mx_CallView_callControls_button_dialpad_hidden {
- margin-right: auto;
- cursor: initial;
-}
-
.mx_CallView_callControls_button_micOn {
&::before {
background-image: url('$(res)/img/voip/mic-on.svg');
@@ -352,6 +282,30 @@ limitations under the License.
}
}
+.mx_CallView_callControls_button_screensharingOn {
+ &::before {
+ background-image: url('$(res)/img/voip/screensharing-on.svg');
+ }
+}
+
+.mx_CallView_callControls_button_screensharingOff {
+ &::before {
+ background-image: url('$(res)/img/voip/screensharing-off.svg');
+ }
+}
+
+.mx_CallView_callControls_button_sidebarOn {
+ &::before {
+ background-image: url('$(res)/img/voip/sidebar-on.svg');
+ }
+}
+
+.mx_CallView_callControls_button_sidebarOff {
+ &::before {
+ background-image: url('$(res)/img/voip/sidebar-off.svg');
+ }
+}
+
.mx_CallView_callControls_button_hangup {
&::before {
background-image: url('$(res)/img/voip/hangup.svg');
@@ -359,17 +313,11 @@ limitations under the License.
}
.mx_CallView_callControls_button_more {
- margin-left: auto;
&::before {
background-image: url('$(res)/img/voip/more.svg');
}
}
-.mx_CallView_callControls_button_more_hidden {
- margin-left: auto;
- cursor: initial;
-}
-
.mx_CallView_callControls_button_invisible {
visibility: hidden;
pointer-events: none;
diff --git a/res/css/views/voip/_CallViewHeader.scss b/res/css/views/voip/_CallViewHeader.scss
new file mode 100644
index 0000000000..014cfce478
--- /dev/null
+++ b/res/css/views/voip/_CallViewHeader.scss
@@ -0,0 +1,129 @@
+/*
+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_CallViewHeader {
+ height: 44px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: left;
+ flex-shrink: 0;
+ cursor: pointer;
+}
+
+.mx_CallViewHeader_callType {
+ font-size: 1.2rem;
+ font-weight: bold;
+ vertical-align: middle;
+}
+
+.mx_CallViewHeader_secondaryCallInfo {
+ &::before {
+ content: '·';
+ margin-left: 6px;
+ margin-right: 6px;
+ }
+}
+
+.mx_CallViewHeader_controls {
+ margin-left: auto;
+}
+
+.mx_CallViewHeader_button {
+ display: inline-block;
+ vertical-align: middle;
+ cursor: pointer;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ height: 20px;
+ width: 20px;
+ vertical-align: middle;
+ background-color: $secondary-fg-color;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ mask-position: center;
+ }
+}
+
+.mx_CallViewHeader_button_fullscreen {
+ &::before {
+ mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
+ }
+}
+
+.mx_CallViewHeader_button_expand {
+ &::before {
+ mask-image: url('$(res)/img/element-icons/call/expand.svg');
+ }
+}
+
+.mx_CallViewHeader_callInfo {
+ margin-left: 12px;
+ margin-right: 16px;
+}
+
+.mx_CallViewHeader_roomName {
+ font-weight: bold;
+ font-size: 12px;
+ line-height: initial;
+ height: 15px;
+}
+
+.mx_CallView_secondaryCall_roomName {
+ margin-left: 4px;
+}
+
+.mx_CallViewHeader_callTypeSmall {
+ font-size: 12px;
+ color: $secondary-fg-color;
+ line-height: initial;
+ height: 15px;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ max-width: 240px;
+}
+
+.mx_CallViewHeader_callTypeIcon {
+ display: inline-block;
+ margin-right: 6px;
+ height: 16px;
+ width: 16px;
+ vertical-align: middle;
+
+ &::before {
+ content: '';
+ display: inline-block;
+ vertical-align: top;
+
+ height: 16px;
+ width: 16px;
+ background-color: $secondary-fg-color;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ mask-position: center;
+ }
+
+ &.mx_CallViewHeader_callTypeIcon_voice::before {
+ mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+ }
+
+ &.mx_CallViewHeader_callTypeIcon_video::before {
+ mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+ }
+}
diff --git a/res/css/views/voip/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss
new file mode 100644
index 0000000000..dbadc22028
--- /dev/null
+++ b/res/css/views/voip/_CallViewSidebar.scss
@@ -0,0 +1,61 @@
+/*
+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_CallViewSidebar {
+ position: absolute;
+ right: 16px;
+ bottom: 16px;
+ z-index: 100; // To be above the primary feed
+
+ overflow: auto;
+
+ height: calc(100% - 32px); // Subtract the top and bottom padding
+ width: 20%;
+
+ display: flex;
+ flex-direction: column-reverse;
+ justify-content: flex-start;
+ align-items: flex-end;
+ gap: 12px;
+
+ > .mx_VideoFeed {
+ width: 100%;
+
+ &.mx_VideoFeed_voice {
+ border-radius: 4px;
+
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .mx_VideoFeed_video {
+ border-radius: 4px;
+ }
+
+ .mx_VideoFeed_mic {
+ left: 6px;
+ bottom: 6px;
+ }
+ }
+
+ &.mx_CallViewSidebar_pipMode {
+ top: 16px;
+ bottom: unset;
+ justify-content: flex-end;
+ gap: 4px;
+ }
+}
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 4a3fbdf597..7a8d39dfe3 100644
--- a/res/css/views/voip/_VideoFeed.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -14,37 +14,54 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_VideoFeed_voice {
- background-color: $inverted-bg-color;
-}
+.mx_VideoFeed {
+ overflow: hidden;
+ position: relative;
-
-.mx_VideoFeed_remote {
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
-
- &.mx_VideoFeed_video {
- background-color: #000;
+ &.mx_VideoFeed_voice {
+ background-color: $inverted-bg-color;
+ aspect-ratio: 16 / 9;
}
-}
-.mx_VideoFeed_local {
- max-width: 25%;
- max-height: 25%;
- position: absolute;
- right: 10px;
- top: 10px;
- z-index: 100;
- border-radius: 4px;
-
- &.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/img/voip/missed-video.svg b/res/img/voip/missed-video.svg
new file mode 100644
index 0000000000..a2f3bc73ac
--- /dev/null
+++ b/res/img/voip/missed-video.svg
@@ -0,0 +1,3 @@
+
diff --git a/res/img/voip/missed-voice.svg b/res/img/voip/missed-voice.svg
new file mode 100644
index 0000000000..5e3993584e
--- /dev/null
+++ b/res/img/voip/missed-voice.svg
@@ -0,0 +1,4 @@
+
diff --git a/res/img/voip/screensharing-off.svg b/res/img/voip/screensharing-off.svg
new file mode 100644
index 0000000000..dc19e9892e
--- /dev/null
+++ b/res/img/voip/screensharing-off.svg
@@ -0,0 +1,18 @@
+
diff --git a/res/img/voip/screensharing-on.svg b/res/img/voip/screensharing-on.svg
new file mode 100644
index 0000000000..a8e7fe308e
--- /dev/null
+++ b/res/img/voip/screensharing-on.svg
@@ -0,0 +1,18 @@
+
diff --git a/res/img/voip/sidebar-off.svg b/res/img/voip/sidebar-off.svg
new file mode 100644
index 0000000000..7637a9ab55
--- /dev/null
+++ b/res/img/voip/sidebar-off.svg
@@ -0,0 +1,20 @@
+
diff --git a/res/img/voip/sidebar-on.svg b/res/img/voip/sidebar-on.svg
new file mode 100644
index 0000000000..a625334be4
--- /dev/null
+++ b/res/img/voip/sidebar-on.svg
@@ -0,0 +1,19 @@
+
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..b9429318ac 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;
// ********************
@@ -222,6 +222,13 @@ $appearance-tab-border-color: $room-highlight-color;
$composer-shadow-color: tranparent;
+// Bubble tiles
+$eventbubble-self-bg: #14322E;
+$eventbubble-others-bg: $event-selected-color;
+$eventbubble-bg-hover: #1C2026;
+$eventbubble-avatar-outline: $bg-color;
+$eventbubble-reply-color: #C1C6CD;
+
// ***** Mixins! *****
@define-mixin mx_DialogButton {
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-custom/css/_custom.scss b/res/themes/light-custom/css/_custom.scss
index 1b9254d100..6c37351414 100644
--- a/res/themes/light-custom/css/_custom.scss
+++ b/res/themes/light-custom/css/_custom.scss
@@ -140,3 +140,10 @@ $event-highlight-bg-color: var(--timeline-highlights-color);
//
// redirect some variables away from their hardcoded values in the light theme
$settings-grey-fg-color: $primary-fg-color;
+
+// --eventbubble colors
+$eventbubble-self-bg: var(--eventbubble-self-bg, $eventbubble-self-bg);
+$eventbubble-others-bg: var(--eventbubble-others-bg, $eventbubble-others-bg);
+$eventbubble-bg-hover: var(--eventbubble-bg-hover, $eventbubble-bg-hover);
+$eventbubble-avatar-outline: var(--eventbubble-avatar-outline, $eventbubble-avatar-outline);
+$eventbubble-reply-color: var(--eventbubble-reply-color, $eventbubble-reply-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 e7ba1aa9fb..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.
@@ -56,12 +57,10 @@ limitations under the License.
import React from 'react';
import { MatrixClientPeg } from './MatrixClientPeg';
-import PlatformPeg from './PlatformPeg';
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";
@@ -80,7 +79,6 @@ import CountlyAnalytics from "./CountlyAnalytics";
import { UIFeature } from "./settings/UIFeature";
import { CallError } from "matrix-js-sdk/src/webrtc/call";
import { logger } from 'matrix-js-sdk/src/logger';
-import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker";
import { Action } from './dispatcher/actions';
import VoipUserMapper from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
@@ -88,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';
@@ -129,14 +133,9 @@ interface ThirdpartyLookupResponse {
fields: ThirdpartyLookupResponseFields;
}
-// Unlike 'CallType' in js-sdk, this one includes screen sharing
-// (because a screen sharing call is only a screen sharing call to the caller,
-// to the callee it's just a video call, at least as far as the current impl
-// is concerned).
export enum PlaceCallType {
Voice = 'voice',
Video = 'video',
- ScreenSharing = 'screensharing',
}
export enum CallHandlerEvent {
@@ -483,36 +482,40 @@ 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:
- {
- Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
+ }
+ case CallState.Ended: {
+ const hangupReason = call.hangupReason;
+ Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
this.removeCallForRoom(mappedRoomId);
- if (oldState === CallState.InviteSent && (
- call.hangupParty === CallParty.Remote ||
- (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
- )) {
+ if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
this.play(AudioID.Busy);
let title;
let description;
- if (call.hangupReason === CallErrorCode.UserHangup) {
- title = _t("Call Declined");
- description = _t("The other party declined the call.");
- } else if (call.hangupReason === CallErrorCode.UserBusy) {
+ // TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
+ if (call.hangupReason === CallErrorCode.UserBusy) {
title = _t("User Busy");
description = _t("The user you called is busy.");
- } else if (call.hangupReason === CallErrorCode.InviteTimeout) {
- title = _t("Call Failed");
- // XXX: full stop appended as some relic here, but these
- // strings need proper input from design anyway, so let's
- // not change this string until we have a proper one.
- description = _t('The remote side failed to pick up') + '.';
- } else {
+ } else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
title = _t("Call Failed");
description = _t("The call could not be established");
}
@@ -521,7 +524,7 @@ export default class CallHandler extends EventEmitter {
title, description,
});
} else if (
- call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
+ hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
) {
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
title: _t("Answered Elsewhere"),
@@ -641,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,
@@ -738,25 +754,6 @@ export default class CallHandler extends EventEmitter {
call.placeVoiceCall();
} else if (type === 'video') {
call.placeVideoCall();
- } else if (type === PlaceCallType.ScreenSharing) {
- const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
- if (screenCapErrorString) {
- this.removeCallForRoom(roomId);
- console.log("Can't capture screen: " + screenCapErrorString);
- Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
- title: _t('Unable to capture screen'),
- description: screenCapErrorString,
- });
- return;
- }
-
- call.placeScreenSharingCall(
- async (): Promise => {
- const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
- const [source] = await finished;
- return source;
- },
- );
} else {
console.error("Unknown conf call type: " + type);
}
@@ -950,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) {
@@ -1029,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/DateUtils.ts b/src/DateUtils.ts
index e4a1175d88..e8b81ca315 100644
--- a/src/DateUtils.ts
+++ b/src/DateUtils.ts
@@ -123,6 +123,19 @@ export function formatTime(date: Date, showTwelveHour = false): string {
return pad(date.getHours()) + ':' + pad(date.getMinutes());
}
+export function formatCallTime(delta: Date): string {
+ const hours = delta.getUTCHours();
+ const minutes = delta.getUTCMinutes();
+ const seconds = delta.getUTCSeconds();
+
+ let output = "";
+ if (hours) output += `${hours}h `;
+ if (minutes || output) output += `${minutes}m `;
+ if (seconds || output) output += `${seconds}s`;
+
+ return output;
+}
+
const MILLIS_IN_DAY = 86400000;
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
if (!nextEventDate || !prevEventDate) {
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/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts
index 384f20cd4e..b48bb32efe 100644
--- a/src/components/structures/CallEventGrouper.ts
+++ b/src/components/structures/CallEventGrouper.ts
@@ -27,9 +27,15 @@ export enum CallEventGrouperEvent {
SilencedChanged = "silenced_changed",
}
+const CONNECTING_STATES = [
+ CallState.Connecting,
+ CallState.WaitLocalMedia,
+ CallState.CreateOffer,
+ CallState.CreateAnswer,
+];
+
const SUPPORTED_STATES = [
CallState.Connected,
- CallState.Connecting,
CallState.Ringing,
];
@@ -61,6 +67,10 @@ export default class CallEventGrouper extends EventEmitter {
return [...this.events].find((event) => event.getType() === EventType.CallReject);
}
+ private get selectAnswer(): MatrixEvent {
+ return [...this.events].find((event) => event.getType() === EventType.CallSelectAnswer);
+ }
+
public get isVoice(): boolean {
const invite = this.invite;
if (!invite) return;
@@ -74,6 +84,19 @@ export default class CallEventGrouper extends EventEmitter {
return this.hangup?.getContent()?.reason;
}
+ public get rejectParty(): string {
+ return this.reject?.getSender();
+ }
+
+ public get gotRejected(): boolean {
+ return Boolean(this.reject);
+ }
+
+ public get duration(): Date {
+ if (!this.hangup || !this.selectAnswer) return;
+ return new Date(this.hangup.getDate().getTime() - this.selectAnswer.getDate().getTime());
+ }
+
/**
* Returns true if there are only events from the other side - we missed the call
*/
@@ -119,7 +142,9 @@ export default class CallEventGrouper extends EventEmitter {
}
private setState = () => {
- if (SUPPORTED_STATES.includes(this.call?.state)) {
+ if (CONNECTING_STATES.includes(this.call?.state)) {
+ this.state = CallState.Connecting;
+ } else if (SUPPORTED_STATES.includes(this.call?.state)) {
this.state = this.call.state;
} else {
if (this.callWasMissed) this.state = CustomCallState.Missed;
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/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 39ede68a75..1691d90651 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -51,7 +51,12 @@ import EditorStateTransfer from "../../utils/EditorStateTransfer";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
-const membershipTypes = [EventType.RoomMember, EventType.RoomThirdPartyInvite, EventType.RoomServerAcl];
+const groupedEvents = [
+ EventType.RoomMember,
+ EventType.RoomThirdPartyInvite,
+ EventType.RoomServerAcl,
+ EventType.RoomPinnedEvents,
+];
// check if there is a previous event and it has the same sender as this event
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
@@ -618,7 +623,15 @@ export default class MessagePanel extends React.Component {
for (const Grouper of groupers) {
if (Grouper.canStartGroup(this, mxEv)) {
- grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent, nextEvent, nextTile);
+ grouper = new Grouper(
+ this,
+ mxEv,
+ prevEvent,
+ lastShownEvent,
+ this.props.layout,
+ nextEvent,
+ nextTile,
+ );
}
}
if (!grouper) {
@@ -981,6 +994,7 @@ abstract class BaseGrouper {
public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent,
public readonly lastShownEvent: MatrixEvent,
+ protected readonly layout: Layout,
public readonly nextEvent?: MatrixEvent,
public readonly nextEventTile?: MatrixEvent,
) {
@@ -1107,6 +1121,7 @@ class CreationGrouper extends BaseGrouper {
onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={summaryText}
+ layout={this.layout}
>
{ eventTiles }
,
@@ -1134,10 +1149,11 @@ class RedactionGrouper extends BaseGrouper {
ev: MatrixEvent,
prevEvent: MatrixEvent,
lastShownEvent: MatrixEvent,
+ layout: Layout,
nextEvent: MatrixEvent,
nextEventTile: MatrixEvent,
) {
- super(panel, ev, prevEvent, lastShownEvent, nextEvent, nextEventTile);
+ super(panel, ev, prevEvent, lastShownEvent, layout, nextEvent, nextEventTile);
this.events = [ev];
}
@@ -1202,6 +1218,7 @@ class RedactionGrouper extends BaseGrouper {
onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={Array.from(senders)}
summaryText={_t("%(count)s messages deleted.", { count: eventTiles.length })}
+ layout={this.layout}
>
{ eventTiles }
,
@@ -1222,7 +1239,7 @@ class RedactionGrouper extends BaseGrouper {
// Wrap consecutive member events in a ListSummary, ignore if redacted
class MemberGrouper extends BaseGrouper {
static canStartGroup = function(panel: MessagePanel, ev: MatrixEvent): boolean {
- return panel.shouldShowEvent(ev) && membershipTypes.includes(ev.getType() as EventType);
+ return panel.shouldShowEvent(ev) && groupedEvents.includes(ev.getType() as EventType);
};
constructor(
@@ -1230,8 +1247,9 @@ class MemberGrouper extends BaseGrouper {
public readonly event: MatrixEvent,
public readonly prevEvent: MatrixEvent,
public readonly lastShownEvent: MatrixEvent,
+ protected readonly layout: Layout,
) {
- super(panel, event, prevEvent, lastShownEvent);
+ super(panel, event, prevEvent, lastShownEvent, layout);
this.events = [event];
}
@@ -1239,7 +1257,7 @@ class MemberGrouper extends BaseGrouper {
if (this.panel.wantsDateSeparator(this.events[0], ev.getDate())) {
return false;
}
- return membershipTypes.includes(ev.getType() as EventType);
+ return groupedEvents.includes(ev.getType() as EventType);
}
public add(ev: MatrixEvent, showHiddenEvents?: boolean): void {
@@ -1306,6 +1324,7 @@ class MemberGrouper extends BaseGrouper {
events={this.events}
onToggle={panel.onHeightChanged} // Update scroll state
startExpanded={highlightInMels}
+ layout={this.layout}
>
{ eventTiles }
,
diff --git a/src/components/structures/ScrollPanel.tsx b/src/components/structures/ScrollPanel.tsx
index 1d16755106..112f8d2c21 100644
--- a/src/components/structures/ScrollPanel.tsx
+++ b/src/components/structures/ScrollPanel.tsx
@@ -183,8 +183,14 @@ export default class ScrollPanel extends React.Component {
private readonly itemlist = createRef();
private unmounted = false;
private scrollTimeout: Timer;
+ // Are we currently trying to backfill?
private isFilling: boolean;
+ // Is the current fill request caused by a props update?
+ private isFillingDueToPropsUpdate = false;
+ // Did another request to check the fill state arrive while we were trying to backfill?
private fillRequestWhileRunning: boolean;
+ // Is that next fill request scheduled because of a props update?
+ private pendingFillDueToPropsUpdate: boolean;
private scrollState: IScrollState;
private preventShrinkingState: IPreventShrinkingState;
private unfillDebouncer: number;
@@ -213,7 +219,7 @@ export default class ScrollPanel extends React.Component {
// adding events to the top).
//
// This will also re-check the fill state, in case the paginate was inadequate
- this.checkScroll();
+ this.checkScroll(true);
this.updatePreventShrinking();
}
@@ -251,12 +257,12 @@ export default class ScrollPanel extends React.Component {
// after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary.
- public checkScroll = () => {
+ public checkScroll = (isFromPropsUpdate = false) => {
if (this.unmounted) {
return;
}
this.restoreSavedScrollState();
- this.checkFillState();
+ this.checkFillState(0, isFromPropsUpdate);
};
// return true if the content is fully scrolled down right now; else false.
@@ -319,7 +325,7 @@ export default class ScrollPanel extends React.Component {
}
// check the scroll state and send out backfill requests if necessary.
- public checkFillState = async (depth = 0): Promise => {
+ public checkFillState = async (depth = 0, isFromPropsUpdate = false): Promise => {
if (this.unmounted) {
return;
}
@@ -355,14 +361,20 @@ export default class ScrollPanel extends React.Component {
// don't allow more than 1 chain of calls concurrently
// do make a note when a new request comes in while already running one,
// so we can trigger a new chain of calls once done.
+ // However, we make an exception for when we're already filling due to a
+ // props (or children) update, because very often the children include
+ // spinners to say whether we're paginating or not, so this would cause
+ // infinite paginating.
if (isFirstCall) {
- if (this.isFilling) {
+ if (this.isFilling && !this.isFillingDueToPropsUpdate) {
debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
this.fillRequestWhileRunning = true;
+ this.pendingFillDueToPropsUpdate = isFromPropsUpdate;
return;
}
debuglog("isFilling: setting");
this.isFilling = true;
+ this.isFillingDueToPropsUpdate = isFromPropsUpdate;
}
const itemlist = this.itemlist.current;
@@ -393,11 +405,14 @@ export default class ScrollPanel extends React.Component {
if (isFirstCall) {
debuglog("isFilling: clearing");
this.isFilling = false;
+ this.isFillingDueToPropsUpdate = false;
}
if (this.fillRequestWhileRunning) {
+ const refillDueToPropsUpdate = this.pendingFillDueToPropsUpdate;
this.fillRequestWhileRunning = false;
- this.checkFillState();
+ this.pendingFillDueToPropsUpdate = false;
+ this.checkFillState(0, refillDueToPropsUpdate);
}
};
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 }
+
+
+
);
}
diff --git a/src/components/views/elements/EventListSummary.tsx b/src/components/views/elements/EventListSummary.tsx
index b1cc9c773d..cbb0e17b42 100644
--- a/src/components/views/elements/EventListSummary.tsx
+++ b/src/components/views/elements/EventListSummary.tsx
@@ -22,6 +22,7 @@ import MemberAvatar from '../avatars/MemberAvatar';
import { _t } from '../../../languageHandler';
import { useStateToggle } from "../../../hooks/useStateToggle";
import AccessibleButton from "./AccessibleButton";
+import { Layout } from '../../../settings/Layout';
interface IProps {
// An array of member events to summarise
@@ -33,11 +34,13 @@ interface IProps {
// The list of room members for which to show avatars next to the summary
summaryMembers?: RoomMember[];
// The text to show as the summary of this event list
- summaryText?: string;
+ summaryText?: string | JSX.Element;
// An array of EventTiles to render when expanded
children: ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
+ // The layout currently used
+ layout?: Layout;
}
const EventListSummary: React.FC = ({
@@ -48,6 +51,7 @@ const EventListSummary: React.FC = ({
startExpanded,
summaryMembers = [],
summaryText,
+ layout,
}) => {
const [expanded, toggleExpanded] = useStateToggle(startExpanded);
@@ -63,7 +67,7 @@ const EventListSummary: React.FC = ({
// If we are only given few events then just pass them through
if (events.length < threshold) {
return (
-
{ expanded ? _t('collapse') : _t('expand') }
@@ -103,6 +107,7 @@ const EventListSummary: React.FC = ({
EventListSummary.defaultProps = {
startExpanded: false,
+ layout: Layout.Group,
};
export default EventListSummary;
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(
+ { labelRestricted }
+
);
+ }
+
+ return
+ { options }
+ ;
+};
+
+export default JoinRuleDropdown;
diff --git a/src/components/views/elements/MemberEventListSummary.tsx b/src/components/views/elements/MemberEventListSummary.tsx
index d52462f629..0722cb872a 100644
--- a/src/components/views/elements/MemberEventListSummary.tsx
+++ b/src/components/views/elements/MemberEventListSummary.tsx
@@ -25,12 +25,31 @@ import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
+import defaultDispatcher from '../../../dispatcher/dispatcher';
+import { RightPanelPhases } from '../../../stores/RightPanelStorePhases';
+import { Action } from '../../../dispatcher/actions';
+import { SetRightPanelPhasePayload } from '../../../dispatcher/payloads/SetRightPanelPhasePayload';
+import { jsxJoin } from '../../../utils/ReactUtils';
+import { EventType } from 'matrix-js-sdk/src/@types/event';
+import { Layout } from '../../../settings/Layout';
+
+const onPinnedMessagesClick = (): void => {
+ defaultDispatcher.dispatch({
+ action: Action.SetRightPanelPhase,
+ phase: RightPanelPhases.PinnedMessages,
+ allowClose: false,
+ });
+};
+
+const SENDER_AS_DISPLAY_NAME_EVENTS = [EventType.RoomServerAcl, EventType.RoomPinnedEvents];
interface IProps extends Omit, "summaryText" | "summaryMembers"> {
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number;
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
+ // The currently selected layout
+ layout: Layout;
}
interface IUserEvents {
@@ -57,6 +76,7 @@ enum TransitionType {
ChangedAvatar = "changed_avatar",
NoChange = "no_change",
ServerAcl = "server_acl",
+ ChangedPins = "pinned_messages"
}
const SEP = ",";
@@ -67,6 +87,7 @@ export default class MemberEventListSummary extends React.Component {
summaryLength: 1,
threshold: 3,
avatarsMaxLength: 5,
+ layout: Layout.Group,
};
shouldComponentUpdate(nextProps) {
@@ -89,7 +110,10 @@ export default class MemberEventListSummary extends React.Component {
* `Object.keys(eventAggregates)`.
* @returns {string} the textual summary of the aggregated events that occurred.
*/
- private generateSummary(eventAggregates: Record, orderedTransitionSequences: string[]) {
+ private generateSummary(
+ eventAggregates: Record,
+ orderedTransitionSequences: string[],
+ ): string | JSX.Element {
const summaries = orderedTransitionSequences.map((transitions) => {
const userNames = eventAggregates[transitions];
const nameList = this.renderNameList(userNames);
@@ -118,7 +142,7 @@ export default class MemberEventListSummary extends React.Component {
return null;
}
- return summaries.join(", ");
+ return jsxJoin(summaries, ", ");
}
/**
@@ -212,7 +236,11 @@ export default class MemberEventListSummary extends React.Component {
* @param {number} repeats the number of times the transition was repeated in a row.
* @returns {string} the written Human Readable equivalent of the transition.
*/
- private static getDescriptionForTransition(t: TransitionType, userCount: number, repeats: number) {
+ private static getDescriptionForTransition(
+ t: TransitionType,
+ userCount: number,
+ repeats: number,
+ ): string | JSX.Element {
// The empty interpolations 'severalUsers' and 'oneUser'
// are there only to show translators to non-English languages
// that the verb is conjugated to plural or singular Subject.
@@ -295,6 +323,15 @@ export default class MemberEventListSummary extends React.Component {
{ severalUsers: "", count: repeats })
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
break;
+ case "pinned_messages":
+ res = (userCount > 1)
+ ? _t("%(severalUsers)schanged the pinned messages for the room %(count)s times.",
+ { severalUsers: "", count: repeats },
+ { "a": (sub) => { sub } })
+ : _t("%(oneUser)schanged the pinned messages for the room %(count)s times.",
+ { oneUser: "", count: repeats },
+ { "a": (sub) => { sub } });
+ break;
}
return res;
@@ -313,16 +350,18 @@ export default class MemberEventListSummary extends React.Component {
* if a transition is not recognised.
*/
private static getTransition(e: IUserEvents): TransitionType {
- if (e.mxEvent.getType() === 'm.room.third_party_invite') {
+ const type = e.mxEvent.getType();
+
+ if (type === EventType.RoomThirdPartyInvite) {
// Handle 3pid invites the same as invites so they get bundled together
if (!isValid3pidInvite(e.mxEvent)) {
return TransitionType.InviteWithdrawal;
}
return TransitionType.Invited;
- }
-
- if (e.mxEvent.getType() === 'm.room.server_acl') {
+ } else if (type === EventType.RoomServerAcl) {
return TransitionType.ServerAcl;
+ } else if (type === EventType.RoomPinnedEvents) {
+ return TransitionType.ChangedPins;
}
switch (e.mxEvent.getContent().membership) {
@@ -411,22 +450,23 @@ export default class MemberEventListSummary extends React.Component {
// Object mapping user IDs to an array of IUserEvents
const userEvents: Record = {};
eventsToRender.forEach((e, index) => {
- const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
+ const type = e.getType();
+ const userId = type === EventType.RoomServerAcl ? e.getSender() : e.getStateKey();
// Initialise a user's events
if (!userEvents[userId]) {
userEvents[userId] = [];
}
- if (e.getType() === 'm.room.server_acl') {
+ if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
latestUserAvatarMember.set(userId, e.sender);
} else if (e.target) {
latestUserAvatarMember.set(userId, e.target);
}
let displayName = userId;
- if (e.getType() === 'm.room.third_party_invite') {
+ if (type === EventType.RoomThirdPartyInvite) {
displayName = e.getContent().display_name;
- } else if (e.getType() === 'm.room.server_acl') {
+ } else if (SENDER_AS_DISPLAY_NAME_EVENTS.includes(type as EventType)) {
displayName = e.sender.name;
} else if (e.target) {
displayName = e.target.name;
@@ -453,6 +493,7 @@ export default class MemberEventListSummary extends React.Component {
startExpanded={this.props.startExpanded}
children={this.props.children}
summaryMembers={[...latestUserAvatarMember.values()]}
+ layout={this.props.layout}
summaryText={this.generateSummary(aggregate.names, orderedTransitionSequences)} />;
}
}
diff --git a/src/components/views/elements/Pill.js b/src/components/views/elements/Pill.js
index aba1d443a9..95d29fc9ae 100644
--- a/src/components/views/elements/Pill.js
+++ b/src/components/views/elements/Pill.js
@@ -192,7 +192,8 @@ class Pill extends React.Component {
});
}
- onUserPillClicked = () => {
+ onUserPillClicked = (e) => {
+ e.preventDefault();
dis.dispatch({
action: Action.ViewUser,
member: this.state.member,
diff --git a/src/components/views/messages/CallEvent.tsx b/src/components/views/messages/CallEvent.tsx
index c81055bfb7..bc868c35b3 100644
--- a/src/components/views/messages/CallEvent.tsx
+++ b/src/components/views/messages/CallEvent.tsx
@@ -25,6 +25,7 @@ import { CallErrorCode, CallState } from 'matrix-js-sdk/src/webrtc/call';
import InfoTooltip, { InfoTooltipKind } from '../elements/InfoTooltip';
import classNames from 'classnames';
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
+import { formatCallTime } from "../../../DateUtils";
interface IProps {
mxEvent: MatrixEvent;
@@ -69,6 +70,18 @@ export default class CallEvent extends React.Component {
this.setState({ callState: newState });
};
+ private renderCallBackButton(text: string): JSX.Element {
+ return (
+
+ { text }
+
+ );
+ }
+
private renderContent(state: CallState | CustomCallState): JSX.Element {
if (state === CallState.Ringing) {
const silenceClass = classNames({
@@ -103,17 +116,37 @@ export default class CallEvent extends React.Component {
}
if (state === CallState.Ended) {
const hangupReason = this.props.callEventGrouper.hangupReason;
+ const gotRejected = this.props.callEventGrouper.gotRejected;
- if ([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason) {
+ if (gotRejected) {
+ return (
+
+ );
+ } else if (([CallErrorCode.UserHangup, "user hangup"].includes(hangupReason) || !hangupReason)) {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore)
// Also, if we don't have a reason
+ const duration = this.props.callEventGrouper.duration;
+ let text = _t("Call ended");
+ if (duration) {
+ text += " • " + formatCallTime(duration);
+ }
return (
}
);
}
- // 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.
+ */ }