diff --git a/CHANGELOG.md b/CHANGELOG.md
index d459b4e94a..58d23e3413 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,115 @@
+Changes in [3.20.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0) (2021-05-10)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.20.0-rc.1...v3.20.0)
+
+ * Upgrade to JS SDK 10.1.0
+ * [Release] Don't use the event's metadata to calc the scale of an image
+ [\#6004](https://github.com/matrix-org/matrix-react-sdk/pull/6004)
+
+Changes in [3.20.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.20.0-rc.1) (2021-05-04)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0...v3.20.0-rc.1)
+
+ * Upgrade to JS SDK 10.1.0-rc.1
+ * Translations update from Weblate
+ [\#5966](https://github.com/matrix-org/matrix-react-sdk/pull/5966)
+ * Fix more space panel layout and hover behaviour issues
+ [\#5965](https://github.com/matrix-org/matrix-react-sdk/pull/5965)
+ * Fix edge case with space panel alignment with subspaces on ff
+ [\#5964](https://github.com/matrix-org/matrix-react-sdk/pull/5964)
+ * Fix saving room pill part to history
+ [\#5951](https://github.com/matrix-org/matrix-react-sdk/pull/5951)
+ * Generate room preview even when minimized
+ [\#5948](https://github.com/matrix-org/matrix-react-sdk/pull/5948)
+ * Another change from recovery passphrase to Security Phrase
+ [\#5934](https://github.com/matrix-org/matrix-react-sdk/pull/5934)
+ * Sort rooms in the add existing to space dialog based on recency
+ [\#5943](https://github.com/matrix-org/matrix-react-sdk/pull/5943)
+ * Inhibit sending RR when context switching to a room
+ [\#5944](https://github.com/matrix-org/matrix-react-sdk/pull/5944)
+ * Prevent room list keyboard handling from landing focus on hidden nodes
+ [\#5950](https://github.com/matrix-org/matrix-react-sdk/pull/5950)
+ * Make the text filter search all spaces instead of just the selected one
+ [\#5942](https://github.com/matrix-org/matrix-react-sdk/pull/5942)
+ * Enable indent rule and fix indent
+ [\#5931](https://github.com/matrix-org/matrix-react-sdk/pull/5931)
+ * Prevent peeking members from reacting
+ [\#5946](https://github.com/matrix-org/matrix-react-sdk/pull/5946)
+ * Disallow inline display maths
+ [\#5939](https://github.com/matrix-org/matrix-react-sdk/pull/5939)
+ * Space creation prompt user to add existing rooms for "Just Me" spaces
+ [\#5923](https://github.com/matrix-org/matrix-react-sdk/pull/5923)
+ * Add test coverage collection script
+ [\#5937](https://github.com/matrix-org/matrix-react-sdk/pull/5937)
+ * Fix joining room using via servers regression
+ [\#5936](https://github.com/matrix-org/matrix-react-sdk/pull/5936)
+ * Revert "Fixes the two Todays problem in Redaction"
+ [\#5938](https://github.com/matrix-org/matrix-react-sdk/pull/5938)
+ * Handle encoded matrix URLs
+ [\#5903](https://github.com/matrix-org/matrix-react-sdk/pull/5903)
+ * Render ignored users setting regardless of if there are any
+ [\#5860](https://github.com/matrix-org/matrix-react-sdk/pull/5860)
+ * Fix inserting trailing colon after mention/pill
+ [\#5830](https://github.com/matrix-org/matrix-react-sdk/pull/5830)
+ * Fixes the two Todays problem in Redaction
+ [\#5917](https://github.com/matrix-org/matrix-react-sdk/pull/5917)
+ * Fix page up/down scrolling only half a page
+ [\#5920](https://github.com/matrix-org/matrix-react-sdk/pull/5920)
+ * Voice messages: Composer controls
+ [\#5935](https://github.com/matrix-org/matrix-react-sdk/pull/5935)
+ * Support MSC3086 asserted identity
+ [\#5886](https://github.com/matrix-org/matrix-react-sdk/pull/5886)
+ * Handle possible edge case with getting stuck in "unsent messages" bar
+ [\#5930](https://github.com/matrix-org/matrix-react-sdk/pull/5930)
+ * Fix suggested rooms not showing up regression from room list optimisation
+ [\#5932](https://github.com/matrix-org/matrix-react-sdk/pull/5932)
+ * Broadcast language change to ElectronPlatform
+ [\#5913](https://github.com/matrix-org/matrix-react-sdk/pull/5913)
+ * Fix VoIP PIP frame color
+ [\#5701](https://github.com/matrix-org/matrix-react-sdk/pull/5701)
+ * Convert some Flow-typed files to TypeScript
+ [\#5912](https://github.com/matrix-org/matrix-react-sdk/pull/5912)
+ * Initial SpaceStore tests work
+ [\#5906](https://github.com/matrix-org/matrix-react-sdk/pull/5906)
+ * Fix issues with space hierarchy in layout and with incompatible servers
+ [\#5926](https://github.com/matrix-org/matrix-react-sdk/pull/5926)
+ * Scale all mxc thumbs using device pixel ratio for hidpi
+ [\#5928](https://github.com/matrix-org/matrix-react-sdk/pull/5928)
+ * Fix add existing to space dialog no longer showing rooms for public spaces
+ [\#5918](https://github.com/matrix-org/matrix-react-sdk/pull/5918)
+ * Disable spaces context switching for when exploring a space
+ [\#5924](https://github.com/matrix-org/matrix-react-sdk/pull/5924)
+ * Autofocus search box in the add existing to space dialog
+ [\#5921](https://github.com/matrix-org/matrix-react-sdk/pull/5921)
+ * Use label element in add existing to space dialog for easier hit target
+ [\#5922](https://github.com/matrix-org/matrix-react-sdk/pull/5922)
+ * Dynamic max and min zoom in the new ImageView
+ [\#5916](https://github.com/matrix-org/matrix-react-sdk/pull/5916)
+ * Improve message error states
+ [\#5897](https://github.com/matrix-org/matrix-react-sdk/pull/5897)
+ * Check for null room in `VisibilityProvider`
+ [\#5914](https://github.com/matrix-org/matrix-react-sdk/pull/5914)
+ * Add unit tests for various collection-based utility functions
+ [\#5910](https://github.com/matrix-org/matrix-react-sdk/pull/5910)
+ * Spaces visual fixes
+ [\#5909](https://github.com/matrix-org/matrix-react-sdk/pull/5909)
+ * Remove reliance on DOM API to generated message preview
+ [\#5908](https://github.com/matrix-org/matrix-react-sdk/pull/5908)
+ * Expand upon voice message event & include overall waveform
+ [\#5888](https://github.com/matrix-org/matrix-react-sdk/pull/5888)
+ * Use floats for image background opacity
+ [\#5905](https://github.com/matrix-org/matrix-react-sdk/pull/5905)
+ * Show invites to spaces at the top of the space panel
+ [\#5902](https://github.com/matrix-org/matrix-react-sdk/pull/5902)
+ * Improve edge cases with spaces context switching
+ [\#5899](https://github.com/matrix-org/matrix-react-sdk/pull/5899)
+ * Fix spaces notification dots wrongly including upgraded (hidden) rooms
+ [\#5900](https://github.com/matrix-org/matrix-react-sdk/pull/5900)
+ * Iterate the spaces face pile design
+ [\#5898](https://github.com/matrix-org/matrix-react-sdk/pull/5898)
+ * Fix alignment issue with nested spaces being cut off wrong
+ [\#5890](https://github.com/matrix-org/matrix-react-sdk/pull/5890)
+
Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0)
diff --git a/package.json b/package.json
index e54de8a96c..d9ea440936 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.19.0",
+ "version": "3.20.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -58,7 +58,7 @@
"blueimp-canvas-to-blob": "^3.28.0",
"browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3",
- "cheerio": "^1.0.0-rc.5",
+ "cheerio": "^1.0.0-rc.9",
"classnames": "^2.2.6",
"commonmark": "^0.29.3",
"counterpart": "^0.18.6",
@@ -97,7 +97,7 @@
"react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.1",
"rfc4648": "^1.4.0",
- "sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db",
+ "sanitize-html": "^2.3.2",
"tar-js": "^0.3.0",
"text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0",
@@ -132,11 +132,12 @@
"@types/modernizr": "^3.5.3",
"@types/node": "^14.14.22",
"@types/pako": "^1.0.1",
+ "@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5",
"@types/react": "^16.9",
"@types/react-dom": "^16.9.10",
"@types/react-transition-group": "^4.4.0",
- "@types/sanitize-html": "^1.27.0",
+ "@types/sanitize-html": "^2.3.1",
"@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
diff --git a/res/css/_components.scss b/res/css/_components.scss
index ec9592f3a1..e6c55c0250 100644
--- a/res/css/_components.scss
+++ b/res/css/_components.scss
@@ -54,6 +54,7 @@
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/avatars/_PulsedAvatar.scss";
@import "./views/avatars/_WidgetAvatar.scss";
+@import "./views/beta/_BetaCard.scss";
@import "./views/context_menus/_CallContextMenu.scss";
@import "./views/context_menus/_IconizedContextMenu.scss";
@import "./views/context_menus/_MessageContextMenu.scss";
@@ -237,6 +238,7 @@
@import "./views/settings/tabs/user/_AppearanceUserSettingsTab.scss";
@import "./views/settings/tabs/user/_GeneralUserSettingsTab.scss";
@import "./views/settings/tabs/user/_HelpUserSettingsTab.scss";
+@import "./views/settings/tabs/user/_LabsUserSettingsTab.scss";
@import "./views/settings/tabs/user/_MjolnirUserSettingsTab.scss";
@import "./views/settings/tabs/user/_NotificationUserSettingsTab.scss";
@import "./views/settings/tabs/user/_PreferencesUserSettingsTab.scss";
diff --git a/res/css/structures/_GroupFilterPanel.scss b/res/css/structures/_GroupFilterPanel.scss
index e5a8ef6df2..444435dd57 100644
--- a/res/css/structures/_GroupFilterPanel.scss
+++ b/res/css/structures/_GroupFilterPanel.scss
@@ -56,6 +56,12 @@ limitations under the License.
.mx_GroupFilterPanel .mx_TagTile {
// opacity: 0.5;
position: relative;
+
+ .mx_BetaDot {
+ position: absolute;
+ right: -13px;
+ top: -11px;
+ }
}
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
diff --git a/res/css/structures/_MyGroups.scss b/res/css/structures/_MyGroups.scss
index 73f1332cd0..9c0062b72d 100644
--- a/res/css/structures/_MyGroups.scss
+++ b/res/css/structures/_MyGroups.scss
@@ -17,6 +17,11 @@ limitations under the License.
.mx_MyGroups {
display: flex;
flex-direction: column;
+
+ .mx_BetaCard {
+ margin: 0 72px;
+ max-width: 760px;
+ }
}
.mx_MyGroups .mx_RoomHeader_simpleHeader {
@@ -30,7 +35,7 @@ limitations under the License.
flex-wrap: wrap;
}
-.mx_MyGroups > :not(.mx_RoomHeader) {
+.mx_MyGroups > :not(.mx_RoomHeader):not(.mx_BetaCard) {
max-width: 960px;
margin: 40px;
}
diff --git a/res/css/structures/_SpacePanel.scss b/res/css/structures/_SpacePanel.scss
index 9264e82ea5..c433ccf275 100644
--- a/res/css/structures/_SpacePanel.scss
+++ b/res/css/structures/_SpacePanel.scss
@@ -237,7 +237,6 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpacePanel_badgeContainer {
position: absolute;
- height: 16px;
// Create a flexbox to make aligning dot badges easier
display: flex;
@@ -249,23 +248,37 @@ $activeBorderColor: $secondary-fg-color;
.mx_NotificationBadge_dot {
// make the smaller dot occupy the same width for centering
- margin-left: 7px;
- margin-right: 7px;
+ margin: 0 7px;
}
}
&.collapsed {
.mx_SpaceButton {
.mx_SpacePanel_badgeContainer {
- right: -3px;
- top: -3px;
+ right: 0;
+ top: 0;
+
+ .mx_NotificationBadge {
+ background-clip: padding-box;
+ }
+
+ .mx_NotificationBadge_dot {
+ margin: 0 -1px 0 0;
+ border: 3px solid $groupFilterPanel-bg-color;
+ }
+
+ .mx_NotificationBadge_2char,
+ .mx_NotificationBadge_3char {
+ margin: -5px -5px 0 0;
+ border: 2px solid $groupFilterPanel-bg-color;
+ }
}
&.mx_SpaceButton_active .mx_SpacePanel_badgeContainer {
// when we draw the selection border we move the relative bounds of our parent
// so update our position within the bounds of the parent to maintain position overall
- right: -6px;
- top: -6px;
+ right: -3px;
+ top: -3px;
}
}
}
diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss
index c7d087d8e0..7925686bf1 100644
--- a/res/css/structures/_SpaceRoomDirectory.scss
+++ b/res/css/structures/_SpaceRoomDirectory.scss
@@ -86,7 +86,7 @@ limitations under the License.
color: $primary-fg-color;
.mx_AccessibleButton {
- padding: 2px 8px;
+ padding: 4px 12px;
font-weight: normal;
& + .mx_AccessibleButton {
@@ -94,6 +94,11 @@ limitations under the License.
}
}
+ .mx_AccessibleButton_kind_danger_outline,
+ .mx_AccessibleButton_kind_primary_outline {
+ padding: 3px 12px; // to account for the 1px border
+ }
+
> span {
margin-left: auto;
}
@@ -246,11 +251,17 @@ limitations under the License.
grid-row: 1/3;
.mx_AccessibleButton {
- padding: 8px 18px;
+ line-height: $font-24px;
+ padding: 4px 16px;
display: inline-block;
visibility: hidden;
}
+ .mx_AccessibleButton_kind_danger_outline,
+ .mx_AccessibleButton_kind_primary_outline {
+ padding: 3px 16px; // to account for the 1px border
+ }
+
.mx_Checkbox {
display: inline-flex;
vertical-align: middle;
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 553919d862..c4b3f9a8d3 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -103,6 +103,10 @@ $SpaceRoomViewInnerWidth: 428px;
padding: 8px 22px;
margin-left: 16px;
}
+
+ input.mx_AccessibleButton {
+ border: none; // override default styles
+ }
}
.mx_Field {
@@ -133,6 +137,44 @@ $SpaceRoomViewInnerWidth: 428px;
box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color;
border-radius: 8px;
+ position: relative;
+
+ // XXX remove this when spaces leaves Beta
+ .mx_BetaCard_betaPill {
+ position: absolute;
+ right: 24px;
+ top: 32px;
+ }
+ // XXX remove this when spaces leaves Beta
+ .mx_SpaceRoomView_preview_spaceBetaPrompt {
+ font-weight: $font-semi-bold;
+ font-size: $font-14px;
+ line-height: $font-24px;
+ color: $primary-fg-color;
+ margin-top: 24px;
+ position: relative;
+ padding-left: 24px;
+
+ .mx_AccessibleButton_kind_link {
+ display: inline;
+ padding: 0;
+ font-size: inherit;
+ line-height: inherit;
+ }
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: $font-24px;
+ width: 20px;
+ left: 0;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
+ background-color: $secondary-fg-color;
+ }
+ }
.mx_SpaceRoomView_preview_inviter {
display: flex;
@@ -238,7 +280,8 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SpaceRoomView_landing_inviteButton {
position: relative;
- padding-left: 40px;
+ padding: 4px 18px 4px 40px;
+ line-height: $font-24px;
height: min-content;
&::before {
@@ -254,6 +297,27 @@ $SpaceRoomViewInnerWidth: 428px;
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
+
+ .mx_SpaceRoomView_landing_settingsButton {
+ position: relative;
+ margin-left: 16px;
+ width: 24px;
+ height: 24px;
+
+ &::before {
+ position: absolute;
+ content: "";
+ left: 0;
+ top: 0;
+ height: 24px;
+ width: 24px;
+ background: $tertiary-fg-color;
+ mask-position: center;
+ mask-size: contain;
+ mask-repeat: no-repeat;
+ mask-image: url('$(res)/img/element-icons/settings.svg');
+ }
+ }
}
.mx_SpaceRoomView_landing_topic {
@@ -268,80 +332,6 @@ $SpaceRoomViewInnerWidth: 428px;
background-color: $groupFilterPanel-bg-color;
}
- .mx_SpaceRoomView_landing_adminButtons {
- margin-top: 24px;
-
- .mx_AccessibleButton {
- position: relative;
- width: 160px;
- height: 124px;
- box-sizing: border-box;
- padding: 72px 16px 0;
- border-radius: 12px;
- border: 1px solid $input-border-color;
- margin-right: 28px;
- margin-bottom: 20px;
- font-size: $font-14px;
- display: inline-block;
- vertical-align: bottom;
-
- &:last-child {
- margin-right: 0;
- }
-
- &:hover {
- background-color: rgba(141, 151, 165, 0.1);
- }
-
- &::before, &::after {
- position: absolute;
- content: "";
- left: 16px;
- top: 16px;
- height: 40px;
- width: 40px;
- border-radius: 20px;
- }
-
- &::after {
- mask-position: center;
- mask-size: 30px;
- mask-repeat: no-repeat;
- background: #ffffff; // white icon fill
- }
-
- &.mx_SpaceRoomView_landing_addButton {
- &::before {
- background-color: #ac3ba8;
- }
-
- &::after {
- mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
- }
- }
-
- &.mx_SpaceRoomView_landing_createButton {
- &::before {
- background-color: #368bd6;
- }
-
- &::after {
- mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
- }
- }
-
- &.mx_SpaceRoomView_landing_settingsButton {
- &::before {
- background-color: #5c56f5;
- }
-
- &::after {
- mask-image: url('$(res)/img/element-icons/settings.svg');
- }
- }
- }
- }
-
.mx_SearchBox {
margin: 0 0 20px;
}
@@ -362,6 +352,23 @@ $SpaceRoomViewInnerWidth: 428px;
}
.mx_SpaceRoomView_inviteTeammates {
+ // XXX remove this when spaces leaves Beta
+ .mx_SpaceRoomView_inviteTeammates_betaDisclaimer {
+ padding: 58px 16px 16px;
+ position: relative;
+ border-radius: 8px;
+ background-color: $header-panel-bg-color;
+ max-width: $SpaceRoomViewInnerWidth;
+ margin: 20px 0 30px;
+ box-sizing: border-box;
+
+ .mx_BetaCard_betaPill {
+ position: absolute;
+ left: 16px;
+ top: 16px;
+ }
+ }
+
.mx_SpaceRoomView_inviteTeammates_buttons {
color: $secondary-fg-color;
margin-top: 28px;
diff --git a/res/css/views/beta/_BetaCard.scss b/res/css/views/beta/_BetaCard.scss
new file mode 100644
index 0000000000..05149b7f6c
--- /dev/null
+++ b/res/css/views/beta/_BetaCard.scss
@@ -0,0 +1,112 @@
+/*
+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_BetaCard {
+ margin-bottom: 20px;
+ padding: 24px;
+ background-color: $settings-profile-placeholder-bg-color;
+ border-radius: 8px;
+ display: flex;
+ box-sizing: border-box;
+
+ > div {
+ .mx_BetaCard_title {
+ font-weight: $font-semi-bold;
+ font-size: $font-18px;
+ line-height: $font-22px;
+ color: $primary-fg-color;
+ margin: 4px 0 14px;
+
+ .mx_BetaCard_betaPill {
+ margin-left: 12px;
+ }
+ }
+
+ .mx_BetaCard_caption {
+ font-size: $font-15px;
+ line-height: $font-20px;
+ color: $secondary-fg-color;
+ }
+
+ .mx_AccessibleButton {
+ display: block;
+ margin: 20px 0;
+ padding: 12px 40px;
+ width: max-content;
+ }
+
+ .mx_BetaCard_disclaimer {
+ font-size: $font-12px;
+ line-height: $font-15px;
+ color: $secondary-fg-color;
+ }
+ }
+
+ > img {
+ margin: auto 0 auto 20px;
+ width: 300px;
+ object-fit: contain;
+ height: 100%;
+ }
+}
+
+.mx_BetaCard_betaPill {
+ background-color: $accent-color-alt;
+ padding: 4px 10px;
+ border-radius: 8px;
+ text-transform: uppercase;
+ font-size: 12px;
+ line-height: 15px;
+ color: #FFFFFF;
+ display: inline-block;
+ vertical-align: text-bottom;
+
+ &.mx_BetaCard_betaPill_clickable {
+ cursor: pointer;
+ }
+}
+
+$pulse-color: $accent-color-alt;
+$dot-size: 12px;
+
+.mx_BetaDot {
+ border-radius: 50%;
+ margin: 10px;
+ height: $dot-size;
+ width: $dot-size;
+ transform: scale(1);
+ background: rgba($pulse-color, 1);
+ box-shadow: 0 0 0 0 rgba($pulse-color, 1);
+ animation: mx_Beta_bluePulse 2s infinite;
+ animation-iteration-count: 20;
+}
+
+@keyframes mx_Beta_bluePulse {
+ 0% {
+ transform: scale(0.95);
+ box-shadow: 0 0 0 0 rgba($pulse-color, 0.7);
+ }
+
+ 70% {
+ transform: scale(1);
+ box-shadow: 0 0 0 10px rgba($pulse-color, 0);
+ }
+
+ 100% {
+ transform: scale(0.95);
+ box-shadow: 0 0 0 0 rgba($pulse-color, 0);
+ }
+}
diff --git a/res/css/views/elements/_AccessibleButton.scss b/res/css/views/elements/_AccessibleButton.scss
index 0075dcb511..2997c83cfd 100644
--- a/res/css/views/elements/_AccessibleButton.scss
+++ b/res/css/views/elements/_AccessibleButton.scss
@@ -76,12 +76,16 @@ limitations under the License.
border: 1px solid $button-danger-bg-color;
}
-.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled,
-.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
+.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
color: $button-danger-disabled-fg-color;
background-color: $button-danger-disabled-bg-color;
}
+.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
+ color: $button-danger-disabled-bg-color;
+ border-color: $button-danger-disabled-bg-color;
+}
+
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm {
padding: 5px 12px;
color: $button-danger-fg-color;
diff --git a/res/css/views/messages/_MFileBody.scss b/res/css/views/messages/_MFileBody.scss
index b45126acf8..c215d69ec2 100644
--- a/res/css/views/messages/_MFileBody.scss
+++ b/res/css/views/messages/_MFileBody.scss
@@ -61,9 +61,9 @@ limitations under the License.
.mx_MFileBody_info {
background-color: $message-body-panel-bg-color;
- border-radius: 4px;
- width: 270px;
- padding: 8px;
+ border-radius: 12px;
+ width: 243px; // same width as a playable voice message, accounting for padding
+ padding: 6px 12px;
color: $message-body-panel-fg-color;
.mx_MFileBody_info_icon {
@@ -82,7 +82,7 @@ limitations under the License.
mask-position: center;
mask-size: cover;
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
- background-color: $message-body-panel-fg-color;
+ background-color: $message-body-panel-icon-fg-color;
width: 13px;
height: 15px;
diff --git a/res/css/views/rooms/_VoiceRecordComposerTile.scss b/res/css/views/rooms/_VoiceRecordComposerTile.scss
index 15daf81672..a3ee104bd8 100644
--- a/res/css/views/rooms/_VoiceRecordComposerTile.scss
+++ b/res/css/views/rooms/_VoiceRecordComposerTile.scss
@@ -39,23 +39,25 @@ limitations under the License.
width: 14px; // w&h are size of icon
height: 18px;
vertical-align: middle;
- margin-right: 7px; // distance from left edge of waveform container (container has some margin too)
+ margin-right: 11px; // distance from left edge of waveform container (container has some margin too)
background-color: $voice-record-icon-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/trashcan.svg');
}
-.mx_VoiceMessagePrimaryContainer {
+.mx_MessageComposer_row .mx_VoiceMessagePrimaryContainer {
// Note: remaining class properties are in the PlayerContainer CSS.
margin: 6px; // force the composer area to put a gutter around us
- margin-right: 12px; // isolate from stop button
+ margin-right: 12px; // isolate from stop/send button
position: relative; // important for the live circle
&.mx_VoiceRecordComposerTile_recording {
- padding-left: 16px; // +10px for the live circle, +6px for regular padding
+ // We are putting the circle in this padding, so we need +10px from the regular
+ // padding on the left side.
+ padding-left: 22px;
&::before {
animation: recording-pulse 2s infinite;
@@ -65,8 +67,8 @@ limitations under the License.
width: 10px;
height: 10px;
position: absolute;
- left: 8px;
- top: 16px; // vertically center
+ left: 12px; // 12px from the left edge for container padding
+ top: 18px; // vertically center (middle align with clock)
border-radius: 10px;
}
}
diff --git a/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss b/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss
new file mode 100644
index 0000000000..540db48d65
--- /dev/null
+++ b/res/css/views/settings/tabs/user/_LabsUserSettingsTab.scss
@@ -0,0 +1,25 @@
+/*
+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_LabsUserSettingsTab {
+ .mx_SettingsTab_section {
+ margin-top: 32px;
+
+ .mx_SettingsFlag {
+ margin-right: 0; // remove right margin to align with beta cards
+ }
+ }
+}
diff --git a/res/css/views/spaces/_SpaceCreateMenu.scss b/res/css/views/spaces/_SpaceCreateMenu.scss
index ef3fea351b..ef15206d22 100644
--- a/res/css/views/spaces/_SpaceCreateMenu.scss
+++ b/res/css/views/spaces/_SpaceCreateMenu.scss
@@ -29,6 +29,7 @@ $spacePanelWidth: 71px;
width: 480px;
box-sizing: border-box;
background-color: $primary-bg-color;
+ position: relative;
> div {
> h2 {
@@ -44,6 +45,13 @@ $spacePanelWidth: 71px;
}
}
+ // XXX remove this when spaces leaves Beta
+ .mx_BetaCard_betaPill {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ }
+
.mx_SpaceCreateMenuType {
@mixin SpacePillButton;
}
diff --git a/res/css/views/voice_messages/_PlaybackContainer.scss b/res/css/views/voice_messages/_PlaybackContainer.scss
index 49bd81ef81..64e8f445e1 100644
--- a/res/css/views/voice_messages/_PlaybackContainer.scss
+++ b/res/css/views/voice_messages/_PlaybackContainer.scss
@@ -19,8 +19,9 @@ limitations under the License.
// Container for live recording and playback controls
.mx_VoiceMessagePrimaryContainer {
- padding: 6px; // makes us 4px taller than the send/stop button
- padding-right: 5px; // there's 1px from the waveform itself, so account for that
+ // 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;
background-color: $voice-record-waveform-bg-color;
border-radius: 12px;
@@ -30,11 +31,9 @@ limitations under the License.
color: $voice-record-waveform-fg-color;
font-size: $font-14px;
+ line-height: $font-24px;
.mx_Waveform {
- // We want the bars to be 2px shorter than the play/pause button in the waveform control
- height: 28px; // default is 30px, so we're subtracting the 2px border off the bars
-
.mx_Waveform_bar {
background-color: $voice-record-waveform-incomplete-fg-color;
@@ -47,8 +46,8 @@ limitations under the License.
}
.mx_Clock {
- padding-right: 4px; // isolate from waveform
- padding-left: 8px; // isolate from live circle
- width: 40px; // we're not using a monospace font, so fake it
+ width: 42px; // we're not using a monospace font, so fake it
+ padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
+ padding-left: 8px; // isolate from recording circle / play control
}
}
diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index 7292e325df..0be75be28c 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -65,14 +65,17 @@ limitations under the License.
}
}
-.mx_CallView_voice {
+.mx_CallView_content {
position: relative;
display: flex;
- flex-direction: column;
+ border-radius: 8px;
+}
+
+.mx_CallView_voice {
align-items: center;
justify-content: center;
+ flex-direction: column;
background-color: $inverted-bg-color;
- border-radius: 8px;
}
.mx_CallView_voice_avatarsContainer {
@@ -109,9 +112,7 @@ limitations under the License.
.mx_CallView_video {
width: 100%;
height: 100%;
- position: relative;
z-index: 30;
- border-radius: 8px;
overflow: hidden;
}
diff --git a/res/css/views/voip/_VideoFeed.scss b/res/css/views/voip/_VideoFeed.scss
index 8ead8bba3e..7d85ac264e 100644
--- a/res/css/views/voip/_VideoFeed.scss
+++ b/res/css/views/voip/_VideoFeed.scss
@@ -14,21 +14,37 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+.mx_VideoFeed_voice {
+ // We don't want to collide with the call controls that have 52px of height
+ padding-bottom: 52px;
+ background-color: $inverted-bg-color;
+}
+
+
.mx_VideoFeed_remote {
width: 100%;
height: 100%;
- background-color: #000;
- z-index: 50;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ &.mx_VideoFeed_video {
+ background-color: #000;
+ }
}
.mx_VideoFeed_local {
- width: 25%;
- height: 25%;
+ max-width: 25%;
+ max-height: 25%;
position: absolute;
right: 10px;
top: 10px;
z-index: 100;
border-radius: 4px;
+
+ &.mx_VideoFeed_video {
+ background-color: transparent;
+ }
}
.mx_VideoFeed_mirror {
diff --git a/res/img/betas/spaces.png b/res/img/betas/spaces.png
new file mode 100644
index 0000000000..87394e0a5b
Binary files /dev/null and b/res/img/betas/spaces.png differ
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index cfdda41619..2d0e3d2a8b 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6;
$header-panel-text-secondary-color: #c8c8cd;
$text-primary-color: #ffffff;
$text-secondary-color: #B9BEC6;
+$quaternary-fg-color: #6F7882;
$search-bg-color: #181b21;
$search-placeholder-color: #61708b;
$room-highlight-color: #343a46;
@@ -42,14 +43,6 @@ $preview-bar-bg-color: $header-panel-bg-color;
$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
$inverted-bg-color: $base-color;
-$voice-record-stop-border-color: #6F7882; // "Quarterly"
-$voice-record-waveform-bg-color: #394049; // "Dark Tile"
-$voice-record-waveform-fg-color: $tertiary-fg-color;
-$voice-record-waveform-incomplete-fg-color: #5b646d;
-$voice-record-icon-color: $tertiary-fg-color;
-$voice-playback-button-bg-color: $tertiary-fg-color;
-$voice-playback-button-fg-color: $bg-color;
-
// used by AddressSelector
$selected-color: $room-highlight-color;
@@ -213,9 +206,18 @@ $breadcrumb-placeholder-bg-color: #272c35;
$user-tile-hover-bg-color: $header-panel-bg-color;
-$message-body-panel-bg-color: #21262c82;
-$message-body-panel-icon-bg-color: #8e99a4;
-$message-body-panel-fg-color: $primary-fg-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;
+
+$voice-record-stop-border-color: $quaternary-fg-color;
+$voice-record-waveform-bg-color: $message-body-panel-bg-color;
+$voice-record-waveform-fg-color: $message-body-panel-fg-color;
+$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
+$voice-record-icon-color: $quaternary-fg-color;
+$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
+$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
// Appearance tab colors
$appearance-tab-border-color: $room-highlight-color;
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 6413a99ce0..a852ad94e9 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -124,15 +124,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
-// See non-legacy dark for variable information
-$voice-record-stop-border-color: #6F7882;
-$voice-record-waveform-bg-color: #394049;
-$voice-record-waveform-fg-color: $tertiary-fg-color;
-$voice-record-waveform-incomplete-fg-color: #5b646d;
-$voice-record-icon-color: $tertiary-fg-color;
-$voice-playback-button-bg-color: $tertiary-fg-color;
-$voice-playback-button-fg-color: $bg-color;
-
$roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #1A1D23;
@@ -209,9 +200,19 @@ $breadcrumb-placeholder-bg-color: #272c35;
$user-tile-hover-bg-color: $header-panel-bg-color;
-$message-body-panel-bg-color: #21262c82;
-$message-body-panel-icon-bg-color: #8e99a4;
-$message-body-panel-fg-color: $primary-fg-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;
+
+// See non-legacy dark for variable information
+$voice-record-stop-border-color: #6F7882;
+$voice-record-waveform-bg-color: $message-body-panel-bg-color;
+$voice-record-waveform-fg-color: $message-body-panel-fg-color;
+$voice-record-waveform-incomplete-fg-color: #6F7882;
+$voice-record-icon-color: #6F7882;
+$voice-playback-button-bg-color: $tertiary-fg-color;
+$voice-playback-button-fg-color: #21262C;
// Appearance tab colors
$appearance-tab-border-color: $room-highlight-color;
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 2151724071..84666bc662 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -191,17 +191,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
-// See non-legacy _light for variable information
-$voice-record-stop-border-color: #E3E8F0;
-$voice-record-stop-symbol-color: #ff4b55;
-$voice-record-waveform-bg-color: #E3E8F0;
-$voice-record-waveform-fg-color: $muted-fg-color;
-$voice-record-waveform-incomplete-fg-color: #C1C6CD;
-$voice-record-live-circle-color: #ff4b55;
-$voice-record-icon-color: $muted-fg-color;
-$voice-playback-button-bg-color: $primary-bg-color;
-$voice-playback-button-fg-color: $muted-fg-color;
-
$roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #fff;
@@ -334,9 +323,21 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
$user-tile-hover-bg-color: $header-panel-bg-color;
-$message-body-panel-bg-color: #e3e8f082;
-$message-body-panel-icon-bg-color: #ffffff;
-$message-body-panel-fg-color: $muted-fg-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;
+
+// See non-legacy _light for variable information
+$voice-record-stop-symbol-color: #ff4b55;
+$voice-record-live-circle-color: #ff4b55;
+$voice-record-stop-border-color: #E3E8F0;
+$voice-record-waveform-bg-color: $message-body-panel-bg-color;
+$voice-record-waveform-fg-color: $message-body-panel-fg-color;
+$voice-record-waveform-incomplete-fg-color: #C1C6CD;
+$voice-record-icon-color: $tertiary-fg-color;
+$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
+$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
// FontSlider colors
$appearance-tab-border-color: $input-darker-bg-color;
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 1763fcdd48..c889f43d0b 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
$primary-fg-color: #2e2f32;
$secondary-fg-color: #737D8C;
$tertiary-fg-color: #8D99A5;
+$quaternary-fg-color: #C1C6CD;
$header-panel-bg-color: #f3f8fd;
// typical text (dark-on-white in light skin)
@@ -182,16 +183,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
$groupFilterPanel-divider-color: $roomlist-header-color;
-$voice-record-stop-border-color: #E3E8F0;
-$voice-record-stop-symbol-color: #ff4b55; // $warning-color, but without letting people change it in themes
-$voice-record-waveform-bg-color: #E3E8F0;
-$voice-record-waveform-fg-color: $muted-fg-color;
-$voice-record-waveform-incomplete-fg-color: #C1C6CD;
-$voice-record-live-circle-color: #ff4b55; // $warning-color, but without letting people change it in themes
-$voice-record-icon-color: $muted-fg-color;
-$voice-playback-button-bg-color: $primary-bg-color;
-$voice-playback-button-fg-color: $muted-fg-color;
-
$roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b;
$roomtile-selected-bg-color: #FFF;
@@ -331,9 +322,23 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
$user-tile-hover-bg-color: $header-panel-bg-color;
-$message-body-panel-bg-color: #e3e8f082;
-$message-body-panel-icon-bg-color: #ffffff;
-$message-body-panel-fg-color: $muted-fg-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;
+
+// These two don't change between themes. They are the $warning-color, but we don't
+// want custom themes to affect them by accident.
+$voice-record-stop-symbol-color: #ff4b55;
+$voice-record-live-circle-color: #ff4b55;
+
+$voice-record-stop-border-color: #E3E8F0; // "Separator"
+$voice-record-waveform-bg-color: $message-body-panel-bg-color;
+$voice-record-waveform-fg-color: $message-body-panel-fg-color;
+$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
+$voice-record-icon-color: $tertiary-fg-color;
+$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
+$voice-playback-button-fg-color: $message-body-panel-icon-fg-color;
// FontSlider colors
$appearance-tab-border-color: $input-darker-bg-color;
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 0ab26ef943..e8f2f1bc08 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -118,6 +118,16 @@ declare global {
interface HTMLAudioElement {
type?: string;
+ // sinkId & setSinkId are experimental and typescript doesn't know about them
+ sinkId: string;
+ setSinkId(outputId: string);
+ }
+
+ interface HTMLVideoElement {
+ type?: string;
+ // sinkId & setSinkId are experimental and typescript doesn't know about them
+ sinkId: string;
+ setSinkId(outputId: string);
}
interface Element {
diff --git a/src/Avatar.ts b/src/Avatar.ts
index d218ae8b46..a6499c688e 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
import DMRoomMap from './utils/DMRoomMap';
import {mediaFromMxc} from "./customisations/Media";
+import SettingsStore from "./settings/SettingsStore";
export type ResizeMethod = "crop" | "scale";
@@ -143,7 +144,7 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
}
// space rooms cannot be DMs so skip the rest
- if (room.isSpaceRoom()) return null;
+ if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return null;
let otherMember = null;
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index 91fd7e4c7d..0268ebfe46 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -85,6 +85,7 @@ import { Action } from './dispatcher/actions';
import VoipUserMapper from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
+import EventEmitter from 'events';
import SdkConfig from './SdkConfig';
import { ensureDMExists, findDMForUser } from './createRoom';
@@ -138,22 +139,12 @@ export enum PlaceCallType {
ScreenSharing = 'screensharing',
}
-function getRemoteAudioElement(): HTMLAudioElement {
- // this needs to be somewhere at the top of the DOM which
- // always exists to avoid audio interruptions.
- // Might as well just use DOM.
- const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement;
- if (!remoteAudioElement) {
- console.error(
- "Failed to find remoteAudio element - cannot play audio!" +
- "You need to add an to the DOM.",
- );
- return null;
- }
- return remoteAudioElement;
+export enum CallHandlerEvent {
+ CallsChanged = "calls_changed",
+ CallChangeRoom = "call_change_room",
}
-export default class CallHandler {
+export default class CallHandler extends EventEmitter {
private calls = new Map(); // roomId -> call
// Calls started as an attended transfer, ie. with the intention of transferring another
// call with a different party to this one.
@@ -514,6 +505,7 @@ export default class CallHandler {
}
this.calls.set(mappedRoomId, newCall);
+ this.emit(CallHandlerEvent.CallsChanged, this.calls);
this.setCallListeners(newCall);
this.setCallState(newCall, newCall.state);
});
@@ -546,10 +538,7 @@ export default class CallHandler {
this.removeCallForRoom(mappedRoomId);
mappedRoomId = newMappedRoomId;
this.calls.set(mappedRoomId, call);
- dis.dispatch({
- action: Action.CallChangeRoom,
- call,
- });
+ this.emit(CallHandlerEvent.CallChangeRoom, call);
}
}
});
@@ -598,11 +587,6 @@ export default class CallHandler {
}
}
- private setCallAudioElement(call: MatrixCall) {
- const audioElement = getRemoteAudioElement();
- if (audioElement) call.setRemoteAudioElement(audioElement);
- }
-
private setCallState(call: MatrixCall, status: CallState) {
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
@@ -619,6 +603,7 @@ export default class CallHandler {
private removeCallForRoom(roomId: string) {
this.calls.delete(roomId);
+ this.emit(CallHandlerEvent.CallsChanged, this.calls);
}
private showICEFallbackPrompt() {
@@ -679,11 +664,7 @@ export default class CallHandler {
}, null, true);
}
- private async placeCall(
- roomId: string, type: PlaceCallType,
- localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
- transferee: MatrixCall,
- ) {
+ private async placeCall(roomId: string, type: PlaceCallType, transferee: MatrixCall) {
Analytics.trackEvent('voip', 'placeCall', 'type', type);
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
@@ -695,22 +676,19 @@ export default class CallHandler {
const call = MatrixClientPeg.get().createCall(mappedRoomId);
this.calls.set(roomId, call);
+ this.emit(CallHandlerEvent.CallsChanged, this.calls);
if (transferee) {
this.transferees[call.callId] = transferee;
}
this.setCallListeners(call);
- this.setCallAudioElement(call);
this.setActiveCallRoomId(roomId);
if (type === PlaceCallType.Voice) {
call.placeVoiceCall();
} else if (type === 'video') {
- call.placeVideoCall(
- remoteElement,
- localElement,
- );
+ call.placeVideoCall();
} else if (type === PlaceCallType.ScreenSharing) {
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
if (screenCapErrorString) {
@@ -724,13 +702,12 @@ export default class CallHandler {
}
call.placeScreenSharingCall(
- remoteElement,
- localElement,
async (): Promise => {
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
return source;
- });
+ },
+ );
} else {
console.error("Unknown conf call type: " + type);
}
@@ -787,17 +764,12 @@ export default class CallHandler {
} else if (members.length === 2) {
console.info(`Place ${payload.type} call in ${payload.room_id}`);
- this.placeCall(
- payload.room_id, payload.type, payload.local_element, payload.remote_element,
- payload.transferee,
- );
+ this.placeCall(payload.room_id, payload.type, payload.transferee);
} else { // > 2
dis.dispatch({
action: "place_conference_call",
room_id: payload.room_id,
type: payload.type,
- remote_element: payload.remote_element,
- local_element: payload.local_element,
});
}
}
@@ -833,6 +805,7 @@ export default class CallHandler {
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
this.calls.set(mappedRoomId, call)
+ this.emit(CallHandlerEvent.CallsChanged, this.calls);
this.setCallListeners(call);
// get ready to send encrypted events in the room, so if the user does answer
@@ -875,7 +848,6 @@ export default class CallHandler {
const call = this.calls.get(payload.room_id);
call.answer();
- this.setCallAudioElement(call);
this.setActiveCallRoomId(payload.room_id);
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
dis.dispatch({
diff --git a/src/CallMediaHandler.js b/src/CallMediaHandler.js
index 7c7940cab5..634f0bb336 100644
--- a/src/CallMediaHandler.js
+++ b/src/CallMediaHandler.js
@@ -16,7 +16,7 @@
import SettingsStore from "./settings/SettingsStore";
import {SettingLevel} from "./settings/SettingLevel";
-import {setMatrixCallAudioInput, setMatrixCallAudioOutput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
+import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
export default {
hasAnyLabeledDevices: async function() {
@@ -50,18 +50,15 @@ export default {
},
loadDevices: function() {
- const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
- setMatrixCallAudioOutput(audioOutDeviceId);
setMatrixCallAudioInput(audioDeviceId);
setMatrixCallVideoInput(videoDeviceId);
},
setAudioOutput: function(deviceId) {
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
- setMatrixCallAudioOutput(deviceId);
},
setAudioInput: function(deviceId) {
diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx
index 6b2568d68c..ef5ac383e3 100644
--- a/src/HtmlUtils.tsx
+++ b/src/HtmlUtils.tsx
@@ -422,8 +422,12 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
safeBody = sanitizeHtml(formattedBody, sanitizeParams);
if (SettingsStore.getValue("feature_latex_maths")) {
- const phtml = cheerio.load(safeBody,
- { _useHtmlParser2: true, decodeEntities: false })
+ const phtml = cheerio.load(safeBody, {
+ // @ts-ignore: The `_useHtmlParser2` internal option is the
+ // simplest way to both parse and render using `htmlparser2`.
+ _useHtmlParser2: true,
+ decodeEntities: false,
+ });
// @ts-ignore - The types for `replaceWith` wrongly expect
// Cheerio instance to be returned.
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
@@ -431,6 +435,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
{
throwOnError: false,
+ // @ts-ignore - `e` can be an Element, not just a Node
displayMode: e.name == 'div',
output: "htmlAndMathml",
});
diff --git a/src/SlashCommands.tsx b/src/SlashCommands.tsx
index 6ce1439164..4a7b37b5e5 100644
--- a/src/SlashCommands.tsx
+++ b/src/SlashCommands.tsx
@@ -38,7 +38,7 @@ 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 } from "parse5";
+import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
import BugReportDialog from "./components/views/dialogs/BugReportDialog";
import { ensureDMExists } from "./createRoom";
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
@@ -856,7 +856,7 @@ export const Commands = [
// some superfast regex over the text so we don't have to.
const embed = parseHtml(widgetUrl);
if (embed && embed.childNodes && embed.childNodes.length === 1) {
- const iframe = embed.childNodes[0];
+ const iframe = embed.childNodes[0] as ChildElement;
if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) {
const srcAttr = iframe.attrs.find(a => a.name === 'src');
console.log("Pulling URL out of iframe (embed code)");
diff --git a/src/components/structures/GroupFilterPanel.js b/src/components/structures/GroupFilterPanel.js
index 7c050e7433..2ff91e4976 100644
--- a/src/components/structures/GroupFilterPanel.js
+++ b/src/components/structures/GroupFilterPanel.js
@@ -123,12 +123,19 @@ class GroupFilterPanel extends React.Component {
mx_GroupFilterPanel_items_selected: itemsSelected,
});
+ let betaDot;
+ if (SettingsStore.getBetaInfo("feature_spaces") && !localStorage.getItem("mx_seenSpacesBeta")) {
+ betaDot = ;
+ }
+
let createButton = (
+ className="mx_TagTile mx_TagTile_plus">
+ { betaDot }
+
);
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
diff --git a/src/components/structures/LoggedInView.tsx b/src/components/structures/LoggedInView.tsx
index 0255a3bf35..c4b9696807 100644
--- a/src/components/structures/LoggedInView.tsx
+++ b/src/components/structures/LoggedInView.tsx
@@ -59,6 +59,9 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
import { IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
+import CallHandler, { CallHandlerEvent } from '../../CallHandler';
+import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
+import AudioFeedArrayForCall from '../views/voip/AudioFeedArrayForCall';
// We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity.
@@ -119,6 +122,7 @@ interface IState {
usageLimitEventContent?: IUsageLimit;
usageLimitEventTs?: number;
useCompactLayout: boolean;
+ activeCalls: Array;
}
/**
@@ -160,6 +164,7 @@ class LoggedInView extends React.Component {
// use compact timeline view
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
usageLimitDismissed: false,
+ activeCalls: [],
};
// stash the MatrixClient in case we log out before we are unmounted
@@ -175,6 +180,7 @@ class LoggedInView extends React.Component {
componentDidMount() {
document.addEventListener('keydown', this._onNativeKeyDown, false);
+ CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this._updateServerNoticeEvents();
@@ -199,6 +205,7 @@ class LoggedInView extends React.Component {
componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false);
+ CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
@@ -206,6 +213,12 @@ class LoggedInView extends React.Component {
this.resizer.detach();
}
+ private onCallsChanged = () => {
+ this.setState({
+ activeCalls: CallHandler.sharedInstance().getAllActiveCalls(),
+ });
+ };
+
// Child components assume that the client peg will not be null, so give them some
// sort of assurance here by only allowing a re-render if the client is truthy.
//
@@ -661,6 +674,12 @@ class LoggedInView extends React.Component {
bodyClasses += ' mx_MatrixChat_useCompactLayout';
}
+ const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
+ return (
+
+ );
+ });
+
return (
{
+ {audioFeedArraysForCalls}
);
}
diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx
index 078b296295..f01c1ea960 100644
--- a/src/components/structures/MatrixChat.tsx
+++ b/src/components/structures/MatrixChat.tsx
@@ -740,6 +740,8 @@ export default class MatrixChat extends React.PureComponent {
this.showScreenAfterLogin();
break;
case 'toggle_my_groups':
+ // persist that the user has interacted with this, use it to dismiss the beta dot
+ localStorage.setItem("mx_seenSpacesBeta", "1");
// We just dispatch the page change rather than have to worry about
// what the logic is for each of these branches.
if (this.state.page_type === PageTypes.MyGroups) {
@@ -1094,7 +1096,7 @@ export default class MatrixChat extends React.PureComponent {
private leaveRoomWarnings(roomId: string) {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
- const isSpace = roomToLeave?.isSpaceRoom();
+ const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
// Show a warning if there are additional complications.
const warnings = [];
@@ -1133,7 +1135,7 @@ export default class MatrixChat extends React.PureComponent {
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
const warnings = this.leaveRoomWarnings(roomId);
- const isSpace = roomToLeave?.isSpaceRoom();
+ const isSpace = SettingsStore.getValue("feature_spaces") && roomToLeave?.isSpaceRoom();
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
title: isSpace ? _t("Leave space") : _t("Leave room"),
description: (
@@ -1684,6 +1686,10 @@ export default class MatrixChat extends React.PureComponent {
const type = screen === "start_sso" ? "sso" : "cas";
PlatformPeg.get().startSingleSignOn(cli, type, this.getFragmentAfterLogin());
} else if (screen === 'groups') {
+ if (SettingsStore.getValue("feature_spaces")) {
+ dis.dispatch({ action: "view_home_page" });
+ return;
+ }
dis.dispatch({
action: 'view_my_groups',
});
@@ -1767,6 +1773,11 @@ export default class MatrixChat extends React.PureComponent {
subAction: params.action,
});
} else if (screen.indexOf('group/') === 0) {
+ if (SettingsStore.getValue("feature_spaces")) {
+ dis.dispatch({ action: "view_home_page" });
+ return;
+ }
+
const groupId = screen.substring(6);
// TODO: Check valid group ID
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index c93f07fa0f..e5e0065be8 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -544,11 +544,13 @@ export default class MessagePanel extends React.Component {
}
if (!grouper) {
const wantTile = this._shouldShowEvent(mxEv);
+ const isGrouped = false;
if (wantTile) {
// make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate.
- ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile));
+ ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, isGrouped,
+ nextEvent, nextTile));
prevEvent = mxEv;
}
@@ -564,7 +566,7 @@ export default class MessagePanel extends React.Component {
return ret;
}
- _getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
+ _getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) {
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator');
@@ -584,7 +586,7 @@ export default class MessagePanel extends React.Component {
// do we need a date separator since the last event?
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
- if (wantsDateSeparator) {
+ if (wantsDateSeparator && !isGrouped) {
const dateSeparator =
;
ret.push(dateSeparator);
}
@@ -968,9 +970,9 @@ class CreationGrouper {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
-
const panel = this.panel;
const ret = [];
+ const isGrouped = true;
const createEvent = this.createEvent;
const lastShownEvent = this.lastShownEvent;
@@ -984,12 +986,12 @@ class CreationGrouper {
// If this m.room.create event should be shown (room upgrade) then show it before the summary
if (panel._shouldShowEvent(createEvent)) {
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
- ret.push(...panel._getTilesForEvent(createEvent, createEvent, false));
+ ret.push(...panel._getTilesForEvent(createEvent, createEvent));
}
for (const ejected of this.ejectedEvents) {
ret.push(...panel._getTilesForEvent(
- createEvent, ejected, createEvent === lastShownEvent,
+ createEvent, ejected, createEvent === lastShownEvent, isGrouped,
));
}
@@ -998,7 +1000,7 @@ class CreationGrouper {
// of EventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
- return panel._getTilesForEvent(e, e, e === lastShownEvent);
+ return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1];
@@ -1083,7 +1085,7 @@ class RedactionGrouper {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
-
+ const isGrouped = true;
const panel = this.panel;
const ret = [];
const lastShownEvent = this.lastShownEvent;
@@ -1103,7 +1105,8 @@ class RedactionGrouper {
let eventTiles = this.events.map((e, i) => {
senders.add(e.sender);
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
- return panel._getTilesForEvent(prevEvent, e, e === lastShownEvent, this.nextEvent, this.nextEventTile);
+ return panel._getTilesForEvent(
+ prevEvent, e, e === lastShownEvent, isGrouped, this.nextEvent, this.nextEventTile);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
@@ -1182,7 +1185,7 @@ class MemberGrouper {
const DateSeparator = sdk.getComponent('messages.DateSeparator');
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
-
+ const isGrouped = true;
const panel = this.panel;
const lastShownEvent = this.lastShownEvent;
const ret = [];
@@ -1215,7 +1218,7 @@ class MemberGrouper {
// of MemberEventListSummary, render each member event as if the previous
// one was itself. This way, the timestamp of the previous event === the
// timestamp of the current event, and no DateSeparator is inserted.
- return panel._getTilesForEvent(e, e, e === lastShownEvent);
+ return panel._getTilesForEvent(e, e, e === lastShownEvent, isGrouped);
}).reduce((a, b) => a.concat(b), []);
if (eventTiles.length === 0) {
diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js
index 2ab11dad25..1fab6c4348 100644
--- a/src/components/structures/MyGroups.js
+++ b/src/components/structures/MyGroups.js
@@ -25,6 +25,7 @@ import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
+import BetaCard from "../views/beta/BetaCard";
@replaceableComponent("structures.MyGroups")
export default class MyGroups extends React.Component {
@@ -139,6 +140,7 @@ export default class MyGroups extends React.Component {
*/}
+
{ contentHeader }
{ content }
diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js
index 5bcb3b2450..d8c763eabd 100644
--- a/src/components/structures/RightPanel.js
+++ b/src/components/structures/RightPanel.js
@@ -35,6 +35,7 @@ import {Action} from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
+import SettingsStore from "../../settings/SettingsStore";
@replaceableComponent("structures.RightPanel")
export default class RightPanel extends React.Component {
@@ -85,7 +86,9 @@ export default class RightPanel extends React.Component {
return RightPanelPhases.GroupMemberList;
}
return rps.groupPanelPhase;
- } else if (this.props.room?.isSpaceRoom() && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)) {
+ } else if (SettingsStore.getValue("feature_spaces") && this.props.room?.isSpaceRoom()
+ && !RIGHT_PANEL_SPACE_PHASES.includes(rps.roomPanelPhase)
+ ) {
return RightPanelPhases.SpaceMemberList;
} else if (userForPanel) {
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
diff --git a/src/components/structures/RoomSearch.tsx b/src/components/structures/RoomSearch.tsx
index 34682877e0..bda46aef07 100644
--- a/src/components/structures/RoomSearch.tsx
+++ b/src/components/structures/RoomSearch.tsx
@@ -27,8 +27,8 @@ import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
-import {replaceableComponent} from "../../utils/replaceableComponent";
-import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore";
+import { replaceableComponent } from "../../utils/replaceableComponent";
+import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
interface IProps {
isMinimized: boolean;
diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx
index 5108643673..c0f3c59457 100644
--- a/src/components/structures/RoomView.tsx
+++ b/src/components/structures/RoomView.tsx
@@ -1750,7 +1750,10 @@ export default class RoomView extends React.Component {
}
const myMembership = this.state.room.getMyMembership();
- if (myMembership === "invite" && !this.state.room.isSpaceRoom()) { // SpaceRoomView handles invites itself
+ if (myMembership === "invite"
+ // SpaceRoomView handles invites itself
+ && (!SettingsStore.getValue("feature_spaces") || !this.state.room.isSpaceRoom())
+ ) {
if (this.state.joining || this.state.rejecting) {
return (
@@ -1892,7 +1895,7 @@ export default class RoomView extends React.Component {
room={this.state.room}
/>
);
- if (!this.state.canPeek && !this.state.room?.isSpaceRoom()) {
+ if (!this.state.canPeek && (!SettingsStore.getValue("feature_spaces") || !this.state.room?.isSpaceRoom())) {
return (
- { _t("Make sure the right people have access to %(name)s", { name: space.name }) }
+ { _t("Make sure the right people have access to %(name)s", {
+ name: justCreatedOpts?.createOpts?.name || space.name,
+ }) }
{
/>;
});
- const onNextClick = async () => {
+ const onNextClick = async (ev) => {
+ ev.preventDefault();
+ if (busy) return;
setError("");
for (let i = 0; i < fieldRefs.length; i++) {
const fieldRef = fieldRefs[i];
@@ -552,7 +669,10 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
setBusy(false);
};
- let onClick = onFinished;
+ let onClick = (ev) => {
+ ev.preventDefault();
+ onFinished();
+ };
let buttonLabel = _t("Skip for now");
if (emailAddresses.some(name => name.trim())) {
onClick = onNextClick;
@@ -565,8 +685,21 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
{ _t("Make sure the right people have access. You can invite more later.") }
+
+
+ { _t("This is an experimental feature. For now, " +
+ "new users receiving an invite will have to open the invite on to actually join.", {}, {
+ b: sub => { sub },
+ link: () =>
+ app.element.io
+ ,
+ }) }
+
+
{ error &&
{ error }
}
- { fields }
+
{
-
- { buttonLabel }
-
+
;
};
@@ -702,7 +841,7 @@ export default class SpaceRoomView extends React.PureComponent {
private renderBody() {
switch (this.state.phase) {
case Phase.Landing:
- if (this.state.myMembership === "join") {
+ if (this.state.myMembership === "join" && SettingsStore.getValue("feature_spaces")) {
return ;
} else {
return {
return {
onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>;
case Phase.PublicShare:
- return ;
+ return ;
case Phase.PrivateScope:
return {
this.setState({ phase: invite ? Phase.PrivateInvite : Phase.PrivateExistingRooms });
}}
diff --git a/src/components/views/beta/BetaCard.tsx b/src/components/views/beta/BetaCard.tsx
new file mode 100644
index 0000000000..96b1fbabe5
--- /dev/null
+++ b/src/components/views/beta/BetaCard.tsx
@@ -0,0 +1,90 @@
+/*
+Copyright 2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import React from "react";
+import classNames from "classnames";
+
+import {_t} from "../../../languageHandler";
+import AccessibleButton from "../elements/AccessibleButton";
+import SettingsStore from "../../../settings/SettingsStore";
+import {SettingLevel} from "../../../settings/SettingLevel";
+import TextWithTooltip from "../elements/TextWithTooltip";
+
+interface IProps {
+ title?: string;
+ featureId: string;
+}
+
+export const BetaPill = ({ onClick }: { onClick?: () => void }) => {
+ if (onClick) {
+ return
+