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 4e2e933a52..b73462d188 100644
--- a/package.json
+++ b/package.json
@@ -65,8 +65,8 @@
"counterpart": "^0.18.6",
"diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5",
- "emojibase-data": "^5.1.1",
- "emojibase-regex": "^4.1.1",
+ "emojibase-data": "^6.2.0",
+ "emojibase-regex": "^5.1.3",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "6.1.0",
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 7df45b857e..76551b51f8 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,21 @@
@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";
@import "./views/dialogs/_NewSessionReviewDialog.scss";
@@ -159,6 +163,7 @@
@import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss";
@import "./views/groups/_GroupUserSettings.scss";
+@import "./views/messages/_CallEvent.scss";
@import "./views/messages/_CreateEvent.scss";
@import "./views/messages/_DateSeparator.scss";
@import "./views/messages/_EventTileBubble.scss";
@@ -171,7 +176,6 @@
@import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss";
-@import "./views/messages/_MVoiceMessageBody.scss";
@import "./views/messages/_MediaBody.scss";
@import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss";
@@ -200,8 +204,8 @@
@import "./views/rooms/_E2EIcon.scss";
@import "./views/rooms/_EditMessageComposer.scss";
@import "./views/rooms/_EntityTile.scss";
-@import "./views/rooms/_EventTile.scss";
@import "./views/rooms/_EventBubbleTile.scss";
+@import "./views/rooms/_EventTile.scss";
@import "./views/rooms/_GroupLayout.scss";
@import "./views/rooms/_IRCLayout.scss";
@import "./views/rooms/_JumpToBottomButton.scss";
@@ -268,6 +272,7 @@
@import "./views/voip/_CallPreview.scss";
@import "./views/voip/_CallView.scss";
@import "./views/voip/_CallViewForRoom.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/_FilePanel.scss b/res/css/structures/_FilePanel.scss
index 7b975110e1..c180a8a02d 100644
--- a/res/css/structures/_FilePanel.scss
+++ b/res/css/structures/_FilePanel.scss
@@ -45,9 +45,14 @@ limitations under the License.
/* Overrides for the attachment body tiles */
-.mx_FilePanel .mx_EventTile {
+.mx_FilePanel .mx_EventTile:not([data-layout=bubble]) {
word-break: break-word;
- margin-top: 32px;
+ margin-top: 10px;
+ padding-top: 0;
+
+ .mx_EventTile_line {
+ padding-left: 0;
+ }
}
.mx_FilePanel .mx_EventTile .mx_MImageBody {
@@ -118,10 +123,6 @@ limitations under the License.
padding-left: 0px;
}
-.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
- background-color: $primary-bg-color;
-}
-
.mx_FilePanel_empty::before {
mask-image: url('$(res)/img/element-icons/room/files.svg');
}
diff --git a/res/css/structures/_NotificationPanel.scss b/res/css/structures/_NotificationPanel.scss
index e54feca175..d271cd2bcc 100644
--- a/res/css/structures/_NotificationPanel.scss
+++ b/res/css/structures/_NotificationPanel.scss
@@ -84,7 +84,7 @@ limitations under the License.
display: inline;
}
-.mx_NotificationPanel .mx_EventTile_senderDetails {
+.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_senderDetails {
padding-left: 36px; // align with the room name
position: relative;
@@ -105,7 +105,7 @@ limitations under the License.
padding-left: 5px;
}
-.mx_NotificationPanel .mx_EventTile_line {
+.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line {
margin-right: 0px;
padding-left: 36px; // align with the room name
padding-top: 0px;
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 48b565be7f..58a4b426c2 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px;
}
.mx_SpaceRoomView_landing {
+ display: flex;
+ flex-direction: column;
+
> .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px;
@@ -332,23 +335,22 @@ $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;
+ 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
+ }
- // hide the HR as we have our own
- & + hr {
- display: none;
- }
+ .mx_SpaceRoomDirectory_list {
+ // we don't want this container to get forced into the flexbox layout
+ display: contents;
}
}
@@ -504,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/views/audio_messages/_AudioPlayer.scss b/res/css/views/audio_messages/_AudioPlayer.scss
index 9a65ad008f..77dcebbb9a 100644
--- a/res/css/views/audio_messages/_AudioPlayer.scss
+++ b/res/css/views/audio_messages/_AudioPlayer.scss
@@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_AudioPlayer_container {
+.mx_MediaBody.mx_AudioPlayer_container {
padding: 16px 12px 12px 12px;
- max-width: 267px; // use max to make the control fit in the files/pinned panels
.mx_AudioPlayer_primaryContainer {
display: flex;
diff --git a/res/css/views/audio_messages/_PlaybackContainer.scss b/res/css/views/audio_messages/_PlaybackContainer.scss
index 5548f6198e..773fc50fb9 100644
--- a/res/css/views/audio_messages/_PlaybackContainer.scss
+++ b/res/css/views/audio_messages/_PlaybackContainer.scss
@@ -18,10 +18,10 @@ limitations under the License.
// are shared amongst multiple voice message components.
// Container for live recording and playback controls
-.mx_VoiceMessagePrimaryContainer {
- // 7px top and bottom for visual design. 12px left & right, but the waveform (right)
- // has a 1px padding on it that we want to account for.
- padding: 7px 12px 7px 11px;
+.mx_MediaBody.mx_VoiceMessagePrimaryContainer {
+ // The waveform (right) has a 1px padding on it that we want to account for, otherwise
+ // inherit from mx_MediaBody
+ padding-right: 11px;
// Cheat at alignment a bit
display: flex;
diff --git a/res/css/views/avatars/_BaseAvatar.scss b/res/css/views/avatars/_BaseAvatar.scss
index 65e4493f19..cbddd97e18 100644
--- a/res/css/views/avatars/_BaseAvatar.scss
+++ b/res/css/views/avatars/_BaseAvatar.scss
@@ -27,7 +27,6 @@ limitations under the License.
// https://bugzilla.mozilla.org/show_bug.cgi?id=255139
display: inline-block;
user-select: none;
- line-height: 1;
}
.mx_BaseAvatar_initial {
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 2678f7b4ad..e7cfbf6050 100644
--- a/res/css/views/dialogs/_CreateRoomDialog.scss
+++ b/res/css/views/dialogs/_CreateRoomDialog.scss
@@ -65,7 +65,7 @@ limitations under the License.
.mx_CreateRoomDialog_aliasContainer {
display: flex;
// put margin on container so it can collapse with siblings
- margin: 10px 0;
+ margin: 24px 0 10px;
.mx_RoomAliasField {
margin: 0;
@@ -101,10 +101,6 @@ limitations under the License.
margin-left: 30px;
}
- .mx_CreateRoomDialog_topic {
- margin-bottom: 36px;
- }
-
.mx_Dialog_content > .mx_SettingsFlag {
margin-top: 24px;
}
@@ -114,4 +110,3 @@ limitations under the License.
font-size: $font-12px;
}
}
-
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/_ForwardDialog.scss b/res/css/views/dialogs/_ForwardDialog.scss
index 95d7ce74c4..e018f60172 100644
--- a/res/css/views/dialogs/_ForwardDialog.scss
+++ b/res/css/views/dialogs/_ForwardDialog.scss
@@ -36,6 +36,10 @@ limitations under the License.
flex-shrink: 0;
overflow-y: auto;
+ .mx_EventTile[data-layout=bubble] {
+ margin-top: 20px;
+ }
+
div {
pointer-events: none;
}
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/dialogs/_ManageRestrictedJoinRuleDialog.scss b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
new file mode 100644
index 0000000000..91df76675a
--- /dev/null
+++ b/res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
@@ -0,0 +1,150 @@
+/*
+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_ManageRestrictedJoinRuleDialog_wrapper {
+ .mx_Dialog {
+ display: flex;
+ flex-direction: column;
+ }
+}
+
+.mx_ManageRestrictedJoinRuleDialog {
+ width: 480px;
+ color: $primary-fg-color;
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ min-height: 0;
+ height: 60vh;
+
+ .mx_SearchBox {
+ // To match the space around the title
+ margin: 0 0 15px 0;
+ flex-grow: 0;
+ }
+
+ .mx_ManageRestrictedJoinRuleDialog_content {
+ flex-grow: 1;
+ }
+
+ .mx_ManageRestrictedJoinRuleDialog_noResults {
+ display: block;
+ margin-top: 24px;
+ }
+
+ .mx_ManageRestrictedJoinRuleDialog_section {
+ &:not(:first-child) {
+ margin-top: 24px;
+ }
+
+ > h3 {
+ margin: 0;
+ color: $secondary-fg-color;
+ font-size: $font-12px;
+ font-weight: $font-semi-bold;
+ line-height: $font-15px;
+ }
+
+ .mx_ManageRestrictedJoinRuleDialog_entry {
+ display: flex;
+ margin-top: 12px;
+
+ > div {
+ flex-grow: 1;
+ }
+
+ img.mx_RoomAvatar_isSpaceRoom,
+ .mx_RoomAvatar_isSpaceRoom img {
+ border-radius: 4px;
+ }
+
+ .mx_ManageRestrictedJoinRuleDialog_entry_name {
+ margin: 0 8px;
+ font-size: $font-15px;
+ line-height: 30px;
+ flex-grow: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .mx_ManageRestrictedJoinRuleDialog_entry_description {
+ margin-top: 8px;
+ font-size: $font-12px;
+ line-height: $font-15px;
+ color: $tertiary-fg-color;
+ }
+
+ .mx_Checkbox {
+ align-items: center;
+ }
+ }
+ }
+
+ .mx_ManageRestrictedJoinRuleDialog_section_spaces {
+ .mx_BaseAvatar {
+ margin-right: 12px;
+ }
+
+ .mx_BaseAvatar_image {
+ border-radius: 8px;
+ }
+ }
+
+ .mx_ManageRestrictedJoinRuleDialog_section_info {
+ 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_ManageRestrictedJoinRuleDialog_footer {
+ margin-top: 20px;
+
+ .mx_ManageRestrictedJoinRuleDialog_footer_buttons {
+ display: flex;
+ width: max-content;
+ margin-left: auto;
+
+ .mx_AccessibleButton {
+ display: inline-block;
+
+ & + .mx_AccessibleButton {
+ margin-left: 24px;
+ }
+ }
+ }
+ }
+}
diff --git a/res/css/views/elements/_DesktopCapturerSourcePicker.scss b/res/css/views/elements/_DesktopCapturerSourcePicker.scss
index 69dde5925e..49a0a44417 100644
--- a/res/css/views/elements/_DesktopCapturerSourcePicker.scss
+++ b/res/css/views/elements/_DesktopCapturerSourcePicker.scss
@@ -16,57 +16,43 @@ 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 {
+ display: flex;
+ flex-direction: column;
+ margin: 8px;
+ }
-.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;
+ width: 312px;
+ 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;
+ width: 312px;
+ }
}
diff --git a/res/css/views/elements/_Dropdown.scss b/res/css/views/elements/_Dropdown.scss
index 2a2508c17c..3b67e0191e 100644
--- a/res/css/views/elements/_Dropdown.scss
+++ b/res/css/views/elements/_Dropdown.scss
@@ -27,7 +27,7 @@ limitations under the License.
display: flex;
align-items: center;
position: relative;
- border-radius: 3px;
+ border-radius: 4px;
border: 1px solid $strong-input-border-color;
font-size: $font-12px;
user-select: none;
@@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus {
z-index: 2;
margin: 0;
padding: 0px;
- border-radius: 3px;
+ border-radius: 4px;
border: 1px solid $input-focused-border-color;
background-color: $primary-bg-color;
max-height: 200px;
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/elements/_InfoTooltip.scss b/res/css/views/elements/_InfoTooltip.scss
index 5858a60629..5329e7f1f8 100644
--- a/res/css/views/elements/_InfoTooltip.scss
+++ b/res/css/views/elements/_InfoTooltip.scss
@@ -30,5 +30,12 @@ limitations under the License.
mask-position: center;
content: '';
vertical-align: middle;
+}
+
+.mx_InfoTooltip_icon_info::before {
mask-image: url('$(res)/img/element-icons/info.svg');
}
+
+.mx_InfoTooltip_icon_warning::before {
+ mask-image: url('$(res)/img/element-icons/warning.svg');
+}
diff --git a/res/css/views/elements/_ReplyThread.scss b/res/css/views/elements/_ReplyThread.scss
index 44532ea6a7..032cb49359 100644
--- a/res/css/views/elements/_ReplyThread.scss
+++ b/res/css/views/elements/_ReplyThread.scss
@@ -19,8 +19,9 @@ limitations under the License.
margin-left: 0;
margin-right: 0;
margin-bottom: 8px;
- padding-left: 10px;
- border-left: 4px solid $button-bg-color;
+ padding: 0 10px;
+ border-left: 2px solid $button-bg-color;
+ border-radius: 2px;
.mx_ReplyThread_show {
cursor: pointer;
diff --git a/res/css/views/messages/_CallEvent.scss b/res/css/views/messages/_CallEvent.scss
new file mode 100644
index 0000000000..0c1b41ca38
--- /dev/null
+++ b/res/css/views/messages/_CallEvent.scss
@@ -0,0 +1,162 @@
+/*
+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_CallEvent {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+
+ background-color: $dark-panel-bg-color;
+ border-radius: 8px;
+ margin: 10px auto;
+ width: 75%;
+ box-sizing: border-box;
+ height: 60px;
+
+ &.mx_CallEvent_voice {
+ .mx_CallEvent_type_icon::before,
+ .mx_CallEvent_content_button_callBack span::before,
+ .mx_CallEvent_content_button_answer span::before {
+ mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
+ }
+ }
+
+ &.mx_CallEvent_video {
+ .mx_CallEvent_type_icon::before,
+ .mx_CallEvent_content_button_callBack span::before,
+ .mx_CallEvent_content_button_answer span::before {
+ mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+ }
+ }
+
+ &.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;
+ align-items: center;
+ margin-left: 12px;
+
+ .mx_CallEvent_info_basic {
+ display: flex;
+ flex-direction: column;
+ margin-left: 10px; // To match mx_CallEvent
+
+ .mx_CallEvent_sender {
+ font-weight: 600;
+ font-size: 1.5rem;
+ line-height: 1.8rem;
+ margin-bottom: 3px;
+ }
+
+ .mx_CallEvent_type {
+ font-weight: 400;
+ color: $secondary-fg-color;
+ font-size: 1.2rem;
+ line-height: $font-13px;
+ display: flex;
+ align-items: center;
+
+ .mx_CallEvent_type_icon {
+ height: 13px;
+ width: 13px;
+ margin-right: 5px;
+
+ &::before {
+ content: '';
+ position: absolute;
+ height: 13px;
+ width: 13px;
+ background-color: $tertiary-fg-color;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+ }
+ }
+ }
+ }
+
+ .mx_CallEvent_content {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ color: $secondary-fg-color;
+ margin-right: 16px;
+
+ .mx_CallEvent_content_button {
+ height: 24px;
+ padding: 0px 12px;
+ margin-left: 8px;
+
+ 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;
+ mask-size: 16px;
+ width: 16px;
+ height: 16px;
+ margin-right: 8px;
+ }
+ }
+ }
+
+ .mx_CallEvent_content_button_reject span::before {
+ mask-image: url('$(res)/img/element-icons/call/hangup.svg');
+ }
+
+ .mx_CallEvent_content_tooltip {
+ margin-right: 5px;
+ }
+
+ .mx_CallEvent_iconButton {
+ display: inline-flex;
+ margin-right: 8px;
+
+ &::before {
+ content: '';
+
+ height: 16px;
+ width: 16px;
+ background-color: $tertiary-fg-color;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ mask-position: center;
+ }
+ }
+
+ .mx_CallEvent_silence::before {
+ mask-image: url('$(res)/img/voip/silence.svg');
+ }
+
+ .mx_CallEvent_unSilence::before {
+ mask-image: url('$(res)/img/voip/un-silence.svg');
+ }
+ }
+}
diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss
index b91c461ce5..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,11 +60,7 @@ limitations under the License.
}
.mx_MFileBody_info {
- background-color: $message-body-panel-bg-color;
- border-radius: 12px;
- width: 243px; // same width as a playable voice message, accounting for padding
- padding: 6px 12px;
- color: $message-body-panel-fg-color;
+ cursor: pointer;
.mx_MFileBody_info_icon {
background-color: $message-body-panel-icon-bg-color;
diff --git a/res/css/views/messages/_MImageBody.scss b/res/css/views/messages/_MImageBody.scss
index 0a199c1f45..a748435cd8 100644
--- a/res/css/views/messages/_MImageBody.scss
+++ b/res/css/views/messages/_MImageBody.scss
@@ -16,23 +16,15 @@ limitations under the License.
$timelineImageBorderRadius: 4px;
-.mx_MImageBody {
- display: block;
-}
-
.mx_MImageBody_thumbnail {
- position: absolute;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
+ object-fit: contain;
border-radius: $timelineImageBorderRadius;
display: flex;
justify-content: center;
align-items: center;
- > canvas {
+ > div > canvas {
border-radius: $timelineImageBorderRadius;
}
}
diff --git a/res/css/views/messages/_MediaBody.scss b/res/css/views/messages/_MediaBody.scss
index 12e441750c..7f4bfd3fdc 100644
--- a/res/css/views/messages/_MediaBody.scss
+++ b/res/css/views/messages/_MediaBody.scss
@@ -20,9 +20,11 @@ limitations under the License.
.mx_MediaBody {
background-color: $message-body-panel-bg-color;
border-radius: 12px;
+ max-width: 243px; // use max-width instead of width so it fits within right panels
color: $message-body-panel-fg-color;
font-size: $font-14px;
line-height: $font-24px;
-}
+ padding: 6px 12px;
+}
diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss
index e2fafe6c62..69f3c672b7 100644
--- a/res/css/views/messages/_MessageActionBar.scss
+++ b/res/css/views/messages/_MessageActionBar.scss
@@ -107,3 +107,12 @@ limitations under the License.
.mx_MessageActionBar_cancelButton::after {
mask-image: url('$(res)/img/element-icons/trashcan.svg');
}
+
+.mx_MessageActionBar_downloadButton::after {
+ mask-size: 14px;
+ mask-image: url('$(res)/img/download.svg');
+}
+
+.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
+ background-color: transparent; // hide the download icon mask
+}
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 c66f635ffe..1e25deba26 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,17 +38,22 @@ limitations under the License.
padding-top: 0;
}
- &:hover {
+ &::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 {
@@ -80,7 +85,7 @@ limitations under the License.
.mx_MessageActionBar {
right: 0;
- transform: translate3d(50%, 50%, 0);
+ transform: translate3d(90%, 50%, 0);
}
--backgroundColor: $eventbubble-others-bg;
@@ -91,12 +96,17 @@ limitations under the License.
float: right;
> a {
left: auto;
- right: -48px;
+ right: -68px;
}
}
.mx_SenderProfile {
display: none;
}
+
+ .mx_ReplyTile .mx_SenderProfile {
+ display: block;
+ }
+
.mx_ReactionsRow {
float: right;
clear: right;
@@ -126,7 +136,9 @@ limitations under the License.
margin: 0 -12px 0 -9px;
> a {
position: absolute;
- left: -48px;
+ padding: 10px 20px;
+ top: 0;
+ left: -68px;
}
}
@@ -148,12 +160,24 @@ limitations under the License.
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;
+ }
+
&[data-has-reply=true] {
> .mx_EventTile_line {
flex-direction: column;
@@ -204,89 +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;
-
- .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(50%, 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: 0;
- }
- }
-
- .mx_EventListSummary_toggle {
- margin-right: 55px;
- }
-
/* Special layout scenario for "Unable To Decrypt (UTD)" events */
&.mx_EventTile_bad > .mx_EventTile_line {
display: grid;
@@ -321,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: 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 d6ad37f6bb..6e207d674b 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,10 +131,6 @@ $hover-select-border: 4px;
}
}
- &.mx_EventTile_info .mx_EventTile_line {
- padding-left: calc($left-gutter + 18px);
- }
-
&.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
}
@@ -208,43 +203,11 @@ $hover-select-border: 4px;
text-decoration: none;
}
- /* 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
- horizontal scrollbars occasionally appearing, like in
- https://github.com/vector-im/vector-web/issues/1154
- */
- .mx_EventTile_content {
- display: block;
- overflow-y: hidden;
- overflow-x: hidden;
- margin-right: 34px;
- }
-
/* De-zalgoing */
.mx_EventTile_body {
overflow-y: hidden;
}
- /* Spoiler stuff */
- .mx_EventTile_spoiler {
- cursor: pointer;
- }
-
- .mx_EventTile_spoiler_reason {
- color: $event-timestamp-color;
- font-size: $font-11px;
- }
-
- .mx_EventTile_spoiler_content {
- filter: blur(5px) saturate(0.1) sepia(1);
- transition-duration: 0.5s;
- }
-
- .mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
- filter: none;
- }
-
&:hover.mx_EventTile_verified .mx_EventTile_line,
&:hover.mx_EventTile_unverified .mx_EventTile_line,
&:hover.mx_EventTile_unknown .mx_EventTile_line {
@@ -303,10 +266,49 @@ $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
+ horizontal scrollbars occasionally appearing, like in
+ https://github.com/vector-im/vector-web/issues/1154 */
+.mx_EventTile_content {
+ overflow-y: hidden;
+ overflow-x: hidden;
+ margin-right: 34px;
+}
+
+/* Spoiler stuff */
+.mx_EventTile_spoiler {
+ cursor: pointer;
+}
+
+.mx_EventTile_spoiler_reason {
+ color: $event-timestamp-color;
+ font-size: $font-11px;
+}
+
+.mx_EventTile_spoiler_content {
+ filter: blur(5px) saturate(0.1) sepia(1);
+ transition-duration: 0.5s;
+}
+
+.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
+ filter: none;
+}
+
.mx_RoomView_timeline_rr_enabled {
.mx_EventTile:not([data-layout=bubble]) {
@@ -319,6 +321,10 @@ $hover-select-border: 4px;
// 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;
@@ -333,6 +339,13 @@ $hover-select-border: 4px;
.mx_EventTile_msgOption {
grid-column: 2;
}
+
+ &:hover {
+ .mx_EventTile_line {
+ // To avoid bubble events being highlighted
+ background-color: inherit !important;
+ }
+ }
}
.mx_EventTile_readAvatars {
@@ -446,8 +459,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 {
@@ -462,6 +481,10 @@ $hover-select-border: 4px;
background-color: $header-panel-bg-color;
}
+ pre code > * {
+ display: inline-block;
+ }
+
pre {
// have to use overlay rather than auto otherwise Linux and Windows
// Chrome gets very confused about vertical spacing:
@@ -559,6 +582,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/_GroupLayout.scss b/res/css/views/rooms/_GroupLayout.scss
index ddee81a914..ebb7f99e45 100644
--- a/res/css/views/rooms/_GroupLayout.scss
+++ b/res/css/views/rooms/_GroupLayout.scss
@@ -26,6 +26,7 @@ $left-gutter: 64px;
> .mx_EventTile_avatar {
position: absolute;
+ z-index: 9;
}
.mx_MessageTimestamp {
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/_LinkPreviewWidget.scss b/res/css/views/rooms/_LinkPreviewWidget.scss
index 0832337ecd..24900ee14b 100644
--- a/res/css/views/rooms/_LinkPreviewWidget.scss
+++ b/res/css/views/rooms/_LinkPreviewWidget.scss
@@ -19,7 +19,8 @@ limitations under the License.
margin-right: 15px;
margin-bottom: 15px;
display: flex;
- border-left: 4px solid $preview-widget-bar-color;
+ border-left: 2px solid $preview-widget-bar-color;
+ border-radius: 2px;
color: $preview-widget-fg-color;
}
@@ -33,7 +34,7 @@ limitations under the License.
.mx_LinkPreviewWidget_caption {
margin-left: 15px;
flex: 1 1 auto;
- overflow-x: hidden; // cause it to wrap rather than clip
+ overflow: hidden; // cause it to wrap rather than clip
}
.mx_LinkPreviewWidget_title {
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/_SendMessageComposer.scss b/res/css/views/rooms/_SendMessageComposer.scss
index 9f6a8d52ce..4b7eb54188 100644
--- a/res/css/views/rooms/_SendMessageComposer.scss
+++ b/res/css/views/rooms/_SendMessageComposer.scss
@@ -29,8 +29,10 @@ limitations under the License.
display: flex;
flex-direction: column;
// min-height at this level so the mx_BasicMessageComposer_input
- // still stays vertically centered when less than 50px
- min-height: 50px;
+ // still stays vertically centered when less than 55px.
+ // We also set this to ensure the voice message recording widget
+ // doesn't cause a jump.
+ min-height: 55px;
.mx_BasicMessageComposer_input {
padding: 3px 0;
diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss
index 892f5fe744..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;
@@ -47,14 +46,14 @@ limitations under the License.
color: $settings-subsection-fg-color;
font-size: $font-14px;
display: block;
- margin: 10px 100px 10px 0; // Align with the rest of the view
+ margin: 10px 80px 10px 0; // Align with the rest of the view
}
.mx_SettingsTab_section {
margin-bottom: 24px;
.mx_SettingsFlag {
- margin-right: 100px;
+ margin-right: 80px;
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/room/_SecurityRoomSettingsTab.scss b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss
index 23dcc532b2..2aab201352 100644
--- a/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss
+++ b/res/css/views/settings/tabs/room/_SecurityRoomSettingsTab.scss
@@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+.mx_SecurityRoomSettingsTab {
+ .mx_SettingsTab_showAdvanced {
+ padding: 0;
+ margin-bottom: 16px;
+ }
+
+ .mx_SecurityRoomSettingsTab_spacesWithAccess {
+ > h4 {
+ color: $secondary-fg-color;
+ font-weight: $font-semi-bold;
+ font-size: $font-12px;
+ line-height: $font-15px;
+ text-transform: uppercase;
+ }
+
+ > span {
+ font-weight: 500;
+ font-size: $font-14px;
+ line-height: 32px; // matches height of avatar for v-align
+ color: $secondary-fg-color;
+ display: inline-block;
+
+ img.mx_RoomAvatar_isSpaceRoom,
+ .mx_RoomAvatar_isSpaceRoom img {
+ border-radius: 8px;
+ }
+
+ .mx_BaseAvatar {
+ margin-right: 8px;
+ }
+
+ & + span {
+ margin-left: 16px;
+ }
+ }
+ }
+}
+
.mx_SecurityRoomSettingsTab_warning {
display: block;
@@ -26,5 +64,51 @@ limitations under the License.
}
.mx_SecurityRoomSettingsTab_encryptionSection {
- margin-bottom: 25px;
+ padding-bottom: 24px;
+ border-bottom: 1px solid $menu-border-color;
+ margin-bottom: 32px;
+}
+
+.mx_SecurityRoomSettingsTab_upgradeRequired {
+ margin-left: 16px;
+ padding: 4px 16px;
+ border: 1px solid $accent-color;
+ border-radius: 8px;
+ color: $accent-color;
+ font-size: $font-12px;
+ line-height: $font-15px;
+}
+
+.mx_SecurityRoomSettingsTab_joinRule {
+ .mx_RadioButton {
+ padding-top: 16px;
+ margin-bottom: 8px;
+
+ .mx_RadioButton_content {
+ margin-left: 14px;
+ font-weight: $font-semi-bold;
+ font-size: $font-15px;
+ line-height: $font-24px;
+ color: $primary-fg-color;
+ display: block;
+ }
+ }
+
+ > span {
+ display: inline-block;
+ margin-left: 34px;
+ margin-bottom: 16px;
+ font-size: $font-15px;
+ line-height: $font-24px;
+ color: $secondary-fg-color;
+
+ & + .mx_RadioButton {
+ border-top: 1px solid $menu-border-color;
+ }
+ }
+
+ .mx_AccessibleButton_kind_link {
+ padding: 0;
+ font-size: inherit;
+ }
}
diff --git a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
index 94983a60bf..ca5a6f0a66 100644
--- a/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
+++ b/res/css/views/settings/tabs/user/_AppearanceUserSettingsTab.scss
@@ -15,8 +15,7 @@ limitations under the License.
*/
.mx_AppearanceUserSettingsTab_fontSlider,
-.mx_AppearanceUserSettingsTab_fontSlider_preview,
-.mx_AppearanceUserSettingsTab_Layout {
+.mx_AppearanceUserSettingsTab_fontSlider_preview {
@mixin mx_Settings_fullWidthField;
}
@@ -45,6 +44,11 @@ limitations under the License.
border-radius: 10px;
padding: 0 16px 9px 16px;
pointer-events: none;
+ display: flow-root;
+
+ .mx_EventTile[data-layout=bubble] {
+ margin-top: 30px;
+ }
.mx_EventTile_msgOption {
display: none;
@@ -154,13 +158,10 @@ limitations under the License.
.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
display: flex;
flex-direction: row;
+ gap: 24px;
color: $primary-fg-color;
- .mx_AppearanceUserSettingsTab_spacer {
- width: 24px;
- }
-
> .mx_AppearanceUserSettingsTab_Layout_RadioButton {
flex-grow: 0;
flex-shrink: 1;
@@ -210,6 +211,21 @@ limitations under the License.
.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 {
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/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index 205d431752..104e2993d8 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -67,7 +67,26 @@ 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 {
+ // We don't want to collide with the call controls that have 52px of height
+ padding-bottom: 52px;
+ background-color: $inverted-bg-color;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ }
+
+ &.mx_VideoFeed_video {
+ background-color: #000;
+ }
+ }
}
.mx_CallView_voice {
@@ -260,7 +279,7 @@ limitations under the License.
max-width: 240px;
}
-.mx_CallView_header_phoneIcon {
+.mx_CallView_header_callTypeIcon {
display: inline-block;
margin-right: 6px;
height: 16px;
@@ -274,12 +293,19 @@ limitations under the License.
height: 16px;
width: 16px;
- background-color: $warning-color;
+ background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
+ }
+
+ &.mx_CallView_header_callTypeIcon_voice::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
+
+ &.mx_CallView_header_callTypeIcon_video::before {
+ mask-image: url('$(res)/img/element-icons/call/video-call.svg');
+ }
}
.mx_CallView_callControls {
@@ -287,9 +313,9 @@ limitations under the License.
display: flex;
justify-content: center;
bottom: 5px;
- width: 100%;
opacity: 1;
transition: opacity 0.5s;
+ z-index: 200; // To be above _all_ feeds
}
.mx_CallView_callControls_hidden {
@@ -297,10 +323,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 +362,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 +391,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 +422,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/_CallViewSidebar.scss b/res/css/views/voip/_CallViewSidebar.scss
new file mode 100644
index 0000000000..79bf3cbf09
--- /dev/null
+++ b/res/css/views/voip/_CallViewSidebar.scss
@@ -0,0 +1,52 @@
+/*
+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 {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ aspect-ratio: 16 / 9;
+ }
+ }
+
+ &.mx_CallViewSidebar_pipMode {
+ top: 16px;
+ bottom: unset;
+ justify-content: flex-end;
+ gap: 4px;
+ }
+}
diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss
index 4a3fbdf597..07a4a0e530 100644
--- a/res/css/views/voip/_VideoFeed.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -14,32 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_VideoFeed_voice {
- background-color: $inverted-bg-color;
-}
-
-
-.mx_VideoFeed_remote {
- width: 100%;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
-
- &.mx_VideoFeed_video {
- background-color: #000;
- }
-}
-
-.mx_VideoFeed_local {
- max-width: 25%;
- max-height: 25%;
- position: absolute;
- right: 10px;
- top: 10px;
- z-index: 100;
+.mx_VideoFeed {
border-radius: 4px;
+
+ &.mx_VideoFeed_voice {
+ background-color: $inverted-bg-color;
+ }
+
&.mx_VideoFeed_video {
background-color: transparent;
}
diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2
index a52e5a3800..128aac8139 100644
Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-colr.woff2 differ
diff --git a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2
index 660a93193d..a95e89c094 100644
Binary files a/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 and b/res/fonts/Twemoji_Mozilla/TwemojiMozilla-sbix.woff2 differ
diff --git a/res/img/element-icons/warning.svg b/res/img/element-icons/warning.svg
new file mode 100644
index 0000000000..eef5193140
--- /dev/null
+++ b/res/img/element-icons/warning.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/res/img/feather-customised/globe.svg b/res/img/feather-customised/globe.svg
deleted file mode 100644
index 8af7dc41dc..0000000000
--- a/res/img/feather-customised/globe.svg
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
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 2a4ebff034..655492661c 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -209,8 +209,8 @@ $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: #21262C; // "Separator"
-$message-body-panel-icon-bg-color: $tertiary-fg-color;
+$message-body-panel-icon-fg-color: $secondary-fg-color;
+$message-body-panel-icon-bg-color: #21262C; // "System Dark"
$voice-record-stop-border-color: $quaternary-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
@@ -295,3 +295,11 @@ $eventbubble-reply-color: #C1C6CD;
.hljs-tag {
color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
}
+
+.hljs-addition {
+ background: #1a4b59;
+}
+
+.hljs-deletion {
+ background: #53232a;
+}
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 555ef4f66c..0c0197cfb0 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -207,8 +207,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #394049;
-$message-body-panel-icon-fg-color: $primary-bg-color;
-$message-body-panel-icon-bg-color: $secondary-fg-color;
+$message-body-panel-icon-fg-color: $secondary-fg-color;
+$message-body-panel-icon-bg-color: #21262C;
// See non-legacy dark for variable information
$voice-record-stop-border-color: #6F7882;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index f349a804a8..b7d45452ff 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -331,7 +331,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: $primary-bg-color;
+$message-body-panel-icon-bg-color: #F4F6FA;
// See non-legacy _light for variable information
$voice-record-stop-symbol-color: #ff4b55;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index ef5f4d8c86..32722515d8 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -327,7 +327,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: $primary-bg-color;
+$message-body-panel-icon-bg-color: #F4F6FA;
// These two don't change between themes. They are the $warning-color, but we don't
// want custom themes to affect them by accident.
diff --git a/src/@types/common.ts b/src/@types/common.ts
index 1fb9ba4303..36ef7a9ace 100644
--- a/src/@types/common.ts
+++ b/src/@types/common.ts
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { JSXElementConstructor } from "react";
+import React, { JSXElementConstructor } from "react";
// Based on https://stackoverflow.com/a/53229857/3532235
export type Without = {[P in Exclude]?: never};
@@ -22,3 +22,4 @@ export type XOR = (T | U) extends object ? (Without & U) | (Without<
export type Writeable = { -readonly [P in keyof T]: T[P] };
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor;
+export type ReactAnyComponent = React.Component | React.ExoticComponent;
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 051e865464..9d6bc2c6fb 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -92,6 +92,7 @@ declare global {
mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore;
+ mxOnRecaptchaLoaded?: () => void;
}
interface Document {
@@ -116,7 +117,7 @@ declare global {
}
interface StorageEstimate {
- usageDetails?: {[key: string]: number};
+ usageDetails?: { [key: string]: number };
}
interface HTMLAudioElement {
@@ -187,6 +188,21 @@ declare global {
parameterDescriptors?: AudioParamDescriptor[];
}
);
+
+ // eslint-disable-next-line no-var
+ var grecaptcha:
+ | undefined
+ | {
+ reset: (id: string) => void;
+ render: (
+ divId: string,
+ options: {
+ sitekey: string;
+ callback: (response: string) => void;
+ },
+ ) => string;
+ isReady: () => boolean;
+ };
}
/* eslint-enable @typescript-eslint/naming-convention */
diff --git a/src/@types/svg.d.ts b/src/@types/svg.d.ts
new file mode 100644
index 0000000000..96f671c52f
--- /dev/null
+++ b/src/@types/svg.d.ts
@@ -0,0 +1,20 @@
+/*
+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.
+*/
+
+declare module "*.svg" {
+ const path: string;
+ export default path;
+}
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 489d28e26b..e7c1dda54f 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -56,7 +56,6 @@ 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';
@@ -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';
@@ -99,7 +97,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
// (and store the ID of their native room)
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
-export enum AudioID {
+enum AudioID {
Ring = 'ringAudio',
Ringback = 'ringbackAudio',
CallEnd = 'callendAudio',
@@ -129,19 +127,15 @@ 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 {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
+ SilencedCallsChanged = "silenced_calls_changed",
}
export default class CallHandler extends EventEmitter {
@@ -164,6 +158,8 @@ export default class CallHandler extends EventEmitter {
// do the async lookup when we get new information and then store these mappings here
private assertedIdentityNativeUsers = new Map();
+ private silencedCalls = new Set(); // callIds
+
static sharedInstance() {
if (!window.mxCallHandler) {
window.mxCallHandler = new CallHandler();
@@ -224,6 +220,33 @@ export default class CallHandler extends EventEmitter {
}
}
+ public silenceCall(callId: string) {
+ this.silencedCalls.add(callId);
+ this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
+
+ // Don't pause audio if we have calls which are still ringing
+ if (this.areAnyCallsUnsilenced()) return;
+ this.pause(AudioID.Ring);
+ }
+
+ public unSilenceCall(callId: string) {
+ this.silencedCalls.delete(callId);
+ this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
+ this.play(AudioID.Ring);
+ }
+
+ public isCallSilenced(callId: string): boolean {
+ return this.silencedCalls.has(callId);
+ }
+
+ /**
+ * Returns true if there is at least one unsilenced call
+ * @returns {boolean}
+ */
+ private areAnyCallsUnsilenced(): boolean {
+ return this.calls.size > this.silencedCalls.size;
+ }
+
private async checkProtocols(maxTries) {
try {
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
@@ -301,6 +324,13 @@ export default class CallHandler extends EventEmitter {
}, true);
};
+ public getCallById(callId: string): MatrixCall {
+ for (const call of this.calls.values()) {
+ if (call.callId === callId) return call;
+ }
+ return null;
+ }
+
getCallForRoom(roomId: string): MatrixCall {
return this.calls.get(roomId) || null;
}
@@ -441,6 +471,10 @@ export default class CallHandler extends EventEmitter {
break;
}
+ if (newState !== CallState.Ringing) {
+ this.silencedCalls.delete(call.callId);
+ }
+
switch (newState) {
case CallState.Ringing:
this.play(AudioID.Ring);
@@ -450,28 +484,18 @@ export default class CallHandler extends EventEmitter {
break;
case CallState.Ended:
{
- Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
+ 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");
}
@@ -480,7 +504,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"),
@@ -697,25 +721,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);
}
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index a37b7f0ac9..af5d2b3019 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import linkifyMatrix from './linkify-matrix';
import SettingsStore from './settings/SettingsStore';
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
-import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji";
+import { getEmojiFromUnicode } from "./emoji";
import ReplyThread from "./components/views/elements/ReplyThread";
import { mediaFromMxc } from "./customisations/Media";
@@ -57,7 +57,7 @@ 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'];
+export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix'];
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
@@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean {
* @return {String} The shortcode (such as :thumbup:)
*/
export function unicodeToShortcode(char: string): string {
- const data = getEmojiFromUnicode(char);
- return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
-}
-
-/**
- * Returns the unicode character for an emoji shortcode
- *
- * @param {String} shortcode The shortcode (such as :thumbup:)
- * @return {String} The emoji character; null if none exists
- */
-export function shortcodeToUnicode(shortcode: string): string {
- shortcode = shortcode.slice(1, shortcode.length - 1);
- const data = SHORTCODE_TO_EMOJI.get(shortcode);
- return data ? data.unicode : null;
+ const shortcodes = getEmojiFromUnicode(char)?.shortcodes;
+ return shortcodes?.length ? `:${shortcodes[0]}:` : '';
}
export function processHtmlForSending(html: string): string {
diff --git a/src/Registration.js b/src/Registration.js
index 70dcd38454..c59d244149 100644
--- a/src/Registration.js
+++ b/src/Registration.js
@@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) {
description: _t("Use your account or create a new one to continue."),
button: _t("Create Account"),
extraButtons: [
- {
- modal.close();
- dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
- }}>{ _t('Sign In') } ,
+ {
+ modal.close();
+ dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
+ }}
+ >
+ { _t('Sign In') }
+ ,
],
onFinished: (proceed) => {
if (proceed) {
diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 9f5ac83a56..b4deb6d8c4 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress';
import { abbreviateUrl } from './utils/UrlUtils';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
-import { inviteUsersToRoom } from "./RoomInvite";
import { WidgetType } from "./widgets/WidgetType";
import { Jitsi } from "./widgets/Jitsi";
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
@@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature";
import { CHAT_EFFECTS } from "./effects";
import CallHandler from "./CallHandler";
import { guessAndSetDMRoom } from "./Rooms";
+import { upgradeRoom } from './utils/RoomUpgrade';
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
import ErrorDialog from './components/views/dialogs/ErrorDialog';
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
@@ -277,50 +277,8 @@ export const Commands = [
/*isPriority=*/false, /*isStatic=*/true);
return success(finished.then(async ([resp]) => {
- if (!resp.continue) return;
-
- let checkForUpgradeFn;
- try {
- const upgradePromise = cli.upgradeRoom(roomId, args);
-
- // We have to wait for the js-sdk to give us the room back so
- // we can more effectively abuse the MultiInviter behaviour
- // which heavily relies on the Room object being available.
- if (resp.invite) {
- checkForUpgradeFn = async (newRoom) => {
- // The upgradePromise should be done by the time we await it here.
- const { replacement_room: newRoomId } = await upgradePromise;
- if (newRoom.roomId !== newRoomId) return;
-
- const toInvite = [
- ...room.getMembersWithMembership("join"),
- ...room.getMembersWithMembership("invite"),
- ].map(m => m.userId).filter(m => m !== cli.getUserId());
-
- if (toInvite.length > 0) {
- // Errors are handled internally to this function
- await inviteUsersToRoom(newRoomId, toInvite);
- }
-
- cli.removeListener('Room', checkForUpgradeFn);
- };
- cli.on('Room', checkForUpgradeFn);
- }
-
- // We have to await after so that the checkForUpgradesFn has a proper reference
- // to the new room's ID.
- await upgradePromise;
- } catch (e) {
- console.error(e);
-
- if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
-
- Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
- title: _t('Error upgrading room'),
- description: _t(
- 'Double check that your server supports the room version chosen and try again.'),
- });
- }
+ if (!resp?.continue) return;
+ await upgradeRoom(room, args, resp.invite);
}));
}
return reject(this.getUsage());
diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx
index 0056a37c85..7bad8eb50e 100644
--- a/src/TextForEvent.tsx
+++ b/src/TextForEvent.tsx
@@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
-import { MatrixClientPeg } from './MatrixClientPeg';
import { _t } from './languageHandler';
import * as Roles from './Roles';
import { isValid3pidInvite } from "./RoomInvite";
@@ -318,90 +317,6 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
});
}
-function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
- return () => {
- const senderName = event.sender ? event.sender.name : _t('Someone');
- const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
- return _t('%(senderName)s answered the call.', { senderName }) + ' ' + supported;
- };
-}
-
-function textForCallHangupEvent(event: MatrixEvent): () => string | null {
- const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
- const eventContent = event.getContent();
- let getReason = () => "";
- if (!MatrixClientPeg.get().supportsVoip()) {
- getReason = () => _t('(not supported by this browser)');
- } else if (eventContent.reason) {
- if (eventContent.reason === "ice_failed") {
- // We couldn't establish a connection at all
- getReason = () => _t('(could not connect media)');
- } else if (eventContent.reason === "ice_timeout") {
- // We established a connection but it died
- getReason = () => _t('(connection failed)');
- } else if (eventContent.reason === "user_media_failed") {
- // The other side couldn't open capture devices
- getReason = () => _t("(their device couldn't start the camera / microphone)");
- } else if (eventContent.reason === "unknown_error") {
- // An error code the other side doesn't have a way to express
- // (as opposed to an error code they gave but we don't know about,
- // in which case we show the error code)
- getReason = () => _t("(an error occurred)");
- } else if (eventContent.reason === "invite_timeout") {
- getReason = () => _t('(no answer)');
- } else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
- // 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)
- getReason = () => '';
- } else {
- getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason });
- }
- }
- return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
-}
-
-function textForCallRejectEvent(event: MatrixEvent): () => string | null {
- return () => {
- const senderName = event.sender ? event.sender.name : _t('Someone');
- return _t('%(senderName)s declined the call.', { senderName });
- };
-}
-
-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 textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender();
@@ -652,10 +567,6 @@ interface IHandlers {
const handlers: IHandlers = {
'm.room.message': textForMessageEvent,
- 'm.call.invite': textForCallInviteEvent,
- 'm.call.answer': textForCallAnswerEvent,
- 'm.call.hangup': textForCallHangupEvent,
- 'm.call.reject': textForCallRejectEvent,
};
const stateHandlers: IHandlers = {
diff --git a/src/UserAddress.ts b/src/UserAddress.ts
index a2c546deb7..248814aa01 100644
--- a/src/UserAddress.ts
+++ b/src/UserAddress.ts
@@ -14,35 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import PropTypes from "prop-types";
-
const emailRegex = /^\S+@\S+\.\S+$/;
const mxUserIdRegex = /^@\S+:\S+$/;
const mxRoomIdRegex = /^!\S+:\S+$/;
-export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
-
export enum AddressType {
Email = "email",
MatrixUserId = "mx-user-id",
MatrixRoomId = "mx-room-id",
}
+export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId];
+
// PropType definition for an object describing
// an address that can be invited to a room (which
// could be a third party identifier or a matrix ID)
// along with some additional information about the
// address / target.
-export const UserAddressType = PropTypes.shape({
- addressType: PropTypes.oneOf(addressTypes).isRequired,
- address: PropTypes.string.isRequired,
- displayName: PropTypes.string,
- avatarMxc: PropTypes.string,
+export interface IUserAddress {
+ addressType: AddressType;
+ address: string;
+ displayName?: string;
+ avatarMxc?: string;
// true if the address is known to be a valid address (eg. is a real
// user we've seen) or false otherwise (eg. is just an address the
// user has entered)
- isKnown: PropTypes.bool,
-});
+ isKnown?: boolean;
+}
export function getAddressType(inputText: string): AddressType | null {
if (emailRegex.test(inputText)) {
diff --git a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx
similarity index 77%
rename from src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js
rename to src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx
index e1c2b7b202..4d8f5e5663 100644
--- a/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.js
+++ b/src/async-components/views/dialogs/eventindex/DisableEventIndexDialog.tsx
@@ -15,8 +15,10 @@ limitations under the License.
*/
import React from 'react';
-import * as sdk from '../../../../index';
-import PropTypes from 'prop-types';
+
+import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
+import Spinner from "../../../../components/views/elements/Spinner";
+import DialogButtons from "../../../../components/views/elements/DialogButtons";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from '../../../../languageHandler';
@@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore";
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import { Action } from "../../../../dispatcher/actions";
import { SettingLevel } from "../../../../settings/SettingLevel";
+interface IProps {
+ onFinished: (success: boolean) => void;
+}
+
+interface IState {
+ disabling: boolean;
+}
/*
* Allows the user to disable the Event Index.
*/
-export default class DisableEventIndexDialog extends React.Component {
- static propTypes = {
- onFinished: PropTypes.func.isRequired,
- }
-
- constructor(props) {
+export default class DisableEventIndexDialog extends React.Component {
+ constructor(props: IProps) {
super(props);
-
this.state = {
disabling: false,
};
}
- _onDisable = async () => {
+ private onDisable = async (): Promise => {
this.setState({
disabling: true,
});
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex();
- this.props.onFinished();
+ this.props.onFinished(true);
dis.fire(Action.ViewUserSettings);
- }
-
- render() {
- const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
- const Spinner = sdk.getComponent('elements.Spinner');
- const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
+ };
+ public render(): React.ReactNode {
return (
{ _t("If disabled, messages from encrypted rooms won't appear in search results.") }
{ this.state.disabling ? :
}
{
- Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
- import("./DisableEventIndexDialog"),
+ const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
+ Modal.createTrackedDialog("Disable message search", "Disable message search",
+ DisableEventIndexDialog,
null, null, /* priority = */ false, /* static = */ true,
);
};
diff --git a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
index 412194ab43..2cef1c0e41 100644
--- a/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
+++ b/src/async-components/views/dialogs/security/CreateKeyBackupDialog.js
@@ -269,7 +269,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
{ _t("Advanced") }
-
+
{ _t("Set up with a Security Key") }
diff --git a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
index aa78d68830..641df4f897 100644
--- a/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
+++ b/src/async-components/views/dialogs/security/CreateSecretStorageDialog.js
@@ -474,7 +474,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
outlined
>
-
+
{ _t("Generate a Security Key") }
{ _t("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }
@@ -493,7 +493,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
outlined
>
-
+
{ _t("Enter a Security Phrase") }
{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }
@@ -701,7 +701,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
{ this._recoveryKey.encodedPrivateKey }
-
diff --git a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js
index 0435d81968..dbed9f3968 100644
--- a/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js
+++ b/src/async-components/views/dialogs/security/ExportE2eKeysDialog.js
@@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
-
@@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
-
diff --git a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js
index 6017d07047..0936ad696d 100644
--- a/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js
+++ b/src/async-components/views/dialogs/security/ImportE2eKeysDialog.js
@@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
-
diff --git a/src/audio/ManagedPlayback.ts b/src/audio/ManagedPlayback.ts
new file mode 100644
index 0000000000..bff6ce7088
--- /dev/null
+++ b/src/audio/ManagedPlayback.ts
@@ -0,0 +1,37 @@
+/*
+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 { DEFAULT_WAVEFORM, Playback } from "./Playback";
+import { PlaybackManager } from "./PlaybackManager";
+
+/**
+ * A managed playback is a Playback instance that is guided by a PlaybackManager.
+ */
+export class ManagedPlayback extends Playback {
+ public constructor(private manager: PlaybackManager, buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) {
+ super(buf, seedWaveform);
+ }
+
+ public async play(): Promise {
+ this.manager.playOnly(this);
+ return super.play();
+ }
+
+ public destroy() {
+ this.manager.destroyPlaybackInstance(this);
+ super.destroy();
+ }
+}
diff --git a/src/voice/Playback.ts b/src/audio/Playback.ts
similarity index 72%
rename from src/voice/Playback.ts
rename to src/audio/Playback.ts
index 1a1ee54466..33d346629a 100644
--- a/src/voice/Playback.ts
+++ b/src/audio/Playback.ts
@@ -32,7 +32,7 @@ export enum PlaybackState {
export const PLAYBACK_WAVEFORM_SAMPLES = 39;
const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120]
-const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
+export const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
function makePlaybackWaveform(input: number[]): number[] {
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
@@ -59,9 +59,10 @@ export class Playback extends EventEmitter implements IDestroyable {
public readonly thumbnailWaveform: number[];
private readonly context: AudioContext;
- private source: AudioBufferSourceNode;
+ private source: AudioBufferSourceNode | MediaElementAudioSourceNode;
private state = PlaybackState.Decoding;
private audioBuf: AudioBuffer;
+ private element: HTMLAudioElement;
private resampledWaveform: number[];
private waveformObservable = new SimpleObservable();
private readonly clock: PlaybackClock;
@@ -129,36 +130,64 @@ export class Playback extends EventEmitter implements IDestroyable {
this.removeAllListeners();
this.clock.destroy();
this.waveformObservable.close();
+ if (this.element) {
+ URL.revokeObjectURL(this.element.src);
+ this.element.remove();
+ }
}
public async prepare() {
- // Safari compat: promise API not supported on this function
- this.audioBuf = await new Promise((resolve, reject) => {
- this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
- // This error handler is largely for Safari as well, which doesn't support Opus/Ogg
- // very well.
- console.error("Error decoding recording: ", e);
- console.warn("Trying to re-encode to WAV instead...");
+ // The point where we use an audio element is fairly arbitrary, though we don't want
+ // it to be too low. As of writing, voice messages want to show a waveform but audio
+ // messages do not. Using an audio element means we can't show a waveform preview, so
+ // we try to target the difference between a voice message file and large audio file.
+ // Overall, the point of this is to avoid memory-related issues due to storing a massive
+ // audio buffer in memory, as that can balloon to far greater than the input buffer's
+ // byte length.
+ if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb
+ console.log("Audio file too large: processing through element");
+ this.element = document.createElement("AUDIO") as HTMLAudioElement;
+ const prom = new Promise((resolve, reject) => {
+ this.element.onloadeddata = () => resolve(null);
+ this.element.onerror = (e) => reject(e);
+ });
+ this.element.src = URL.createObjectURL(new Blob([this.buf]));
+ await prom; // make sure the audio element is ready for us
+ } else {
+ // Safari compat: promise API not supported on this function
+ this.audioBuf = await new Promise((resolve, reject) => {
+ this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
+ try {
+ // This error handler is largely for Safari as well, which doesn't support Opus/Ogg
+ // very well.
+ console.error("Error decoding recording: ", e);
+ console.warn("Trying to re-encode to WAV instead...");
- const wav = await decodeOgg(this.buf);
+ const wav = await decodeOgg(this.buf);
- // noinspection ES6MissingAwait - not needed when using callbacks
- this.context.decodeAudioData(wav, b => resolve(b), e => {
- console.error("Still failed to decode recording: ", e);
- reject(e);
+ // noinspection ES6MissingAwait - not needed when using callbacks
+ this.context.decodeAudioData(wav, b => resolve(b), e => {
+ console.error("Still failed to decode recording: ", e);
+ reject(e);
+ });
+ } catch (e) {
+ console.error("Caught decoding error:", e);
+ reject(e);
+ }
});
});
- });
- // Update the waveform to the real waveform once we have channel data to use. We don't
- // exactly trust the user-provided waveform to be accurate...
- const waveform = Array.from(this.audioBuf.getChannelData(0));
- this.resampledWaveform = makePlaybackWaveform(waveform);
+ // Update the waveform to the real waveform once we have channel data to use. We don't
+ // exactly trust the user-provided waveform to be accurate...
+ const waveform = Array.from(this.audioBuf.getChannelData(0));
+ this.resampledWaveform = makePlaybackWaveform(waveform);
+ }
+
this.waveformObservable.update(this.resampledWaveform);
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update
- this.clock.durationSeconds = this.audioBuf.duration;
+ this.clock.durationSeconds = this.element ? this.element.duration : this.audioBuf.duration;
}
private onPlaybackEnd = async () => {
@@ -171,7 +200,11 @@ export class Playback extends EventEmitter implements IDestroyable {
if (this.state === PlaybackState.Stopped) {
this.disconnectSource();
this.makeNewSourceBuffer();
- this.source.start();
+ if (this.element) {
+ await this.element.play();
+ } else {
+ (this.source as AudioBufferSourceNode).start();
+ }
}
// We use the context suspend/resume functions because it allows us to pause a source
@@ -182,13 +215,21 @@ export class Playback extends EventEmitter implements IDestroyable {
}
private disconnectSource() {
+ if (this.element) return; // leave connected, we can (and must) re-use it
this.source?.disconnect();
this.source?.removeEventListener("ended", this.onPlaybackEnd);
}
private makeNewSourceBuffer() {
- this.source = this.context.createBufferSource();
- this.source.buffer = this.audioBuf;
+ if (this.element && this.source) return; // leave connected, we can (and must) re-use it
+
+ if (this.element) {
+ this.source = this.context.createMediaElementSource(this.element);
+ } else {
+ this.source = this.context.createBufferSource();
+ this.source.buffer = this.audioBuf;
+ }
+
this.source.addEventListener("ended", this.onPlaybackEnd);
this.source.connect(this.context.destination);
}
@@ -241,7 +282,11 @@ export class Playback extends EventEmitter implements IDestroyable {
// when it comes time to the user hitting play. After a couple jumps, the user
// will have desynced the clock enough to be about 10-15 seconds off, while this
// keeps it as close to perfect as humans can perceive.
- this.source.start(now, timeSeconds);
+ if (this.element) {
+ this.element.currentTime = timeSeconds;
+ } else {
+ (this.source as AudioBufferSourceNode).start(now, timeSeconds);
+ }
// Dev note: it's critical that the code gap between `this.source.start()` and
// `this.pause()` is as small as possible: we do not want to delay *anything*
diff --git a/src/voice/PlaybackClock.ts b/src/audio/PlaybackClock.ts
similarity index 93%
rename from src/voice/PlaybackClock.ts
rename to src/audio/PlaybackClock.ts
index e3f41930de..712d1bfa94 100644
--- a/src/voice/PlaybackClock.ts
+++ b/src/audio/PlaybackClock.ts
@@ -103,8 +103,8 @@ export class PlaybackClock implements IDestroyable {
* @param {MatrixEvent} event The event to use for placeholders.
*/
public populatePlaceholdersFrom(event: MatrixEvent) {
- const durationSeconds = Number(event.getContent()['info']?.['duration']);
- if (Number.isFinite(durationSeconds)) this.placeholderDuration = durationSeconds;
+ const durationMs = Number(event.getContent()['info']?.['duration']);
+ if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
}
/**
@@ -132,6 +132,10 @@ export class PlaybackClock implements IDestroyable {
public flagStop() {
this.stopped = true;
+
+ // Reset the clock time now so that the update going out will trigger components
+ // to check their seek/position information (alongside the clock).
+ this.clipStart = this.context.currentTime;
}
public syncTo(contextTime: number, clipTime: number) {
diff --git a/src/audio/PlaybackManager.ts b/src/audio/PlaybackManager.ts
new file mode 100644
index 0000000000..58fa61df56
--- /dev/null
+++ b/src/audio/PlaybackManager.ts
@@ -0,0 +1,54 @@
+/*
+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 { DEFAULT_WAVEFORM, Playback } from "./Playback";
+import { ManagedPlayback } from "./ManagedPlayback";
+
+/**
+ * Handles management of playback instances to ensure certain functionality, like
+ * one playback operating at any one time.
+ */
+export class PlaybackManager {
+ private static internalInstance: PlaybackManager;
+
+ private instances: ManagedPlayback[] = [];
+
+ public static get instance(): PlaybackManager {
+ if (!PlaybackManager.internalInstance) {
+ PlaybackManager.internalInstance = new PlaybackManager();
+ }
+ return PlaybackManager.internalInstance;
+ }
+
+ /**
+ * Stops all other playback instances. If no playback is provided, all instances
+ * are stopped.
+ * @param playback Optional. The playback to leave untouched.
+ */
+ public playOnly(playback?: Playback) {
+ this.instances.filter(p => p !== playback).forEach(p => p.stop());
+ }
+
+ public destroyPlaybackInstance(playback: ManagedPlayback) {
+ this.instances = this.instances.filter(p => p !== playback);
+ }
+
+ public createPlaybackInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback {
+ const instance = new ManagedPlayback(this, buf, waveform);
+ this.instances.push(instance);
+ return instance;
+ }
+}
diff --git a/src/voice/RecorderWorklet.ts b/src/audio/RecorderWorklet.ts
similarity index 100%
rename from src/voice/RecorderWorklet.ts
rename to src/audio/RecorderWorklet.ts
diff --git a/src/voice/VoiceRecording.ts b/src/audio/VoiceRecording.ts
similarity index 97%
rename from src/voice/VoiceRecording.ts
rename to src/audio/VoiceRecording.ts
index 536283689a..efd616e5ae 100644
--- a/src/voice/VoiceRecording.ts
+++ b/src/audio/VoiceRecording.ts
@@ -333,12 +333,17 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
if (this.lastUpload) return this.lastUpload;
- this.emit(RecordingState.Uploading);
- const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], {
- type: this.contentType,
- }));
- this.lastUpload = { mxc, encrypted };
- this.emit(RecordingState.Uploaded);
+ try {
+ this.emit(RecordingState.Uploading);
+ const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], {
+ type: this.contentType,
+ }));
+ this.lastUpload = { mxc, encrypted };
+ this.emit(RecordingState.Uploaded);
+ } catch (e) {
+ this.emit(RecordingState.Ended);
+ throw e;
+ }
return this.lastUpload;
}
}
diff --git a/src/voice/compat.ts b/src/audio/compat.ts
similarity index 100%
rename from src/voice/compat.ts
rename to src/audio/compat.ts
diff --git a/src/voice/consts.ts b/src/audio/consts.ts
similarity index 100%
rename from src/voice/consts.ts
rename to src/audio/consts.ts
diff --git a/src/autocomplete/EmojiProvider.tsx b/src/autocomplete/EmojiProvider.tsx
index 2fc77e9a17..d3175edbdb 100644
--- a/src/autocomplete/EmojiProvider.tsx
+++ b/src/autocomplete/EmojiProvider.tsx
@@ -25,7 +25,6 @@ import { PillCompletion } from './Components';
import { ICompletion, ISelectionRange } from './Autocompleter';
import { uniq, sortBy } from 'lodash';
import SettingsStore from "../settings/SettingsStore";
-import { shortcodeToUnicode } from '../HtmlUtils';
import { EMOJI, IEmoji } from '../emoji';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
@@ -36,20 +35,18 @@ const LIMIT = 20;
// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
-interface IEmojiShort {
+interface ISortedEmoji {
emoji: IEmoji;
- shortname: string;
_orderBy: number;
}
-const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => {
+const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
if (a.group === b.group) {
return a.order - b.order;
}
return a.group - b.group;
}).map((emoji, index) => ({
emoji,
- shortname: `:${emoji.shortcodes[0]}:`,
// Include the index so that we can preserve the original order
_orderBy: index,
}));
@@ -64,20 +61,18 @@ function score(query, space) {
}
export default class EmojiProvider extends AutocompleteProvider {
- matcher: QueryMatcher;
- nameMatcher: QueryMatcher;
+ matcher: QueryMatcher;
+ nameMatcher: QueryMatcher;
constructor() {
super(EMOJI_REGEX);
- this.matcher = new QueryMatcher(EMOJI_SHORTNAMES, {
- keys: ['emoji.emoticon', 'shortname'],
- funcs: [
- (o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
- ],
+ this.matcher = new QueryMatcher(SORTED_EMOJI, {
+ keys: ['emoji.emoticon'],
+ funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)],
// For matching against ascii equivalents
shouldMatchWordsOnly: false,
});
- this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
+ this.nameMatcher = new QueryMatcher(SORTED_EMOJI, {
keys: ['emoji.annotation'],
// For removing punctuation
shouldMatchWordsOnly: true,
@@ -105,34 +100,33 @@ export default class EmojiProvider extends AutocompleteProvider {
const sorters = [];
// make sure that emoticons come first
- sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
+ sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
- // then sort by score (Infinity if matchedString not in shortname)
- sorters.push((c) => score(matchedString, c.shortname));
+ // then sort by score (Infinity if matchedString not in shortcode)
+ sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
// then sort by max score of all shortcodes, trim off the `:`
- sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s))));
- // If the matchedString is not empty, sort by length of shortname. Example:
+ sorters.push(c => Math.min(
+ ...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
+ ));
+ // If the matchedString is not empty, sort by length of shortcode. Example:
// matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...]
if (matchedString.length > 1) {
- sorters.push((c) => c.shortname.length);
+ sorters.push(c => c.emoji.shortcodes[0].length);
}
// Finally, sort by original ordering
- sorters.push((c) => c._orderBy);
+ sorters.push(c => c._orderBy);
completions = sortBy(uniq(completions), sorters);
- completions = completions.map(({ shortname }) => {
- const unicode = shortcodeToUnicode(shortname);
- return {
- completion: unicode,
- component: (
-
- { unicode }
-
- ),
- range,
- };
- }).slice(0, LIMIT);
+ completions = completions.map(c => ({
+ completion: c.emoji.unicode,
+ component: (
+
+ { c.emoji.unicode }
+
+ ),
+ range,
+ })).slice(0, LIMIT);
}
return completions;
}
diff --git a/src/components/structures/CallEventGrouper.ts b/src/components/structures/CallEventGrouper.ts
new file mode 100644
index 0000000000..ce3b530858
--- /dev/null
+++ b/src/components/structures/CallEventGrouper.ts
@@ -0,0 +1,153 @@
+/*
+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.
+*/
+
+import { EventType } from "matrix-js-sdk/src/@types/event";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
+import CallHandler, { CallHandlerEvent } from '../../CallHandler';
+import { EventEmitter } from 'events';
+import { MatrixClientPeg } from "../../MatrixClientPeg";
+import defaultDispatcher from "../../dispatcher/dispatcher";
+
+export enum CallEventGrouperEvent {
+ StateChanged = "state_changed",
+ SilencedChanged = "silenced_changed",
+}
+
+const SUPPORTED_STATES = [
+ CallState.Connected,
+ CallState.Connecting,
+ CallState.Ringing,
+];
+
+export enum CustomCallState {
+ Missed = "missed",
+}
+
+export default class CallEventGrouper extends EventEmitter {
+ private events: Set = new Set();
+ private call: MatrixCall;
+ public state: CallState | CustomCallState;
+
+ constructor() {
+ super();
+
+ CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall);
+ CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
+ }
+
+ private get invite(): MatrixEvent {
+ return [...this.events].find((event) => event.getType() === EventType.CallInvite);
+ }
+
+ private get hangup(): MatrixEvent {
+ return [...this.events].find((event) => event.getType() === EventType.CallHangup);
+ }
+
+ private get reject(): MatrixEvent {
+ return [...this.events].find((event) => event.getType() === EventType.CallReject);
+ }
+
+ public get isVoice(): boolean {
+ const invite = this.invite;
+ if (!invite) return;
+
+ // FIXME: Find a better way to determine this from the event?
+ if (invite.getContent()?.offer?.sdp?.indexOf('m=video') !== -1) return false;
+ return true;
+ }
+
+ public get hangupReason(): string | null {
+ return this.hangup?.getContent()?.reason;
+ }
+
+ public get rejectParty(): string {
+ return this.reject?.getSender();
+ }
+
+ public get gotRejected(): boolean {
+ return Boolean(this.reject);
+ }
+
+ /**
+ * Returns true if there are only events from the other side - we missed the call
+ */
+ private get callWasMissed(): boolean {
+ return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId());
+ }
+
+ private get callId(): string {
+ return [...this.events][0].getContent().call_id;
+ }
+
+ private onSilencedCallsChanged = () => {
+ const newState = CallHandler.sharedInstance().isCallSilenced(this.callId);
+ this.emit(CallEventGrouperEvent.SilencedChanged, newState);
+ };
+
+ public answerCall = () => {
+ this.call?.answer();
+ };
+
+ public rejectCall = () => {
+ this.call?.reject();
+ };
+
+ public callBack = () => {
+ defaultDispatcher.dispatch({
+ action: 'place_call',
+ type: this.isVoice ? CallType.Voice : CallType.Video,
+ room_id: [...this.events][0]?.getRoomId(),
+ });
+ };
+
+ public toggleSilenced = () => {
+ const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId);
+ silenced ?
+ CallHandler.sharedInstance().unSilenceCall(this.callId) :
+ CallHandler.sharedInstance().silenceCall(this.callId);
+ };
+
+ private setCallListeners() {
+ if (!this.call) return;
+ this.call.addListener(CallEvent.State, this.setState);
+ }
+
+ private setState = () => {
+ if (SUPPORTED_STATES.includes(this.call?.state)) {
+ this.state = this.call.state;
+ } else {
+ if (this.callWasMissed) this.state = CustomCallState.Missed;
+ else if (this.reject) this.state = CallState.Ended;
+ else if (this.hangup) this.state = CallState.Ended;
+ else if (this.invite && this.call) this.state = CallState.Connecting;
+ }
+ this.emit(CallEventGrouperEvent.StateChanged, this.state);
+ };
+
+ private setCall = () => {
+ if (this.call) return;
+
+ this.call = CallHandler.sharedInstance().getCallById(this.callId);
+ this.setCallListeners();
+ this.setState();
+ };
+
+ public add(event: MatrixEvent) {
+ this.events.add(event);
+ this.setCall();
+ }
+}
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/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js
index 472a43e142..037a0eba2a 100644
--- a/src/components/structures/EmbeddedPage.js
+++ b/src/components/structures/EmbeddedPage.js
@@ -120,8 +120,7 @@ export default class EmbeddedPage extends React.PureComponent {
const content =
-
;
+ />;
if (this.props.scrollbar) {
return
diff --git a/src/components/structures/FilePanel.tsx b/src/components/structures/FilePanel.tsx
index c6d72d04bb..52cf5ae55b 100644
--- a/src/components/structures/FilePanel.tsx
+++ b/src/components/structures/FilePanel.tsx
@@ -36,6 +36,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { TileShape } from '../views/rooms/EventTile';
+import { Layout } from "../../settings/Layout";
interface IProps {
roomId: string;
@@ -267,6 +268,7 @@ class FilePanel extends React.Component {
tileShape={TileShape.FileGrid}
resizeNotifier={this.props.resizeNotifier}
empty={emptyState}
+ layout={Layout.Group}
/>
);
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 9d69fce801..99fa94e62b 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -222,7 +222,7 @@ class FeaturedRoom extends React.Component {
let roomNameNode = null;
if (permalink) {
- roomNameNode = { roomName } ;
+ roomNameNode = { roomName } ;
} else {
roomNameNode = { roomName } ;
}
@@ -1185,10 +1185,13 @@ export default class GroupView extends React.Component {
avatarImage = ;
} else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
- avatarImage = ;
}
@@ -1199,9 +1202,12 @@ export default class GroupView extends React.Component {
@@ -1238,7 +1244,8 @@ export default class GroupView extends React.Component {
groupAvatarUrl={groupAvatarUrl}
groupName={groupName}
onClick={onGroupHeaderItemClick}
- width={28} height={28}
+ width={28}
+ height={28}
/>;
if (summary.profile && summary.profile.name) {
nameNode =
@@ -1269,28 +1276,32 @@ export default class GroupView extends React.Component {
key="_cancelButton"
onClick={this._onCancelClick}
>
-
+
,
);
} else {
if (summary.user && summary.user.membership === 'join') {
rightButtons.push(
-
- ,
+ />,
);
}
rightButtons.push(
-
- ,
+ />,
);
}
diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx
index 9fd96b161a..698a36127b 100644
--- a/src/components/structures/MessagePanel.tsx
+++ b/src/components/structures/MessagePanel.tsx
@@ -36,6 +36,7 @@ import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
import { replaceableComponent } from "../../utils/replaceableComponent";
import defaultDispatcher from '../../dispatcher/dispatcher';
+import CallEventGrouper from "./CallEventGrouper";
import WhoIsTypingTile from '../views/rooms/WhoIsTypingTile';
import ScrollPanel, { IScrollState } from "./ScrollPanel";
import EventListSummary from '../views/elements/EventListSummary';
@@ -232,6 +233,11 @@ export default class MessagePanel extends React.Component
{
private readonly showTypingNotificationsWatcherRef: string;
private eventNodes: Record;
+ // A map of
+ private callEventGroupers = new Map();
+
+ private membersCount = 0;
+
constructor(props, context) {
super(props, context);
@@ -252,11 +258,14 @@ export default class MessagePanel extends React.Component {
}
componentDidMount() {
+ this.calculateRoomMembersCount();
+ this.props.room?.on("RoomState.members", this.calculateRoomMembersCount);
this.isMounted = true;
}
componentWillUnmount() {
this.isMounted = false;
+ this.props.room?.off("RoomState.members", this.calculateRoomMembersCount);
SettingsStore.unwatchSetting(this.showTypingNotificationsWatcherRef);
}
@@ -270,6 +279,10 @@ export default class MessagePanel extends React.Component {
}
}
+ private calculateRoomMembersCount = (): void => {
+ this.membersCount = this.props.room?.getMembers().length || 0;
+ };
+
private onShowTypingNotificationsChange = (): void => {
this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
@@ -576,6 +589,20 @@ export default class MessagePanel extends React.Component {
const last = (mxEv === lastShownEvent);
const { nextEvent, nextTile } = this.getNextEventInfo(this.props.events, i);
+ if (
+ mxEv.getType().indexOf("m.call.") === 0 ||
+ mxEv.getType().indexOf("org.matrix.call.") === 0
+ ) {
+ const callId = mxEv.getContent().call_id;
+ if (this.callEventGroupers.has(callId)) {
+ this.callEventGroupers.get(callId).add(mxEv);
+ } else {
+ const callEventGrouper = new CallEventGrouper();
+ callEventGrouper.add(mxEv);
+ this.callEventGroupers.set(callId, callEventGrouper);
+ }
+ }
+
if (grouper) {
if (grouper.shouldGroup(mxEv)) {
grouper.add(mxEv, this.showHiddenEvents);
@@ -591,7 +618,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) {
@@ -692,6 +727,7 @@ export default class MessagePanel extends React.Component {
// it's successful: we received it.
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
+ const callEventGrouper = this.callEventGroupers.get(mxEv.getContent().call_id);
// use txnId as key if available so that we don't remount during sending
ret.push(
@@ -722,7 +758,8 @@ export default class MessagePanel extends React.Component {
layout={this.props.layout}
enableFlair={this.props.enableFlair}
showReadReceipts={this.props.showReadReceipts}
- hideSender={this.props.room.getMembers().length <= 2 && this.props.layout === Layout.Bubble}
+ callEventGrouper={callEventGrouper}
+ hideSender={this.membersCount <= 2 && this.props.layout === Layout.Bubble}
/>
,
);
@@ -952,6 +989,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,
) {
@@ -1078,6 +1116,7 @@ class CreationGrouper extends BaseGrouper {
onToggle={panel.onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]}
summaryText={summaryText}
+ layout={this.layout}
>
{ eventTiles }
,
@@ -1105,10 +1144,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];
}
@@ -1173,6 +1213,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 }
,
@@ -1201,8 +1242,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];
}
@@ -1277,6 +1319,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/MyGroups.js b/src/components/structures/MyGroups.js
index fca5613ede..dab18c4161 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -109,8 +109,7 @@ export default class MyGroups extends React.Component {
-
-
+
{ _t('Create a new community') }
diff --git a/src/components/structures/NotificationPanel.tsx b/src/components/structures/NotificationPanel.tsx
index 8abc161bab..f71c017c06 100644
--- a/src/components/structures/NotificationPanel.tsx
+++ b/src/components/structures/NotificationPanel.tsx
@@ -23,6 +23,7 @@ import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { TileShape } from "../views/rooms/EventTile";
+import { Layout } from "../../settings/Layout";
interface IProps {
onClose(): void;
@@ -52,6 +53,7 @@ export default class NotificationPanel extends React.PureComponent
{
tileShape={TileShape.Notif}
empty={emptyState}
alwaysShowTimestamps={true}
+ layout={Layout.Group}
/>
);
} else {
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index ac4d197346..8b10c54cba 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -266,8 +266,12 @@ export default class RoomStatusBar extends React.PureComponent {
-
+
{ _t('Connectivity to the server has been lost.') }
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 7860e65362..474b99262d 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -166,6 +166,10 @@ export interface IState {
canReply: boolean;
layout: Layout;
lowBandwidth: boolean;
+ alwaysShowTimestamps: boolean;
+ showTwelveHourTimestamps: boolean;
+ readMarkerInViewThresholdMs: number;
+ readMarkerOutOfViewThresholdMs: number;
showHiddenEventsInTimeline: boolean;
showReadReceipts: boolean;
showRedactions: boolean;
@@ -231,6 +235,10 @@ export default class RoomView extends React.Component
{
canReply: false,
layout: SettingsStore.getValue("layout"),
lowBandwidth: SettingsStore.getValue("lowBandwidth"),
+ alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"),
+ showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"),
+ readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"),
+ readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"),
showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline"),
showReadReceipts: true,
showRedactions: true,
@@ -263,14 +271,26 @@ export default class RoomView extends React.Component {
WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
this.settingWatchers = [
- SettingsStore.watchSetting("layout", null, () =>
- this.setState({ layout: SettingsStore.getValue("layout") }),
+ SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
+ this.setState({ layout: value as Layout }),
),
- SettingsStore.watchSetting("lowBandwidth", null, () =>
- this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }),
+ SettingsStore.watchSetting("lowBandwidth", null, (...[,,, value]) =>
+ this.setState({ lowBandwidth: value as boolean }),
),
- SettingsStore.watchSetting("showHiddenEventsInTimeline", null, () =>
- this.setState({ showHiddenEventsInTimeline: SettingsStore.getValue("showHiddenEventsInTimeline") }),
+ SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[,,, value]) =>
+ this.setState({ alwaysShowTimestamps: value as boolean }),
+ ),
+ SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[,,, value]) =>
+ this.setState({ showTwelveHourTimestamps: value as boolean }),
+ ),
+ SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[,,, value]) =>
+ this.setState({ readMarkerInViewThresholdMs: value as number }),
+ ),
+ SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[,,, value]) =>
+ this.setState({ readMarkerOutOfViewThresholdMs: value as number }),
+ ),
+ SettingsStore.watchSetting("showHiddenEventsInTimeline", null, (...[,,, value]) =>
+ this.setState({ showHiddenEventsInTimeline: value as boolean }),
),
];
}
@@ -337,30 +357,20 @@ export default class RoomView extends React.Component {
// Add watchers for each of the settings we just looked up
this.settingWatchers = this.settingWatchers.concat([
- SettingsStore.watchSetting("showReadReceipts", null, () =>
- this.setState({
- showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId),
- }),
+ SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) =>
+ this.setState({ showReadReceipts: value as boolean }),
),
- SettingsStore.watchSetting("showRedactions", null, () =>
- this.setState({
- showRedactions: SettingsStore.getValue("showRedactions", roomId),
- }),
+ SettingsStore.watchSetting("showRedactions", roomId, (...[,,, value]) =>
+ this.setState({ showRedactions: value as boolean }),
),
- SettingsStore.watchSetting("showJoinLeaves", null, () =>
- this.setState({
- showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId),
- }),
+ SettingsStore.watchSetting("showJoinLeaves", roomId, (...[,,, value]) =>
+ this.setState({ showJoinLeaves: value as boolean }),
),
- SettingsStore.watchSetting("showAvatarChanges", null, () =>
- this.setState({
- showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId),
- }),
+ SettingsStore.watchSetting("showAvatarChanges", roomId, (...[,,, value]) =>
+ this.setState({ showAvatarChanges: value as boolean }),
),
- SettingsStore.watchSetting("showDisplaynameChanges", null, () =>
- this.setState({
- showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId),
- }),
+ SettingsStore.watchSetting("showDisplaynameChanges", roomId, (...[,,, value]) =>
+ this.setState({ showDisplaynameChanges: value as boolean }),
),
]);
@@ -1730,7 +1740,8 @@ export default class RoomView extends React.Component {
onJoinClick={this.onJoinButtonClicked}
onForgetClick={this.onForgetClick}
onRejectClick={this.onRejectThreepidInviteButtonClicked}
- canPreview={false} error={this.state.roomLoadError}
+ canPreview={false}
+ error={this.state.roomLoadError}
roomAlias={roomAlias}
joining={this.state.joining}
inviterName={inviterName}
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/SearchBox.js b/src/components/structures/SearchBox.js
index 3cf4b9b593..6d310662e3 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -136,8 +136,8 @@ export default class SearchBox extends React.Component {
key="button"
tabIndex={-1}
className="mx_SearchBox_closeButton"
- onClick={() => {this._clearSearch("button"); }}>
- ) : undefined;
+ onClick={() => {this._clearSearch("button"); }}
+ />) : undefined;
// show a shorter placeholder when blurred, if requested
// this is used for the room filter field that has
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 06b2f4a629..4064b2f48e 100644
--- a/src/components/structures/SpaceRoomView.tsx
+++ b/src/components/structures/SpaceRoomView.tsx
@@ -16,7 +16,7 @@ limitations under the License.
import React, { RefObject, useContext, useRef, useState } from "react";
import { EventType } from "matrix-js-sdk/src/@types/event";
-import { Preset } from "matrix-js-sdk/src/@types/partials";
+import { Preset, JoinRule } from "matrix-js-sdk/src/@types/partials";
import { Room } from "matrix-js-sdk/src/models/room";
import { EventSubscription } from "fbemitter";
@@ -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,11 +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 { JoinRule } from "../views/settings/tabs/room/SecurityRoomSettingsTab";
+import { SpaceFeedbackPrompt } from "../views/spaces/SpaceCreateMenu";
interface IProps {
space: Room;
@@ -93,26 +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
-
-
-
{ _t("Spaces are a beta feature.") }
-
{
- if (onClick) onClick();
- Modal.createTrackedDialog("Beta Feedback", "feature_spaces", BetaFeedbackDialog, {
- featureId: "feature_spaces",
- });
- }}>
- { _t("Feedback") }
-
-
-
;
-};
-
const RoomMemberCount = ({ room, children }) => {
const members = useRoomMembers(room);
const count = members.length;
@@ -306,8 +293,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
;
};
-const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
- const cli = useContext(MatrixClientContext);
+const SpaceLandingAddButton = ({ space }) => {
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
let contextMenu;
@@ -330,25 +316,33 @@ const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
e.stopPropagation();
closeMenu();
- if (await showCreateNewRoom(cli, space)) {
- onNewRoomAdded();
+ if (await showCreateNewRoom(space)) {
+ defaultDispatcher.fire(Action.UpdateSpaceHierarchy);
}
}}
/>
{
+ onClick={(e) => {
e.preventDefault();
e.stopPropagation();
closeMenu();
-
- const [added] = await showAddExistingRooms(cli, space);
- if (added) {
- onNewRoomAdded();
- }
+ showAddExistingRooms(space);
}}
/>
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ closeMenu();
+ showCreateNewSubspace(space);
+ }}
+ >
+
+
;
}
@@ -389,19 +383,17 @@ const SpaceLanding = ({ space }) => {
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
- const [refreshToken, forceUpdate] = useStateToggle(false);
-
let addRoomButton;
if (canAddRooms) {
- addRoomButton = ;
+ addRoomButton = ;
}
let settingsButton;
- if (shouldShowSpaceSettings(cli, space)) {
+ if (shouldShowSpaceSettings(space)) {
settingsButton = {
- showSpaceSettings(cli, space);
+ showSpaceSettings(space);
}}
title={_t("Settings")}
/>;
@@ -416,6 +408,7 @@ const SpaceLanding = ({ space }) => {
};
return
+
@@ -440,15 +433,8 @@ const SpaceLanding = ({ space }) => {
) }
-
-
-
+
;
};
@@ -531,7 +517,6 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
value={buttonLabel}
/>
-
;
};
@@ -550,13 +535,12 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
{ _t("Skip for now") }
}
+ filterPlaceholder={_t("Search for rooms or spaces")}
onFinished={onFinished}
+ roomsRenderer={defaultRoomsRenderer}
+ spacesRenderer={defaultSpacesRenderer}
+ dmsRenderer={defaultDmsRenderer}
/>
-
-
-
-
-
;
};
@@ -576,7 +560,6 @@ const SpaceSetupPublicShare = ({ justCreatedOpts, space, onFinished, createdRoom
{ createdRooms ? _t("Go to my first room") : _t("Go to my space") }
-
;
};
@@ -605,9 +588,8 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
{ _t("Teammates might not be able to view or join any private rooms you make.") }
-
{ _t("We're working on this as part of the beta, but just want to let you know.") }
+
{ _t("We're working on this, but just want to let you know.") }
-
;
};
@@ -730,7 +712,6 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
value={buttonLabel}
/>
-
;
};
diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx
index c4210c68a8..0899b1c72a 100644
--- a/src/components/structures/TimelinePanel.tsx
+++ b/src/components/structures/TimelinePanel.tsx
@@ -665,8 +665,8 @@ class TimelinePanel extends React.Component {
private readMarkerTimeout(readMarkerPosition: number): number {
return readMarkerPosition === 0 ?
- this.state.readMarkerInViewThresholdMs :
- this.state.readMarkerOutOfViewThresholdMs;
+ this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs :
+ this.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs;
}
private async updateReadMarkerOnUserActivity(): Promise {
@@ -1493,8 +1493,12 @@ class TimelinePanel extends React.Component {
onUserScroll={this.props.onUserScroll}
onFillRequest={this.onMessageListFillRequest}
onUnfillRequest={this.onMessageListUnfillRequest}
- isTwelveHour={this.state.isTwelveHour}
- alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps}
+ isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour}
+ alwaysShowTimestamps={
+ this.props.alwaysShowTimestamps ??
+ this.context?.alwaysShowTimestamps ??
+ this.state.alwaysShowTimestamps
+ }
className={this.props.className}
tileShape={this.props.tileShape}
resizeNotifier={this.props.resizeNotifier}
diff --git a/src/components/structures/auth/ForgotPassword.tsx b/src/components/structures/auth/ForgotPassword.tsx
index 3755505f3d..f978a6cded 100644
--- a/src/components/structures/auth/ForgotPassword.tsx
+++ b/src/components/structures/auth/ForgotPassword.tsx
@@ -315,7 +315,10 @@ export default class ForgotPassword extends React.Component {
{ _t("An email has been sent to %(emailAddress)s. Once you've followed the " +
"link it contains, click below.", { emailAddress: this.state.email }) }
-
;
}
@@ -328,7 +331,10 @@ export default class ForgotPassword extends React.Component {
"push notifications. To re-enable notifications, sign in again on each " +
"device.",
) }
-
;
}
diff --git a/src/components/structures/auth/Login.tsx b/src/components/structures/auth/Login.tsx
index 6a3d339681..7a05d8c6c6 100644
--- a/src/components/structures/auth/Login.tsx
+++ b/src/components/structures/auth/Login.tsx
@@ -463,7 +463,9 @@ export default class LoginComponent extends React.PureComponent
"Either use HTTPS or enable unsafe scripts .", {},
{
'a': (sub) => {
- return
{ sub }
diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx
index 549e47260f..2b97650d4b 100644
--- a/src/components/structures/auth/Registration.tsx
+++ b/src/components/structures/auth/Registration.tsx
@@ -557,12 +557,16 @@ export default class Registration extends React.Component {
loggedInUserId: this.state.differentLoggedInUserId,
},
) }
- {
- const sessionLoaded = await this.onLoginClickWithCheck(event);
- if (sessionLoaded) {
- dis.dispatch({ action: "view_welcome_page" });
- }
- }}>
+ {
+ const sessionLoaded = await this.onLoginClickWithCheck(event);
+ if (sessionLoaded) {
+ dis.dispatch({ action: "view_welcome_page" });
+ }
+ }}
+ >
{ _t("Continue with previous account") }
;
diff --git a/src/components/views/audio_messages/AudioPlayer.tsx b/src/components/views/audio_messages/AudioPlayer.tsx
index 748b1c9ffc..b83f89fe5b 100644
--- a/src/components/views/audio_messages/AudioPlayer.tsx
+++ b/src/components/views/audio_messages/AudioPlayer.tsx
@@ -14,9 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { Playback, PlaybackState } from "../../../voice/Playback";
import React, { createRef, ReactNode, RefObject } from "react";
-import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import PlayPauseButton from "./PlayPauseButton";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { formatBytes } from "../../../utils/FormattingUtils";
@@ -25,44 +23,13 @@ import { Key } from "../../../Keyboard";
import { _t } from "../../../languageHandler";
import SeekBar from "./SeekBar";
import PlaybackClock from "./PlaybackClock";
-
-interface IProps {
- // Playback instance to render. Cannot change during component lifecycle: create
- // an all-new component instead.
- playback: Playback;
-
- mediaName: string;
-}
-
-interface IState {
- playbackPhase: PlaybackState;
-}
+import AudioPlayerBase from "./AudioPlayerBase";
@replaceableComponent("views.audio_messages.AudioPlayer")
-export default class AudioPlayer extends React.PureComponent {
+export default class AudioPlayer extends AudioPlayerBase {
private playPauseRef: RefObject = createRef();
private seekRef: RefObject = createRef();
- constructor(props: IProps) {
- super(props);
-
- this.state = {
- playbackPhase: PlaybackState.Decoding, // default assumption
- };
-
- // We don't need to de-register: the class handles this for us internally
- this.props.playback.on(UPDATE_EVENT, this.onPlaybackUpdate);
-
- // Don't wait for the promise to complete - it will emit a progress update when it
- // is done, and it's not meant to take long anyhow.
- // noinspection JSIgnoredPromiseFromCall
- this.props.playback.prepare();
- }
-
- private onPlaybackUpdate = (ev: PlaybackState) => {
- this.setState({ playbackPhase: ev });
- };
-
private onKeyDown = (ev: React.KeyboardEvent) => {
// stopPropagation() prevents the FocusComposer catch-all from triggering,
// but we need to do it on key down instead of press (even though the user
@@ -88,37 +55,39 @@ export default class AudioPlayer extends React.PureComponent {
return `(${formatBytes(bytes)})`;
}
- public render(): ReactNode {
+ protected renderComponent(): ReactNode {
// tabIndex=0 to ensure that the whole component becomes a tab stop, where we handle keyboard
// events for accessibility
- return