Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into spaces-jump-to-room
This commit is contained in:
commit
1974d47e4e
70 changed files with 1633 additions and 784 deletions
112
CHANGELOG.md
112
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)
|
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)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0)
|
||||||
|
|
|
@ -28,7 +28,7 @@ Platform Targets:
|
||||||
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
* WebRTC features (VoIP and Video calling) are only available in Chrome & Firefox.
|
||||||
* Mobile Web is not currently a target platform - instead please use the native
|
* Mobile Web is not currently a target platform - instead please use the native
|
||||||
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
iOS (https://github.com/matrix-org/matrix-ios-kit) and Android
|
||||||
(https://github.com/matrix-org/matrix-android-sdk) SDKs.
|
(https://github.com/matrix-org/matrix-android-sdk2) SDKs.
|
||||||
|
|
||||||
All code lands on the `develop` branch - `master` is only used for stable releases.
|
All code lands on the `develop` branch - `master` is only used for stable releases.
|
||||||
**Please file PRs against `develop`!!**
|
**Please file PRs against `develop`!!**
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.19.0",
|
"version": "3.20.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -97,7 +97,7 @@
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"resize-observer-polyfill": "^1.5.1",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"rfc4648": "^1.4.0",
|
"rfc4648": "^1.4.0",
|
||||||
"sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db",
|
"sanitize-html": "^2.3.2",
|
||||||
"tar-js": "^0.3.0",
|
"tar-js": "^0.3.0",
|
||||||
"text-encoding-utf-8": "^1.0.2",
|
"text-encoding-utf-8": "^1.0.2",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
|
@ -132,6 +132,7 @@
|
||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
"@types/node": "^14.14.22",
|
"@types/node": "^14.14.22",
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^1.0.1",
|
||||||
|
"@types/parse5": "^6.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "^16.9",
|
"@types/react": "^16.9",
|
||||||
"@types/react-dom": "^16.9.10",
|
"@types/react-dom": "^16.9.10",
|
||||||
|
|
|
@ -237,7 +237,6 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
|
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
// Create a flexbox to make aligning dot badges easier
|
// Create a flexbox to make aligning dot badges easier
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -249,23 +248,37 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
|
|
||||||
.mx_NotificationBadge_dot {
|
.mx_NotificationBadge_dot {
|
||||||
// make the smaller dot occupy the same width for centering
|
// make the smaller dot occupy the same width for centering
|
||||||
margin-left: 7px;
|
margin: 0 7px;
|
||||||
margin-right: 7px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.collapsed {
|
&.collapsed {
|
||||||
.mx_SpaceButton {
|
.mx_SpaceButton {
|
||||||
.mx_SpacePanel_badgeContainer {
|
.mx_SpacePanel_badgeContainer {
|
||||||
right: -3px;
|
right: 0;
|
||||||
top: -3px;
|
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 {
|
&.mx_SpaceButton_active .mx_SpacePanel_badgeContainer {
|
||||||
// when we draw the selection border we move the relative bounds of our parent
|
// 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
|
// so update our position within the bounds of the parent to maintain position overall
|
||||||
right: -6px;
|
right: -3px;
|
||||||
top: -6px;
|
top: -3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ limitations under the License.
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
padding: 2px 8px;
|
padding: 4px 12px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
|
|
||||||
& + .mx_AccessibleButton {
|
& + .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 {
|
> span {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
@ -246,11 +251,17 @@ limitations under the License.
|
||||||
grid-row: 1/3;
|
grid-row: 1/3;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
padding: 8px 18px;
|
line-height: $font-24px;
|
||||||
|
padding: 4px 16px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_danger_outline,
|
||||||
|
.mx_AccessibleButton_kind_primary_outline {
|
||||||
|
padding: 3px 16px; // to account for the 1px border
|
||||||
|
}
|
||||||
|
|
||||||
.mx_Checkbox {
|
.mx_Checkbox {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
|
|
@ -238,7 +238,8 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
|
|
||||||
.mx_SpaceRoomView_landing_inviteButton {
|
.mx_SpaceRoomView_landing_inviteButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 40px;
|
padding: 4px 18px 4px 40px;
|
||||||
|
line-height: $font-24px;
|
||||||
height: min-content;
|
height: min-content;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
@ -254,6 +255,27 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
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 {
|
.mx_SpaceRoomView_landing_topic {
|
||||||
|
@ -268,80 +290,6 @@ $SpaceRoomViewInnerWidth: 428px;
|
||||||
background-color: $groupFilterPanel-bg-color;
|
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 {
|
.mx_SearchBox {
|
||||||
margin: 0 0 20px;
|
margin: 0 0 20px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_BaseAvatar {
|
.mx_BaseAvatar {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
margin: 5px 16px 5px 5px;
|
margin: auto 16px auto 5px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,31 +160,32 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_errorText {
|
|
||||||
font-weight: $font-semi-bold;
|
|
||||||
font-size: $font-12px;
|
|
||||||
line-height: $font-15px;
|
|
||||||
color: $notice-primary-color;
|
|
||||||
margin-bottom: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddExistingToSpace {
|
.mx_AddExistingToSpace {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AddExistingToSpaceDialog_footer {
|
.mx_AddExistingToSpaceDialog_footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-top: 32px;
|
margin-top: 20px;
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
font-size: $font-14px;
|
font-size: $font-12px;
|
||||||
line-height: $font-15px;
|
line-height: $font-15px;
|
||||||
font-weight: $font-semi-bold;
|
color: $secondary-fg-color;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_ProgressBar {
|
||||||
font-size: inherit;
|
height: 8px;
|
||||||
display: inline-block;
|
width: 100%;
|
||||||
|
|
||||||
|
@mixin ProgressBarBorderRadius 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_progressText {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
|
@ -192,8 +193,54 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_error {
|
||||||
|
padding-left: 12px;
|
||||||
|
|
||||||
|
> img {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_errorHeading {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $notice-primary-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_errorCaption {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_primary {
|
||||||
|
padding: 8px 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AddExistingToSpaceDialog_retryButton {
|
||||||
|
margin-left: 12px;
|
||||||
|
padding-left: 24px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_link {
|
.mx_AccessibleButton_kind_link {
|
||||||
|
|
|
@ -76,12 +76,16 @@ limitations under the License.
|
||||||
border: 1px solid $button-danger-bg-color;
|
border: 1px solid $button-danger-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled,
|
.mx_AccessibleButton_kind_danger.mx_AccessibleButton_disabled {
|
||||||
.mx_AccessibleButton_kind_danger_outline.mx_AccessibleButton_disabled {
|
|
||||||
color: $button-danger-disabled-fg-color;
|
color: $button-danger-disabled-fg-color;
|
||||||
background-color: $button-danger-disabled-bg-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 {
|
.mx_AccessibleButton_hasKind.mx_AccessibleButton_kind_danger_sm {
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
color: $button-danger-fg-color;
|
color: $button-danger-fg-color;
|
||||||
|
|
|
@ -21,7 +21,7 @@ progress.mx_ProgressBar {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
@mixin ProgressBarBorderRadius "6px";
|
@mixin ProgressBarBorderRadius 6px;
|
||||||
@mixin ProgressBarColour $progressbar-fg-color;
|
@mixin ProgressBarColour $progressbar-fg-color;
|
||||||
@mixin ProgressBarBgColour $progressbar-bg-color;
|
@mixin ProgressBarBgColour $progressbar-bg-color;
|
||||||
::-webkit-progress-value {
|
::-webkit-progress-value {
|
||||||
|
|
|
@ -61,9 +61,9 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_MFileBody_info {
|
.mx_MFileBody_info {
|
||||||
background-color: $message-body-panel-bg-color;
|
background-color: $message-body-panel-bg-color;
|
||||||
border-radius: 4px;
|
border-radius: 12px;
|
||||||
width: 270px;
|
width: 243px; // same width as a playable voice message, accounting for padding
|
||||||
padding: 8px;
|
padding: 6px 12px;
|
||||||
color: $message-body-panel-fg-color;
|
color: $message-body-panel-fg-color;
|
||||||
|
|
||||||
.mx_MFileBody_info_icon {
|
.mx_MFileBody_info_icon {
|
||||||
|
@ -82,7 +82,7 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: cover;
|
mask-size: cover;
|
||||||
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
|
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;
|
width: 13px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
|
|
||||||
|
|
|
@ -39,23 +39,25 @@ limitations under the License.
|
||||||
width: 14px; // w&h are size of icon
|
width: 14px; // w&h are size of icon
|
||||||
height: 18px;
|
height: 18px;
|
||||||
vertical-align: middle;
|
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;
|
background-color: $voice-record-icon-color;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
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.
|
// Note: remaining class properties are in the PlayerContainer CSS.
|
||||||
|
|
||||||
margin: 6px; // force the composer area to put a gutter around us
|
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
|
position: relative; // important for the live circle
|
||||||
|
|
||||||
&.mx_VoiceRecordComposerTile_recording {
|
&.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 {
|
&::before {
|
||||||
animation: recording-pulse 2s infinite;
|
animation: recording-pulse 2s infinite;
|
||||||
|
@ -65,8 +67,8 @@ limitations under the License.
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 8px;
|
left: 12px; // 12px from the left edge for container padding
|
||||||
top: 16px; // vertically center
|
top: 18px; // vertically center (middle align with clock)
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,3 +22,34 @@ limitations under the License.
|
||||||
.mx_HelpUserSettingsTab span.mx_AccessibleButton {
|
.mx_HelpUserSettingsTab span.mx_AccessibleButton {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_HelpUserSettingsTab code {
|
||||||
|
word-break: break-all;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_HelpUserSettingsTab_accessToken {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: solid 1px $light-fg-color;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_HelpUserSettingsTab_accessToken_copy {
|
||||||
|
flex-shrink: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 20px;
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_HelpUserSettingsTab_accessToken_copy > div {
|
||||||
|
mask-image: url($copy-button-url);
|
||||||
|
background-color: $message-action-bar-fg-color;
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
|
@ -19,8 +19,9 @@ limitations under the License.
|
||||||
|
|
||||||
// Container for live recording and playback controls
|
// Container for live recording and playback controls
|
||||||
.mx_VoiceMessagePrimaryContainer {
|
.mx_VoiceMessagePrimaryContainer {
|
||||||
padding: 6px; // makes us 4px taller than the send/stop button
|
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
|
||||||
padding-right: 5px; // there's 1px from the waveform itself, so account for that
|
// has a 1px padding on it that we want to account for.
|
||||||
|
padding: 7px 12px 7px 11px;
|
||||||
background-color: $voice-record-waveform-bg-color;
|
background-color: $voice-record-waveform-bg-color;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
|
|
||||||
|
@ -30,11 +31,9 @@ limitations under the License.
|
||||||
|
|
||||||
color: $voice-record-waveform-fg-color;
|
color: $voice-record-waveform-fg-color;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
|
||||||
.mx_Waveform {
|
.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 {
|
.mx_Waveform_bar {
|
||||||
background-color: $voice-record-waveform-incomplete-fg-color;
|
background-color: $voice-record-waveform-incomplete-fg-color;
|
||||||
|
|
||||||
|
@ -47,8 +46,8 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Clock {
|
.mx_Clock {
|
||||||
padding-right: 4px; // isolate from waveform
|
width: 42px; // we're not using a monospace font, so fake it
|
||||||
padding-left: 8px; // isolate from live circle
|
padding-right: 6px; // with the fixed width this ends up as a visual 8px most of the time, as intended.
|
||||||
width: 40px; // we're not using a monospace font, so fake it
|
padding-left: 8px; // isolate from recording circle / play control
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,14 +65,17 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_voice {
|
.mx_CallView_content {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallView_voice {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
background-color: $inverted-bg-color;
|
background-color: $inverted-bg-color;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CallView_voice_avatarsContainer {
|
.mx_CallView_voice_avatarsContainer {
|
||||||
|
@ -109,9 +112,7 @@ limitations under the License.
|
||||||
.mx_CallView_video {
|
.mx_CallView_video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: relative;
|
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
border-radius: 8px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,21 +14,37 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 {
|
.mx_VideoFeed_remote {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&.mx_VideoFeed_video {
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
z-index: 50;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_local {
|
.mx_VideoFeed_local {
|
||||||
width: 25%;
|
max-width: 25%;
|
||||||
height: 25%;
|
max-height: 25%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
|
||||||
|
&.mx_VideoFeed_video {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoFeed_mirror {
|
.mx_VideoFeed_mirror {
|
||||||
|
|
|
@ -9,6 +9,7 @@ $header-panel-text-primary-color: #B9BEC6;
|
||||||
$header-panel-text-secondary-color: #c8c8cd;
|
$header-panel-text-secondary-color: #c8c8cd;
|
||||||
$text-primary-color: #ffffff;
|
$text-primary-color: #ffffff;
|
||||||
$text-secondary-color: #B9BEC6;
|
$text-secondary-color: #B9BEC6;
|
||||||
|
$quaternary-fg-color: #6F7882;
|
||||||
$search-bg-color: #181b21;
|
$search-bg-color: #181b21;
|
||||||
$search-placeholder-color: #61708b;
|
$search-placeholder-color: #61708b;
|
||||||
$room-highlight-color: #343a46;
|
$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);
|
$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
|
||||||
$inverted-bg-color: $base-color;
|
$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
|
// used by AddressSelector
|
||||||
$selected-color: $room-highlight-color;
|
$selected-color: $room-highlight-color;
|
||||||
|
|
||||||
|
@ -213,9 +206,18 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-bg-color: #21262c82;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #8e99a4;
|
$message-body-panel-bg-color: #394049; // "Dark Tile"
|
||||||
$message-body-panel-fg-color: $primary-fg-color;
|
$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 colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
|
@ -124,15 +124,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%)
|
||||||
|
|
||||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
$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-preview-color: #9e9e9e;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
$roomtile-selected-bg-color: #1A1D23;
|
$roomtile-selected-bg-color: #1A1D23;
|
||||||
|
@ -209,9 +200,19 @@ $breadcrumb-placeholder-bg-color: #272c35;
|
||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-bg-color: #21262c82;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #8e99a4;
|
$message-body-panel-bg-color: #394049;
|
||||||
$message-body-panel-fg-color: $primary-fg-color;
|
$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 colors
|
||||||
$appearance-tab-border-color: $room-highlight-color;
|
$appearance-tab-border-color: $room-highlight-color;
|
||||||
|
|
|
@ -191,17 +191,6 @@ $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%)
|
||||||
|
|
||||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
$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-preview-color: #9e9e9e;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
$roomtile-selected-bg-color: #fff;
|
$roomtile-selected-bg-color: #fff;
|
||||||
|
@ -334,9 +323,21 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-bg-color: #e3e8f082;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #ffffff;
|
$message-body-panel-bg-color: #E3E8F0;
|
||||||
$message-body-panel-fg-color: $muted-fg-color;
|
$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
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
|
@ -21,6 +21,7 @@ $notice-primary-bg-color: rgba(255, 75, 85, 0.16);
|
||||||
$primary-fg-color: #2e2f32;
|
$primary-fg-color: #2e2f32;
|
||||||
$secondary-fg-color: #737D8C;
|
$secondary-fg-color: #737D8C;
|
||||||
$tertiary-fg-color: #8D99A5;
|
$tertiary-fg-color: #8D99A5;
|
||||||
|
$quaternary-fg-color: #C1C6CD;
|
||||||
$header-panel-bg-color: #f3f8fd;
|
$header-panel-bg-color: #f3f8fd;
|
||||||
|
|
||||||
// typical text (dark-on-white in light skin)
|
// 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;
|
$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-preview-color: $secondary-fg-color;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
$roomtile-selected-bg-color: #FFF;
|
$roomtile-selected-bg-color: #FFF;
|
||||||
|
@ -331,9 +322,23 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
|
||||||
|
|
||||||
$user-tile-hover-bg-color: $header-panel-bg-color;
|
$user-tile-hover-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$message-body-panel-bg-color: #e3e8f082;
|
$message-body-panel-fg-color: $secondary-fg-color;
|
||||||
$message-body-panel-icon-bg-color: #ffffff;
|
$message-body-panel-bg-color: #E3E8F0; // "Separator"
|
||||||
$message-body-panel-fg-color: $muted-fg-color;
|
$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
|
// FontSlider colors
|
||||||
$appearance-tab-border-color: $input-darker-bg-color;
|
$appearance-tab-border-color: $input-darker-bg-color;
|
||||||
|
|
10
src/@types/global.d.ts
vendored
10
src/@types/global.d.ts
vendored
|
@ -118,6 +118,16 @@ declare global {
|
||||||
|
|
||||||
interface HTMLAudioElement {
|
interface HTMLAudioElement {
|
||||||
type?: string;
|
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 {
|
interface Element {
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
import {mediaFromMxc} from "./customisations/Media";
|
import {mediaFromMxc} from "./customisations/Media";
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
export type ResizeMethod = "crop" | "scale";
|
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
|
// 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;
|
let otherMember = null;
|
||||||
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
|
|
|
@ -85,6 +85,7 @@ import { Action } from './dispatcher/actions';
|
||||||
import VoipUserMapper from './VoipUserMapper';
|
import VoipUserMapper from './VoipUserMapper';
|
||||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
||||||
import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
|
import { randomUppercaseString, randomLowercaseString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import EventEmitter from 'events';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import { ensureDMExists, findDMForUser } from './createRoom';
|
import { ensureDMExists, findDMForUser } from './createRoom';
|
||||||
|
|
||||||
|
@ -138,22 +139,12 @@ export enum PlaceCallType {
|
||||||
ScreenSharing = 'screensharing',
|
ScreenSharing = 'screensharing',
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRemoteAudioElement(): HTMLAudioElement {
|
export enum CallHandlerEvent {
|
||||||
// this needs to be somewhere at the top of the DOM which
|
CallsChanged = "calls_changed",
|
||||||
// always exists to avoid audio interruptions.
|
CallChangeRoom = "call_change_room",
|
||||||
// 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 <audio/> to the DOM.",
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return remoteAudioElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CallHandler {
|
export default class CallHandler extends EventEmitter {
|
||||||
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
private calls = new Map<string, MatrixCall>(); // roomId -> call
|
||||||
// Calls started as an attended transfer, ie. with the intention of transferring another
|
// Calls started as an attended transfer, ie. with the intention of transferring another
|
||||||
// call with a different party to this one.
|
// call with a different party to this one.
|
||||||
|
@ -514,6 +505,7 @@ export default class CallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.calls.set(mappedRoomId, newCall);
|
this.calls.set(mappedRoomId, newCall);
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
this.setCallListeners(newCall);
|
this.setCallListeners(newCall);
|
||||||
this.setCallState(newCall, newCall.state);
|
this.setCallState(newCall, newCall.state);
|
||||||
});
|
});
|
||||||
|
@ -546,10 +538,7 @@ export default class CallHandler {
|
||||||
this.removeCallForRoom(mappedRoomId);
|
this.removeCallForRoom(mappedRoomId);
|
||||||
mappedRoomId = newMappedRoomId;
|
mappedRoomId = newMappedRoomId;
|
||||||
this.calls.set(mappedRoomId, call);
|
this.calls.set(mappedRoomId, call);
|
||||||
dis.dispatch({
|
this.emit(CallHandlerEvent.CallChangeRoom, call);
|
||||||
action: Action.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) {
|
private setCallState(call: MatrixCall, status: CallState) {
|
||||||
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
const mappedRoomId = CallHandler.sharedInstance().roomIdForCall(call);
|
||||||
|
|
||||||
|
@ -619,6 +603,7 @@ export default class CallHandler {
|
||||||
|
|
||||||
private removeCallForRoom(roomId: string) {
|
private removeCallForRoom(roomId: string) {
|
||||||
this.calls.delete(roomId);
|
this.calls.delete(roomId);
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
}
|
}
|
||||||
|
|
||||||
private showICEFallbackPrompt() {
|
private showICEFallbackPrompt() {
|
||||||
|
@ -679,11 +664,7 @@ export default class CallHandler {
|
||||||
}, null, true);
|
}, null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async placeCall(
|
private async placeCall(roomId: string, type: PlaceCallType, transferee: MatrixCall) {
|
||||||
roomId: string, type: PlaceCallType,
|
|
||||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
|
||||||
transferee: MatrixCall,
|
|
||||||
) {
|
|
||||||
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
||||||
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
|
||||||
|
|
||||||
|
@ -695,22 +676,19 @@ export default class CallHandler {
|
||||||
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
const call = MatrixClientPeg.get().createCall(mappedRoomId);
|
||||||
|
|
||||||
this.calls.set(roomId, call);
|
this.calls.set(roomId, call);
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
if (transferee) {
|
if (transferee) {
|
||||||
this.transferees[call.callId] = transferee;
|
this.transferees[call.callId] = transferee;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
this.setCallAudioElement(call);
|
|
||||||
|
|
||||||
this.setActiveCallRoomId(roomId);
|
this.setActiveCallRoomId(roomId);
|
||||||
|
|
||||||
if (type === PlaceCallType.Voice) {
|
if (type === PlaceCallType.Voice) {
|
||||||
call.placeVoiceCall();
|
call.placeVoiceCall();
|
||||||
} else if (type === 'video') {
|
} else if (type === 'video') {
|
||||||
call.placeVideoCall(
|
call.placeVideoCall();
|
||||||
remoteElement,
|
|
||||||
localElement,
|
|
||||||
);
|
|
||||||
} else if (type === PlaceCallType.ScreenSharing) {
|
} else if (type === PlaceCallType.ScreenSharing) {
|
||||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||||
if (screenCapErrorString) {
|
if (screenCapErrorString) {
|
||||||
|
@ -724,13 +702,12 @@ export default class CallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
call.placeScreenSharingCall(
|
call.placeScreenSharingCall(
|
||||||
remoteElement,
|
|
||||||
localElement,
|
|
||||||
async (): Promise<DesktopCapturerSource> => {
|
async (): Promise<DesktopCapturerSource> => {
|
||||||
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
|
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
const [source] = await finished;
|
const [source] = await finished;
|
||||||
return source;
|
return source;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error("Unknown conf call type: " + type);
|
console.error("Unknown conf call type: " + type);
|
||||||
}
|
}
|
||||||
|
@ -787,17 +764,12 @@ export default class CallHandler {
|
||||||
} else if (members.length === 2) {
|
} else if (members.length === 2) {
|
||||||
console.info(`Place ${payload.type} call in ${payload.room_id}`);
|
console.info(`Place ${payload.type} call in ${payload.room_id}`);
|
||||||
|
|
||||||
this.placeCall(
|
this.placeCall(payload.room_id, payload.type, payload.transferee);
|
||||||
payload.room_id, payload.type, payload.local_element, payload.remote_element,
|
|
||||||
payload.transferee,
|
|
||||||
);
|
|
||||||
} else { // > 2
|
} else { // > 2
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "place_conference_call",
|
action: "place_conference_call",
|
||||||
room_id: payload.room_id,
|
room_id: payload.room_id,
|
||||||
type: payload.type,
|
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);
|
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||||
this.calls.set(mappedRoomId, call)
|
this.calls.set(mappedRoomId, call)
|
||||||
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
|
||||||
// get ready to send encrypted events in the room, so if the user does answer
|
// 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);
|
const call = this.calls.get(payload.room_id);
|
||||||
call.answer();
|
call.answer();
|
||||||
this.setCallAudioElement(call);
|
|
||||||
this.setActiveCallRoomId(payload.room_id);
|
this.setActiveCallRoomId(payload.room_id);
|
||||||
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import {SettingLevel} from "./settings/SettingLevel";
|
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 {
|
export default {
|
||||||
hasAnyLabeledDevices: async function() {
|
hasAnyLabeledDevices: async function() {
|
||||||
|
@ -50,18 +50,15 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
loadDevices: function() {
|
loadDevices: function() {
|
||||||
const audioOutDeviceId = SettingsStore.getValue("webrtc_audiooutput");
|
|
||||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
|
|
||||||
setMatrixCallAudioOutput(audioOutDeviceId);
|
|
||||||
setMatrixCallAudioInput(audioDeviceId);
|
setMatrixCallAudioInput(audioDeviceId);
|
||||||
setMatrixCallVideoInput(videoDeviceId);
|
setMatrixCallVideoInput(videoDeviceId);
|
||||||
},
|
},
|
||||||
|
|
||||||
setAudioOutput: function(deviceId) {
|
setAudioOutput: function(deviceId) {
|
||||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||||
setMatrixCallAudioOutput(deviceId);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setAudioInput: function(deviceId) {
|
setAudioInput: function(deviceId) {
|
||||||
|
|
|
@ -38,7 +38,7 @@ import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks";
|
||||||
import {inviteUsersToRoom} from "./RoomInvite";
|
import {inviteUsersToRoom} from "./RoomInvite";
|
||||||
import { WidgetType } from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
import { Jitsi } from "./widgets/Jitsi";
|
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 BugReportDialog from "./components/views/dialogs/BugReportDialog";
|
||||||
import { ensureDMExists } from "./createRoom";
|
import { ensureDMExists } from "./createRoom";
|
||||||
import { ViewUserPayload } from "./dispatcher/payloads/ViewUserPayload";
|
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.
|
// some superfast regex over the text so we don't have to.
|
||||||
const embed = parseHtml(widgetUrl);
|
const embed = parseHtml(widgetUrl);
|
||||||
if (embed && embed.childNodes && embed.childNodes.length === 1) {
|
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) {
|
if (iframe.tagName.toLowerCase() === 'iframe' && iframe.attrs) {
|
||||||
const srcAttr = iframe.attrs.find(a => a.name === 'src');
|
const srcAttr = iframe.attrs.find(a => a.name === 'src');
|
||||||
console.log("Pulling URL out of iframe (embed code)");
|
console.log("Pulling URL out of iframe (embed code)");
|
||||||
|
|
|
@ -59,6 +59,9 @@ import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBi
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import SpacePanel from "../views/spaces/SpacePanel";
|
import SpacePanel from "../views/spaces/SpacePanel";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
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)
|
// 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.
|
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||||
|
@ -119,6 +122,7 @@ interface IState {
|
||||||
usageLimitEventContent?: IUsageLimit;
|
usageLimitEventContent?: IUsageLimit;
|
||||||
usageLimitEventTs?: number;
|
usageLimitEventTs?: number;
|
||||||
useCompactLayout: boolean;
|
useCompactLayout: boolean;
|
||||||
|
activeCalls: Array<MatrixCall>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -160,6 +164,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
// use compact timeline view
|
// use compact timeline view
|
||||||
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
useCompactLayout: SettingsStore.getValue('useCompactLayout'),
|
||||||
usageLimitDismissed: false,
|
usageLimitDismissed: false,
|
||||||
|
activeCalls: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
|
@ -175,6 +180,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
document.addEventListener('keydown', this._onNativeKeyDown, false);
|
||||||
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||||
|
|
||||||
this._updateServerNoticeEvents();
|
this._updateServerNoticeEvents();
|
||||||
|
|
||||||
|
@ -199,6 +205,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
document.removeEventListener('keydown', this._onNativeKeyDown, false);
|
||||||
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
|
||||||
this._matrixClient.removeListener("accountData", this.onAccountData);
|
this._matrixClient.removeListener("accountData", this.onAccountData);
|
||||||
this._matrixClient.removeListener("sync", this.onSync);
|
this._matrixClient.removeListener("sync", this.onSync);
|
||||||
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
|
@ -206,6 +213,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
this.resizer.detach();
|
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
|
// 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.
|
// sort of assurance here by only allowing a re-render if the client is truthy.
|
||||||
//
|
//
|
||||||
|
@ -661,6 +674,12 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
bodyClasses += ' mx_MatrixChat_useCompactLayout';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const audioFeedArraysForCalls = this.state.activeCalls.map((call) => {
|
||||||
|
return (
|
||||||
|
<AudioFeedArrayForCall call={call} key={call.callId} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MatrixClientContext.Provider value={this._matrixClient}>
|
<MatrixClientContext.Provider value={this._matrixClient}>
|
||||||
<div
|
<div
|
||||||
|
@ -685,6 +704,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
<CallContainer />
|
<CallContainer />
|
||||||
<NonUrgentToastContainer />
|
<NonUrgentToastContainer />
|
||||||
<HostSignupContainer />
|
<HostSignupContainer />
|
||||||
|
{audioFeedArraysForCalls}
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1094,7 +1094,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
|
|
||||||
private leaveRoomWarnings(roomId: string) {
|
private leaveRoomWarnings(roomId: string) {
|
||||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
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.
|
// Show a warning if there are additional complications.
|
||||||
const warnings = [];
|
const warnings = [];
|
||||||
|
|
||||||
|
@ -1133,7 +1133,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
const roomToLeave = MatrixClientPeg.get().getRoom(roomId);
|
||||||
const warnings = this.leaveRoomWarnings(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, {
|
Modal.createTrackedDialog(isSpace ? "Leave space" : "Leave room", '', QuestionDialog, {
|
||||||
title: isSpace ? _t("Leave space") : _t("Leave room"),
|
title: isSpace ? _t("Leave space") : _t("Leave room"),
|
||||||
description: (
|
description: (
|
||||||
|
|
|
@ -544,11 +544,13 @@ export default class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
if (!grouper) {
|
if (!grouper) {
|
||||||
const wantTile = this._shouldShowEvent(mxEv);
|
const wantTile = this._shouldShowEvent(mxEv);
|
||||||
|
const isGrouped = false;
|
||||||
if (wantTile) {
|
if (wantTile) {
|
||||||
// make sure we unpack the array returned by _getTilesForEvent,
|
// make sure we unpack the array returned by _getTilesForEvent,
|
||||||
// otherwise react will auto-generate keys and we will end up
|
// otherwise react will auto-generate keys and we will end up
|
||||||
// replacing all of the DOM elements every time we paginate.
|
// 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;
|
prevEvent = mxEv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -564,7 +566,7 @@ export default class MessagePanel extends React.Component {
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
_getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
|
_getTilesForEvent(prevEvent, mxEv, last, isGrouped=false, nextEvent, nextEventWithTile) {
|
||||||
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
|
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
|
||||||
const EventTile = sdk.getComponent('rooms.EventTile');
|
const EventTile = sdk.getComponent('rooms.EventTile');
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
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?
|
// do we need a date separator since the last event?
|
||||||
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
|
const wantsDateSeparator = this._wantsDateSeparator(prevEvent, eventDate);
|
||||||
if (wantsDateSeparator) {
|
if (wantsDateSeparator && !isGrouped) {
|
||||||
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
const dateSeparator = <li key={ts1}><DateSeparator key={ts1} ts={ts1} /></li>;
|
||||||
ret.push(dateSeparator);
|
ret.push(dateSeparator);
|
||||||
}
|
}
|
||||||
|
@ -968,9 +970,9 @@ class CreationGrouper {
|
||||||
|
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||||
|
|
||||||
const panel = this.panel;
|
const panel = this.panel;
|
||||||
const ret = [];
|
const ret = [];
|
||||||
|
const isGrouped = true;
|
||||||
const createEvent = this.createEvent;
|
const createEvent = this.createEvent;
|
||||||
const lastShownEvent = this.lastShownEvent;
|
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 this m.room.create event should be shown (room upgrade) then show it before the summary
|
||||||
if (panel._shouldShowEvent(createEvent)) {
|
if (panel._shouldShowEvent(createEvent)) {
|
||||||
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
|
// 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) {
|
for (const ejected of this.ejectedEvents) {
|
||||||
ret.push(...panel._getTilesForEvent(
|
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
|
// of EventListSummary, render each member event as if the previous
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
// timestamp of the current event, and no DateSeparator is inserted.
|
// 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), []);
|
}).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
|
// 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];
|
const ev = this.events[this.events.length - 1];
|
||||||
|
@ -1083,7 +1085,7 @@ class RedactionGrouper {
|
||||||
|
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||||
|
const isGrouped = true;
|
||||||
const panel = this.panel;
|
const panel = this.panel;
|
||||||
const ret = [];
|
const ret = [];
|
||||||
const lastShownEvent = this.lastShownEvent;
|
const lastShownEvent = this.lastShownEvent;
|
||||||
|
@ -1103,7 +1105,8 @@ class RedactionGrouper {
|
||||||
let eventTiles = this.events.map((e, i) => {
|
let eventTiles = this.events.map((e, i) => {
|
||||||
senders.add(e.sender);
|
senders.add(e.sender);
|
||||||
const prevEvent = i === 0 ? this.prevEvent : this.events[i - 1];
|
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), []);
|
}).reduce((a, b) => a.concat(b), []);
|
||||||
|
|
||||||
if (eventTiles.length === 0) {
|
if (eventTiles.length === 0) {
|
||||||
|
@ -1182,7 +1185,7 @@ class MemberGrouper {
|
||||||
|
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||||
|
const isGrouped = true;
|
||||||
const panel = this.panel;
|
const panel = this.panel;
|
||||||
const lastShownEvent = this.lastShownEvent;
|
const lastShownEvent = this.lastShownEvent;
|
||||||
const ret = [];
|
const ret = [];
|
||||||
|
@ -1215,7 +1218,7 @@ class MemberGrouper {
|
||||||
// of MemberEventListSummary, render each member event as if the previous
|
// of MemberEventListSummary, render each member event as if the previous
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
// timestamp of the current event, and no DateSeparator is inserted.
|
// 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), []);
|
}).reduce((a, b) => a.concat(b), []);
|
||||||
|
|
||||||
if (eventTiles.length === 0) {
|
if (eventTiles.length === 0) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import {Action} from "../../dispatcher/actions";
|
||||||
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
|
||||||
import WidgetCard from "../views/right_panel/WidgetCard";
|
import WidgetCard from "../views/right_panel/WidgetCard";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../utils/replaceableComponent";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
@replaceableComponent("structures.RightPanel")
|
@replaceableComponent("structures.RightPanel")
|
||||||
export default class RightPanel extends React.Component {
|
export default class RightPanel extends React.Component {
|
||||||
|
@ -85,7 +86,9 @@ export default class RightPanel extends React.Component {
|
||||||
return RightPanelPhases.GroupMemberList;
|
return RightPanelPhases.GroupMemberList;
|
||||||
}
|
}
|
||||||
return rps.groupPanelPhase;
|
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;
|
return RightPanelPhases.SpaceMemberList;
|
||||||
} else if (userForPanel) {
|
} else if (userForPanel) {
|
||||||
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
|
// XXX FIXME AAAAAARGH: What is going on with this class!? It takes some of its state
|
||||||
|
|
|
@ -27,8 +27,8 @@ import { Action } from "../../dispatcher/actions";
|
||||||
import RoomListStore from "../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../stores/room-list/RoomListStore";
|
||||||
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
|
||||||
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
|
||||||
import {replaceableComponent} from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import SpaceStore, {UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES} from "../../stores/SpaceStore";
|
import SpaceStore, { UPDATE_SELECTED_SPACE, UPDATE_TOP_LEVEL_SPACES } from "../../stores/SpaceStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
|
|
@ -1750,7 +1750,10 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const myMembership = this.state.room.getMyMembership();
|
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) {
|
if (this.state.joining || this.state.rejecting) {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
|
@ -1892,7 +1895,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
room={this.state.room}
|
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 (
|
return (
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
{ previewBar }
|
{ previewBar }
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useMemo, useState} from "react";
|
import React, {ReactNode, useMemo, useState} from "react";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||||
|
@ -24,7 +24,7 @@ import {sortBy} from "lodash";
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import {_t} from "../../languageHandler";
|
import {_t} from "../../languageHandler";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
||||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import SearchBox from "./SearchBox";
|
import SearchBox from "./SearchBox";
|
||||||
|
@ -39,11 +39,14 @@ import {mediaFromMxc} from "../../customisations/Media";
|
||||||
import InfoTooltip from "../views/elements/InfoTooltip";
|
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
import {useStateToggle} from "../../hooks/useStateToggle";
|
||||||
|
import {getOrder} from "../../stores/SpaceStore";
|
||||||
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
interface IHierarchyProps {
|
interface IHierarchyProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
initialText?: string;
|
initialText?: string;
|
||||||
refreshToken?: any;
|
refreshToken?: any;
|
||||||
|
additionalButtons?: ReactNode;
|
||||||
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,8 +109,16 @@ const Tile: React.FC<ITileProps> = ({
|
||||||
const cliRoom = cli.getRoom(room.room_id);
|
const cliRoom = cli.getRoom(room.room_id);
|
||||||
const myMembership = cliRoom?.getMyMembership();
|
const myMembership = cliRoom?.getMyMembership();
|
||||||
|
|
||||||
const onPreviewClick = () => onViewRoomClick(false);
|
const onPreviewClick = (ev: ButtonEvent) => {
|
||||||
const onJoinClick = () => onViewRoomClick(true);
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
onViewRoomClick(false);
|
||||||
|
}
|
||||||
|
const onJoinClick = (ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
onViewRoomClick(true);
|
||||||
|
}
|
||||||
|
|
||||||
let button;
|
let button;
|
||||||
if (myMembership === "join") {
|
if (myMembership === "join") {
|
||||||
|
@ -254,7 +265,11 @@ export const HierarchyLevel = ({
|
||||||
const space = cli.getRoom(spaceId);
|
const space = cli.getRoom(spaceId);
|
||||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||||
|
|
||||||
const sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null);
|
const children = Array.from(relations.get(spaceId)?.values() || []);
|
||||||
|
const sortedChildren = sortBy(children, ev => {
|
||||||
|
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
||||||
|
return getOrder(ev.content.order, null, ev.state_key);
|
||||||
|
});
|
||||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
||||||
const roomId = ev.state_key;
|
const roomId = ev.state_key;
|
||||||
if (!rooms.has(roomId)) return result;
|
if (!rooms.has(roomId)) return result;
|
||||||
|
@ -350,6 +365,7 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
initialText = "",
|
initialText = "",
|
||||||
showRoom,
|
showRoom,
|
||||||
refreshToken,
|
refreshToken,
|
||||||
|
additionalButtons,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
@ -415,22 +431,31 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
countsStr = _t("%(count)s rooms", { count: numRooms, numSpaces });
|
||||||
}
|
}
|
||||||
|
|
||||||
let editSection;
|
let manageButtons;
|
||||||
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
if (space.getMyMembership() === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
|
||||||
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
const selectedRelations = Array.from(selected.keys()).flatMap(parentId => {
|
||||||
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
return [...selected.get(parentId).values()].map(childId => [parentId, childId]) as [string, string][];
|
||||||
});
|
});
|
||||||
|
|
||||||
let buttons;
|
|
||||||
if (selectedRelations.length) {
|
|
||||||
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
const selectionAllSuggested = selectedRelations.every(([parentId, childId]) => {
|
||||||
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
return parentChildMap.get(parentId)?.get(childId)?.content.suggested;
|
||||||
});
|
});
|
||||||
|
|
||||||
const disabled = removing || saving;
|
const disabled = !selectedRelations.length || removing || saving;
|
||||||
|
|
||||||
buttons = <>
|
let Button: React.ComponentType<React.ComponentProps<typeof AccessibleButton>> = AccessibleButton;
|
||||||
<AccessibleButton
|
let props = {};
|
||||||
|
if (!selectedRelations.length) {
|
||||||
|
Button = AccessibleTooltipButton;
|
||||||
|
props = {
|
||||||
|
tooltip: _t("Select a room below first"),
|
||||||
|
yOffset: -40,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
manageButtons = <>
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setRemoving(true);
|
setRemoving(true);
|
||||||
try {
|
try {
|
||||||
|
@ -448,8 +473,9 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
{ removing ? _t("Removing...") : _t("Remove") }
|
{ removing ? _t("Removing...") : _t("Remove") }
|
||||||
</AccessibleButton>
|
</Button>
|
||||||
<AccessibleButton
|
<Button
|
||||||
|
{...props}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
|
@ -480,15 +506,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
? _t("Saving...")
|
? _t("Saving...")
|
||||||
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
: (selectionAllSuggested ? _t("Mark as not suggested") : _t("Mark as suggested"))
|
||||||
}
|
}
|
||||||
</AccessibleButton>
|
</Button>
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
editSection = <span>
|
|
||||||
{ buttons }
|
|
||||||
</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let results;
|
let results;
|
||||||
if (roomsMap.size) {
|
if (roomsMap.size) {
|
||||||
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
|
||||||
|
@ -532,7 +553,10 @@ export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
|
||||||
content = <>
|
content = <>
|
||||||
<div className="mx_SpaceRoomDirectory_listHeader">
|
<div className="mx_SpaceRoomDirectory_listHeader">
|
||||||
{ countsStr }
|
{ countsStr }
|
||||||
{ editSection }
|
<span>
|
||||||
|
{ additionalButtons }
|
||||||
|
{ manageButtons }
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{ error && <div className="mx_SpaceRoomDirectory_error">
|
{ error && <div className="mx_SpaceRoomDirectory_error">
|
||||||
{ error }
|
{ error }
|
||||||
|
|
|
@ -52,8 +52,14 @@ import {useStateToggle} from "../../hooks/useStateToggle";
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
import FacePile from "../views/elements/FacePile";
|
import FacePile from "../views/elements/FacePile";
|
||||||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
||||||
import {allSettled} from "../../utils/promise";
|
import {sleep} from "../../utils/promise";
|
||||||
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
|
import {calculateRoomVia} from "../../utils/permalinks/Permalinks";
|
||||||
|
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
||||||
|
import IconizedContextMenu, {
|
||||||
|
IconizedContextMenuOption,
|
||||||
|
IconizedContextMenuOptionList,
|
||||||
|
} from "../views/context_menus/IconizedContextMenu";
|
||||||
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -217,6 +223,67 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SpaceLandingAddButton = ({ space, onNewRoomAdded }) => {
|
||||||
|
const cli = useContext(MatrixClientContext);
|
||||||
|
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
|
let contextMenu;
|
||||||
|
if (menuDisplayed) {
|
||||||
|
const rect = handle.current.getBoundingClientRect();
|
||||||
|
contextMenu = <IconizedContextMenu
|
||||||
|
left={rect.left + window.pageXOffset + 0}
|
||||||
|
top={rect.bottom + window.pageYOffset + 8}
|
||||||
|
chevronFace={ChevronFace.None}
|
||||||
|
onFinished={closeMenu}
|
||||||
|
className="mx_RoomTile_contextMenu"
|
||||||
|
compact
|
||||||
|
>
|
||||||
|
<IconizedContextMenuOptionList first>
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
label={_t("Create new room")}
|
||||||
|
iconClassName="mx_RoomList_iconPlus"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
closeMenu();
|
||||||
|
|
||||||
|
if (await showCreateNewRoom(cli, space)) {
|
||||||
|
onNewRoomAdded();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
label={_t("Add existing room")}
|
||||||
|
iconClassName="mx_RoomList_iconHash"
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
closeMenu();
|
||||||
|
|
||||||
|
const [added] = await showAddExistingRooms(cli, space);
|
||||||
|
if (added) {
|
||||||
|
onNewRoomAdded();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</IconizedContextMenu>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<ContextMenuButton
|
||||||
|
kind="primary"
|
||||||
|
inputRef={handle}
|
||||||
|
onClick={openMenu}
|
||||||
|
isExpanded={menuDisplayed}
|
||||||
|
label={_t("Add")}
|
||||||
|
>
|
||||||
|
{ _t("Add") }
|
||||||
|
</ContextMenuButton>
|
||||||
|
{ contextMenu }
|
||||||
|
</>;
|
||||||
|
};
|
||||||
|
|
||||||
const SpaceLanding = ({ space }) => {
|
const SpaceLanding = ({ space }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const myMembership = useMyRoomMembership(space);
|
const myMembership = useMyRoomMembership(space);
|
||||||
|
@ -241,32 +308,20 @@ const SpaceLanding = ({ space }) => {
|
||||||
|
|
||||||
const [refreshToken, forceUpdate] = useStateToggle(false);
|
const [refreshToken, forceUpdate] = useStateToggle(false);
|
||||||
|
|
||||||
let addRoomButtons;
|
let addRoomButton;
|
||||||
if (canAddRooms) {
|
if (canAddRooms) {
|
||||||
addRoomButtons = <React.Fragment>
|
addRoomButton = <SpaceLandingAddButton space={space} onNewRoomAdded={forceUpdate} />;
|
||||||
<AccessibleButton className="mx_SpaceRoomView_landing_addButton" onClick={async () => {
|
|
||||||
const [added] = await showAddExistingRooms(cli, space);
|
|
||||||
if (added) {
|
|
||||||
forceUpdate();
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{ _t("Add existing rooms & spaces") }
|
|
||||||
</AccessibleButton>
|
|
||||||
<AccessibleButton className="mx_SpaceRoomView_landing_createButton" onClick={() => {
|
|
||||||
showCreateNewRoom(cli, space);
|
|
||||||
}}>
|
|
||||||
{ _t("Create a new room") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</React.Fragment>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let settingsButton;
|
let settingsButton;
|
||||||
if (shouldShowSpaceSettings(cli, space)) {
|
if (shouldShowSpaceSettings(cli, space)) {
|
||||||
settingsButton = <AccessibleButton className="mx_SpaceRoomView_landing_settingsButton" onClick={() => {
|
settingsButton = <AccessibleTooltipButton
|
||||||
|
className="mx_SpaceRoomView_landing_settingsButton"
|
||||||
|
onClick={() => {
|
||||||
showSpaceSettings(cli, space);
|
showSpaceSettings(cli, space);
|
||||||
}}>
|
}}
|
||||||
{ _t("Settings") }
|
title={_t("Settings")}
|
||||||
</AccessibleButton>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onMembersClick = () => {
|
const onMembersClick = () => {
|
||||||
|
@ -293,17 +348,19 @@ const SpaceLanding = ({ space }) => {
|
||||||
<SpaceInfo space={space} />
|
<SpaceInfo space={space} />
|
||||||
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
|
||||||
{ inviteButton }
|
{ inviteButton }
|
||||||
|
{ settingsButton }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_SpaceRoomView_landing_topic">
|
<div className="mx_SpaceRoomView_landing_topic">
|
||||||
<RoomTopic room={space} />
|
<RoomTopic room={space} />
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="mx_SpaceRoomView_landing_adminButtons">
|
|
||||||
{ addRoomButtons }
|
|
||||||
{ settingsButton }
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} />
|
<SpaceHierarchy
|
||||||
|
space={space}
|
||||||
|
showRoom={showRoom}
|
||||||
|
refreshToken={refreshToken}
|
||||||
|
additionalButtons={addRoomButton}
|
||||||
|
/>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -389,15 +446,24 @@ const SpaceAddExistingRooms = ({ space, onFinished }) => {
|
||||||
let buttonLabel = _t("Skip for now");
|
let buttonLabel = _t("Skip for now");
|
||||||
if (selectedToAdd.size > 0) {
|
if (selectedToAdd.size > 0) {
|
||||||
onClick = async () => {
|
onClick = async () => {
|
||||||
// TODO rate limiting
|
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
|
|
||||||
|
for (const room of selectedToAdd) {
|
||||||
|
const via = calculateRoomVia(room);
|
||||||
try {
|
try {
|
||||||
await allSettled(Array.from(selectedToAdd).map((room) =>
|
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
|
||||||
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
|
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||||
onFinished(true);
|
await sleep(e.data.retry_after_ms);
|
||||||
|
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to add rooms to space", e);
|
console.error("Failed to add rooms to space", e);
|
||||||
setError(_t("Failed to add rooms to space"));
|
setError(_t("Failed to add rooms to space"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
};
|
};
|
||||||
|
|
|
@ -78,8 +78,10 @@ export default class MessageContextMenu extends React.Component {
|
||||||
|
|
||||||
// We explicitly decline to show the redact option on ACL events as it has a potential
|
// We explicitly decline to show the redact option on ACL events as it has a potential
|
||||||
// to obliterate the room - https://github.com/matrix-org/synapse/issues/4042
|
// to obliterate the room - https://github.com/matrix-org/synapse/issues/4042
|
||||||
|
// Similarly for encryption events, since redacting them "breaks everything"
|
||||||
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
const canRedact = room.currentState.maySendRedactionForEvent(this.props.mxEvent, cli.credentials.userId)
|
||||||
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl;
|
&& this.props.mxEvent.getType() !== EventType.RoomServerAcl
|
||||||
|
&& this.props.mxEvent.getType() !== EventType.RoomEncryption;
|
||||||
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
let canPin = room.currentState.mayClientSendStateEvent('m.room.pinned_events', cli);
|
||||||
|
|
||||||
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
// HACK: Intentionally say we can't pin if the user doesn't want to use the functionality
|
||||||
|
|
|
@ -29,12 +29,13 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import {getDisplayAliasForRoom} from "../../../Rooms";
|
import {getDisplayAliasForRoom} from "../../../Rooms";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import {allSettled} from "../../../utils/promise";
|
import {sleep} from "../../../utils/promise";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
|
import {calculateRoomVia} from "../../../utils/permalinks/Permalinks";
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
|
||||||
|
import ProgressBar from "../elements/ProgressBar";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -46,7 +47,11 @@ const Entry = ({ room, checked, onChange }) => {
|
||||||
return <label className="mx_AddExistingToSpace_entry">
|
return <label className="mx_AddExistingToSpace_entry">
|
||||||
<RoomAvatar room={room} height={32} width={32} />
|
<RoomAvatar room={room} height={32} width={32} />
|
||||||
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
|
<span className="mx_AddExistingToSpace_entry_name">{ room.name }</span>
|
||||||
<StyledCheckbox onChange={(e) => onChange(e.target.checked)} checked={checked} />
|
<StyledCheckbox
|
||||||
|
onChange={onChange ? (e) => onChange(e.target.checked) : null}
|
||||||
|
checked={checked}
|
||||||
|
disabled={!onChange}
|
||||||
|
/>
|
||||||
</label>;
|
</label>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -104,9 +109,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
||||||
key={room.roomId}
|
key={room.roomId}
|
||||||
room={room}
|
room={room}
|
||||||
checked={selected.has(room)}
|
checked={selected.has(room)}
|
||||||
onChange={(checked) => {
|
onChange={onChange ? (checked) => {
|
||||||
onChange(checked, room);
|
onChange(checked, room);
|
||||||
}}
|
} : null}
|
||||||
/>;
|
/>;
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,9 +125,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
||||||
key={space.roomId}
|
key={space.roomId}
|
||||||
room={space}
|
room={space}
|
||||||
checked={selected.has(space)}
|
checked={selected.has(space)}
|
||||||
onChange={(checked) => {
|
onChange={onChange ? (checked) => {
|
||||||
onChange(checked, space);
|
onChange(checked, space);
|
||||||
}}
|
} : null}
|
||||||
/>;
|
/>;
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
|
@ -136,9 +141,9 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({ space,
|
||||||
key={room.roomId}
|
key={room.roomId}
|
||||||
room={room}
|
room={room}
|
||||||
checked={selected.has(room)}
|
checked={selected.has(room)}
|
||||||
onChange={(checked) => {
|
onChange={onChange ? (checked) => {
|
||||||
onChange(checked, room);
|
onChange(checked, room);
|
||||||
}}
|
} : null}
|
||||||
/>;
|
/>;
|
||||||
}) }
|
}) }
|
||||||
</div>
|
</div>
|
||||||
|
@ -156,8 +161,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
const existingSubspaces = SpaceStore.instance.getChildSpaces(space.roomId);
|
||||||
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
const [selectedToAdd, setSelectedToAdd] = useState(new Set<Room>());
|
||||||
|
|
||||||
const [busy, setBusy] = useState(false);
|
const [progress, setProgress] = useState<number>(null);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState<Error>(null);
|
||||||
|
|
||||||
let spaceOptionSection;
|
let spaceOptionSection;
|
||||||
if (existingSubspaces.length > 0) {
|
if (existingSubspaces.length > 0) {
|
||||||
|
@ -197,6 +202,82 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
</div>
|
</div>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
|
||||||
|
const addRooms = async () => {
|
||||||
|
setError(null);
|
||||||
|
setProgress(0);
|
||||||
|
|
||||||
|
let error;
|
||||||
|
|
||||||
|
for (const room of selectedToAdd) {
|
||||||
|
const via = calculateRoomVia(room);
|
||||||
|
try {
|
||||||
|
await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async e => {
|
||||||
|
if (e.errcode === "M_LIMIT_EXCEEDED") {
|
||||||
|
await sleep(e.data.retry_after_ms);
|
||||||
|
return SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
setProgress(i => i + 1);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to add rooms to space", e);
|
||||||
|
setError(error = e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!error) {
|
||||||
|
onFinished(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const busy = progress !== null;
|
||||||
|
|
||||||
|
let footer;
|
||||||
|
if (error) {
|
||||||
|
footer = <>
|
||||||
|
<img
|
||||||
|
src={require("../../../../res/img/element-icons/warning-badge.svg")}
|
||||||
|
height="24"
|
||||||
|
width="24"
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
|
||||||
|
<span className="mx_AddExistingToSpaceDialog_error">
|
||||||
|
<div className="mx_AddExistingToSpaceDialog_errorHeading">{ _t("Not all selected were added") }</div>
|
||||||
|
<div className="mx_AddExistingToSpaceDialog_errorCaption">{ _t("Try again") }</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AccessibleButton className="mx_AddExistingToSpaceDialog_retryButton" onClick={addRooms}>
|
||||||
|
{ _t("Retry") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
} else if (busy) {
|
||||||
|
footer = <span>
|
||||||
|
<ProgressBar value={progress} max={selectedToAdd.size} />
|
||||||
|
<div className="mx_AddExistingToSpaceDialog_progressText">
|
||||||
|
{ _t("Adding rooms... (%(progress)s out of %(count)s)", {
|
||||||
|
count: selectedToAdd.size,
|
||||||
|
progress,
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</span>;
|
||||||
|
} else {
|
||||||
|
footer = <>
|
||||||
|
<span>
|
||||||
|
<div>{ _t("Want to add a new room instead?") }</div>
|
||||||
|
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
||||||
|
{ _t("Create a new room") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<AccessibleButton kind="primary" disabled={selectedToAdd.size < 1} onClick={addRooms}>
|
||||||
|
{ _t("Add") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
return <BaseDialog
|
return <BaseDialog
|
||||||
title={title}
|
title={title}
|
||||||
className="mx_AddExistingToSpaceDialog"
|
className="mx_AddExistingToSpaceDialog"
|
||||||
|
@ -204,50 +285,23 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
|
||||||
onFinished={onFinished}
|
onFinished={onFinished}
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
>
|
>
|
||||||
{ error && <div className="mx_AddExistingToSpaceDialog_errorText">{ error }</div> }
|
|
||||||
|
|
||||||
<MatrixClientContext.Provider value={cli}>
|
<MatrixClientContext.Provider value={cli}>
|
||||||
<AddExistingToSpace
|
<AddExistingToSpace
|
||||||
space={space}
|
space={space}
|
||||||
selected={selectedToAdd}
|
selected={selectedToAdd}
|
||||||
onChange={(checked, room) => {
|
onChange={!busy && !error ? (checked, room) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
selectedToAdd.add(room);
|
selectedToAdd.add(room);
|
||||||
} else {
|
} else {
|
||||||
selectedToAdd.delete(room);
|
selectedToAdd.delete(room);
|
||||||
}
|
}
|
||||||
setSelectedToAdd(new Set(selectedToAdd));
|
setSelectedToAdd(new Set(selectedToAdd));
|
||||||
}}
|
} : null}
|
||||||
/>
|
/>
|
||||||
</MatrixClientContext.Provider>
|
</MatrixClientContext.Provider>
|
||||||
|
|
||||||
<div className="mx_AddExistingToSpaceDialog_footer">
|
<div className="mx_AddExistingToSpaceDialog_footer">
|
||||||
<span>
|
{ footer }
|
||||||
<div>{ _t("Don't want to add an existing room?") }</div>
|
|
||||||
<AccessibleButton onClick={() => onCreateRoomClick(cli, space)} kind="link">
|
|
||||||
{ _t("Create a new room") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<AccessibleButton
|
|
||||||
kind="primary"
|
|
||||||
disabled={busy || selectedToAdd.size < 1}
|
|
||||||
onClick={async () => {
|
|
||||||
// TODO rate limiting
|
|
||||||
setBusy(true);
|
|
||||||
try {
|
|
||||||
await allSettled(Array.from(selectedToAdd).map((room) =>
|
|
||||||
SpaceStore.instance.addRoomToSpace(space, room.roomId, calculateRoomVia(room))));
|
|
||||||
onFinished(true);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to add rooms to space", e);
|
|
||||||
setError(_t("Failed to add rooms to space"));
|
|
||||||
}
|
|
||||||
setBusy(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{ busy ? _t("Adding...") : _t("Add") }
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
</div>
|
||||||
</BaseDialog>;
|
</BaseDialog>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1312,7 +1312,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
|
||||||
goButtonFn = this._startDm;
|
goButtonFn = this._startDm;
|
||||||
} else if (this.props.kind === KIND_INVITE) {
|
} else if (this.props.kind === KIND_INVITE) {
|
||||||
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
|
||||||
const isSpace = room?.isSpaceRoom();
|
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();
|
||||||
title = isSpace
|
title = isSpace
|
||||||
? _t("Invite to %(spaceName)s", {
|
? _t("Invite to %(spaceName)s", {
|
||||||
spaceName: room.name || _t("Unnamed Space"),
|
spaceName: room.name || _t("Unnamed Space"),
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2019 New Vector Ltd
|
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@ -16,20 +16,23 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import filesize from "filesize";
|
import filesize from "filesize";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
import { getBlobSafeMimeType } from '../../../utils/blobs';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
file: File;
|
||||||
|
currentIndex: number;
|
||||||
|
totalFiles?: number;
|
||||||
|
onFinished: (uploadConfirmed: boolean, uploadAll?: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.UploadConfirmDialog")
|
@replaceableComponent("views.dialogs.UploadConfirmDialog")
|
||||||
export default class UploadConfirmDialog extends React.Component {
|
export default class UploadConfirmDialog extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private objectUrl: string;
|
||||||
file: PropTypes.object.isRequired,
|
private mimeType: string;
|
||||||
currentIndex: PropTypes.number,
|
|
||||||
totalFiles: PropTypes.number,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
totalFiles: 1,
|
totalFiles: 1,
|
||||||
|
@ -38,22 +41,28 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._objectUrl = URL.createObjectURL(props.file);
|
// Create a fresh `Blob` for previewing (even though `File` already is
|
||||||
|
// one) so we can adjust the MIME type if needed.
|
||||||
|
this.mimeType = getBlobSafeMimeType(props.file.type);
|
||||||
|
const blob = new Blob([props.file], { type:
|
||||||
|
this.mimeType,
|
||||||
|
});
|
||||||
|
this.objectUrl = URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
if (this._objectUrl) URL.revokeObjectURL(this._objectUrl);
|
if (this.objectUrl) URL.revokeObjectURL(this.objectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancelClick = () => {
|
private onCancelClick = () => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUploadClick = () => {
|
private onUploadClick = () => {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUploadAllClick = () => {
|
private onUploadAllClick = () => {
|
||||||
this.props.onFinished(true, true);
|
this.props.onFinished(true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,10 +84,10 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
let preview;
|
let preview;
|
||||||
if (this.props.file.type.startsWith('image/')) {
|
if (this.mimeType.startsWith('image/')) {
|
||||||
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
preview = <div className="mx_UploadConfirmDialog_previewOuter">
|
||||||
<div className="mx_UploadConfirmDialog_previewInner">
|
<div className="mx_UploadConfirmDialog_previewInner">
|
||||||
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this._objectUrl} /></div>
|
<div><img className="mx_UploadConfirmDialog_imagePreview" src={this.objectUrl} /></div>
|
||||||
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
|
<div>{this.props.file.name} ({filesize(this.props.file.size)})</div>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
|
@ -95,7 +104,7 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
|
|
||||||
let uploadAllButton;
|
let uploadAllButton;
|
||||||
if (this.props.currentIndex + 1 < this.props.totalFiles) {
|
if (this.props.currentIndex + 1 < this.props.totalFiles) {
|
||||||
uploadAllButton = <button onClick={this._onUploadAllClick}>
|
uploadAllButton = <button onClick={this.onUploadAllClick}>
|
||||||
{_t("Upload all")}
|
{_t("Upload all")}
|
||||||
</button>;
|
</button>;
|
||||||
}
|
}
|
||||||
|
@ -103,7 +112,7 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_UploadConfirmDialog'
|
<BaseDialog className='mx_UploadConfirmDialog'
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
onFinished={this._onCancelClick}
|
onFinished={this.onCancelClick}
|
||||||
title={title}
|
title={title}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
|
@ -113,7 +122,7 @@ export default class UploadConfirmDialog extends React.Component {
|
||||||
|
|
||||||
<DialogButtons primaryButton={_t('Upload')}
|
<DialogButtons primaryButton={_t('Upload')}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
onPrimaryButtonClick={this._onUploadClick}
|
onPrimaryButtonClick={this.onUploadClick}
|
||||||
focus={true}
|
focus={true}
|
||||||
>
|
>
|
||||||
{uploadAllButton}
|
{uploadAllButton}
|
|
@ -108,23 +108,20 @@ export default class ImageView extends React.Component<IProps, IState> {
|
||||||
window.addEventListener("resize", this.calculateZoom);
|
window.addEventListener("resize", this.calculateZoom);
|
||||||
// After the image loads for the first time we want to calculate the zoom
|
// After the image loads for the first time we want to calculate the zoom
|
||||||
this.image.current.addEventListener("load", this.calculateZoom);
|
this.image.current.addEventListener("load", this.calculateZoom);
|
||||||
// Try to precalculate the zoom from width and height props
|
|
||||||
this.calculateZoom();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
this.focusLock.current.removeEventListener('wheel', this.onWheel);
|
||||||
|
window.removeEventListener("resize", this.calculateZoom);
|
||||||
|
this.image.current.removeEventListener("load", this.calculateZoom);
|
||||||
}
|
}
|
||||||
|
|
||||||
private calculateZoom = () => {
|
private calculateZoom = () => {
|
||||||
const image = this.image.current;
|
const image = this.image.current;
|
||||||
const imageWrapper = this.imageWrapper.current;
|
const imageWrapper = this.imageWrapper.current;
|
||||||
|
|
||||||
const width = this.props.width || image.naturalWidth;
|
const zoomX = imageWrapper.clientWidth / image.naturalWidth;
|
||||||
const height = this.props.height || image.naturalHeight;
|
const zoomY = imageWrapper.clientHeight / image.naturalHeight;
|
||||||
|
|
||||||
const zoomX = imageWrapper.clientWidth / width;
|
|
||||||
const zoomY = imageWrapper.clientHeight / height;
|
|
||||||
|
|
||||||
// If the image is smaller in both dimensions set its the zoom to 1 to
|
// If the image is smaller in both dimensions set its the zoom to 1 to
|
||||||
// display it in its original size
|
// display it in its original size
|
||||||
|
|
|
@ -187,9 +187,15 @@ function DeviceItem({userId, device}: {userId: string, device: IDevice}) {
|
||||||
verifyDevice(cli.getUser(userId), device);
|
verifyDevice(cli.getUser(userId), device);
|
||||||
};
|
};
|
||||||
|
|
||||||
const deviceName = device.ambiguous ?
|
let deviceName;
|
||||||
(device.getDisplayName() ? device.getDisplayName() : "") + " (" + device.deviceId + ")" :
|
if (!device.getDisplayName()?.trim()) {
|
||||||
|
deviceName = device.deviceId;
|
||||||
|
} else {
|
||||||
|
deviceName = device.ambiguous ?
|
||||||
|
device.getDisplayName() + " (" + device.deviceId + ")" :
|
||||||
device.getDisplayName();
|
device.getDisplayName();
|
||||||
|
}
|
||||||
|
|
||||||
let trustedLabel = null;
|
let trustedLabel = null;
|
||||||
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
|
if (userTrust.isVerified()) trustedLabel = isVerified ? _t("Trusted") : _t("Not trusted");
|
||||||
|
|
||||||
|
@ -440,7 +446,7 @@ const UserOptionsSection: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const warnSelfDemote = async (isSpace) => {
|
const warnSelfDemote = async (isSpace: boolean) => {
|
||||||
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
const {finished} = Modal.createTrackedDialog('Demoting Self', '', QuestionDialog, {
|
||||||
title: _t("Demote yourself?"),
|
title: _t("Demote yourself?"),
|
||||||
description:
|
description:
|
||||||
|
@ -727,7 +733,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({member, room, powerLevels,
|
||||||
// if muting self, warn as it may be irreversible
|
// if muting self, warn as it may be irreversible
|
||||||
if (target === cli.getUserId()) {
|
if (target === cli.getUserId()) {
|
||||||
try {
|
try {
|
||||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to warn about self demotion: ", e);
|
console.error("Failed to warn about self demotion: ", e);
|
||||||
return;
|
return;
|
||||||
|
@ -816,7 +822,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
if (canAffectUser && me.powerLevel >= kickPowerLevel) {
|
||||||
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
kickButton = <RoomKickButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />;
|
||||||
}
|
}
|
||||||
if (me.powerLevel >= redactPowerLevel && !room.isSpaceRoom()) {
|
if (me.powerLevel >= redactPowerLevel && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
|
||||||
redactButton = (
|
redactButton = (
|
||||||
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
<RedactMessagesButton member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||||
);
|
);
|
||||||
|
@ -1095,7 +1101,7 @@ const PowerLevelEditor: React.FC<{
|
||||||
} else if (myUserId === target) {
|
} else if (myUserId === target) {
|
||||||
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
// If we are changing our own PL it can only ever be decreasing, which we cannot reverse.
|
||||||
try {
|
try {
|
||||||
if (!(await warnSelfDemote(room?.isSpaceRoom()))) return;
|
if (!(await warnSelfDemote(SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()))) return;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to warn about self demotion: ", e);
|
console.error("Failed to warn about self demotion: ", e);
|
||||||
}
|
}
|
||||||
|
@ -1325,10 +1331,10 @@ const BasicUserInfo: React.FC<{
|
||||||
if (!isRoomEncrypted) {
|
if (!isRoomEncrypted) {
|
||||||
if (!cryptoEnabled) {
|
if (!cryptoEnabled) {
|
||||||
text = _t("This client does not support end-to-end encryption.");
|
text = _t("This client does not support end-to-end encryption.");
|
||||||
} else if (room && !room.isSpaceRoom()) {
|
} else if (room && (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom())) {
|
||||||
text = _t("Messages in this room are not end-to-end encrypted.");
|
text = _t("Messages in this room are not end-to-end encrypted.");
|
||||||
}
|
}
|
||||||
} else if (!room.isSpaceRoom()) {
|
} else if (!SettingsStore.getValue("feature_spaces") || !room.isSpaceRoom()) {
|
||||||
text = _t("Messages in this room are end-to-end encrypted.");
|
text = _t("Messages in this room are end-to-end encrypted.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1405,7 +1411,7 @@ const BasicUserInfo: React.FC<{
|
||||||
canInvite={roomPermissions.canInvite}
|
canInvite={roomPermissions.canInvite}
|
||||||
isIgnored={isIgnored}
|
isIgnored={isIgnored}
|
||||||
member={member}
|
member={member}
|
||||||
isSpace={room?.isSpaceRoom()}
|
isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ adminToolsContainer }
|
{ adminToolsContainer }
|
||||||
|
@ -1567,7 +1573,7 @@ const UserInfo: React.FC<Props> = ({
|
||||||
previousPhase = RightPanelPhases.RoomMemberInfo;
|
previousPhase = RightPanelPhases.RoomMemberInfo;
|
||||||
refireParams = {member: member};
|
refireParams = {member: member};
|
||||||
} else if (room) {
|
} else if (room) {
|
||||||
previousPhase = previousPhase = room.isSpaceRoom()
|
previousPhase = previousPhase = SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()
|
||||||
? RightPanelPhases.SpaceMemberList
|
? RightPanelPhases.SpaceMemberList
|
||||||
: RightPanelPhases.RoomMemberList;
|
: RightPanelPhases.RoomMemberList;
|
||||||
}
|
}
|
||||||
|
@ -1616,7 +1622,7 @@ const UserInfo: React.FC<Props> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
let scopeHeader;
|
let scopeHeader;
|
||||||
if (room?.isSpaceRoom()) {
|
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
|
||||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||||
<RoomAvatar room={room} height={32} width={32} />
|
<RoomAvatar room={room} height={32} width={32} />
|
||||||
<RoomName room={room} />
|
<RoomName room={room} />
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {_t} from '../../../languageHandler';
|
import {_t, _td} from '../../../languageHandler';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import EditorModel from '../../../editor/model';
|
import EditorModel from '../../../editor/model';
|
||||||
|
@ -24,16 +24,18 @@ import {getCaretOffsetAndText} from '../../../editor/dom';
|
||||||
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
|
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
|
||||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
import {findEditableEvent} from '../../../utils/EventUtils';
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import {parseEvent} from '../../../editor/deserialize';
|
||||||
import {PartCreator} from '../../../editor/parts';
|
import {CommandPartCreator} from '../../../editor/parts';
|
||||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer from "./BasicMessageComposer";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
import {CommandCategories, getCommand} from '../../../SlashCommands';
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
function _isReply(mxEvent) {
|
function _isReply(mxEvent) {
|
||||||
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
const relatesTo = mxEvent.getContent()["m.relates_to"];
|
||||||
|
@ -178,6 +180,22 @@ export default class EditMessageComposer extends React.Component {
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusComposer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_isSlashCommand() {
|
||||||
|
const parts = this.model.parts;
|
||||||
|
const firstPart = parts[0];
|
||||||
|
if (firstPart) {
|
||||||
|
if (firstPart.type === "command" && firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firstPart.text.startsWith("/") && !firstPart.text.startsWith("//")
|
||||||
|
&& (firstPart.type === "plain" || firstPart.type === "pill-candidate")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
_isContentModified(newContent) {
|
_isContentModified(newContent) {
|
||||||
// if nothing has changed then bail
|
// if nothing has changed then bail
|
||||||
const oldContent = this.props.editState.getEvent().getContent();
|
const oldContent = this.props.editState.getEvent().getContent();
|
||||||
|
@ -190,20 +208,113 @@ export default class EditMessageComposer extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_sendEdit = () => {
|
_getSlashCommand() {
|
||||||
|
const commandText = this.model.parts.reduce((text, part) => {
|
||||||
|
// use mxid to textify user pills in a command
|
||||||
|
if (part.type === "user-pill") {
|
||||||
|
return text + part.resourceId;
|
||||||
|
}
|
||||||
|
return text + part.text;
|
||||||
|
}, "");
|
||||||
|
const {cmd, args} = getCommand(commandText);
|
||||||
|
return [cmd, args, commandText];
|
||||||
|
}
|
||||||
|
|
||||||
|
async _runSlashCommand(cmd, args, roomId) {
|
||||||
|
const result = cmd.run(roomId, args);
|
||||||
|
let messageContent;
|
||||||
|
let error = result.error;
|
||||||
|
if (result.promise) {
|
||||||
|
try {
|
||||||
|
if (cmd.category === CommandCategories.messages) {
|
||||||
|
messageContent = await result.promise;
|
||||||
|
} else {
|
||||||
|
await result.promise;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
error = err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
console.error("Command failure: %s", error);
|
||||||
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
// assume the error is a server error when the command is async
|
||||||
|
const isServerError = !!result.promise;
|
||||||
|
const title = isServerError ? _td("Server error") : _td("Command error");
|
||||||
|
|
||||||
|
let errText;
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
errText = error;
|
||||||
|
} else if (error.message) {
|
||||||
|
errText = error.message;
|
||||||
|
} else {
|
||||||
|
errText = _t("Server unavailable, overloaded, or something else went wrong.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.createTrackedDialog(title, '', ErrorDialog, {
|
||||||
|
title: _t(title),
|
||||||
|
description: errText,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("Command success.");
|
||||||
|
if (messageContent) return messageContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sendEdit = async () => {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
const editedEvent = this.props.editState.getEvent();
|
const editedEvent = this.props.editState.getEvent();
|
||||||
const editContent = createEditContent(this.model, editedEvent);
|
const editContent = createEditContent(this.model, editedEvent);
|
||||||
const newContent = editContent["m.new_content"];
|
const newContent = editContent["m.new_content"];
|
||||||
|
let shouldSend = true;
|
||||||
|
|
||||||
// If content is modified then send an updated event into the room
|
// If content is modified then send an updated event into the room
|
||||||
if (this._isContentModified(newContent)) {
|
if (this._isContentModified(newContent)) {
|
||||||
const roomId = editedEvent.getRoomId();
|
const roomId = editedEvent.getRoomId();
|
||||||
|
if (!containsEmote(this.model) && this._isSlashCommand()) {
|
||||||
|
const [cmd, args, commandText] = this._getSlashCommand();
|
||||||
|
if (cmd) {
|
||||||
|
if (cmd.category === CommandCategories.messages) {
|
||||||
|
editContent["m.new_content"] = await this._runSlashCommand(cmd, args, roomId);
|
||||||
|
} else {
|
||||||
|
this._runSlashCommand(cmd, args, roomId);
|
||||||
|
shouldSend = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// ask the user if their unknown command should be sent as a message
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
const {finished} = Modal.createTrackedDialog("Unknown command", "", QuestionDialog, {
|
||||||
|
title: _t("Unknown Command"),
|
||||||
|
description: <div>
|
||||||
|
<p>
|
||||||
|
{ _t("Unrecognised command: %(commandText)s", {commandText}) }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t("You can use <code>/help</code> to list available commands. " +
|
||||||
|
"Did you mean to send this as a message?", {}, {
|
||||||
|
code: t => <code>{ t }</code>,
|
||||||
|
}) }
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{ _t("Hint: Begin your message with <code>//</code> to start it with a slash.", {}, {
|
||||||
|
code: t => <code>{ t }</code>,
|
||||||
|
}) }
|
||||||
|
</p>
|
||||||
|
</div>,
|
||||||
|
button: _t('Send as message'),
|
||||||
|
});
|
||||||
|
const [sendAnyway] = await finished;
|
||||||
|
// if !sendAnyway bail to let the user edit the composer and try again
|
||||||
|
if (!sendAnyway) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldSend) {
|
||||||
this._cancelPreviousPendingEdit();
|
this._cancelPreviousPendingEdit();
|
||||||
const prom = this.context.sendMessage(roomId, editContent);
|
const prom = this.context.sendMessage(roomId, editContent);
|
||||||
dis.dispatch({action: "message_sent"});
|
dis.dispatch({action: "message_sent"});
|
||||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, true, false, editContent);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// close the event editing and focus composer
|
// close the event editing and focus composer
|
||||||
dis.dispatch({action: "edit_event", event: null});
|
dis.dispatch({action: "edit_event", event: null});
|
||||||
|
@ -240,7 +351,7 @@ export default class EditMessageComposer extends React.Component {
|
||||||
_createEditorModel() {
|
_createEditorModel() {
|
||||||
const {editState} = this.props;
|
const {editState} = this.props;
|
||||||
const room = this._getRoom();
|
const room = this._getRoom();
|
||||||
const partCreator = new PartCreator(room, this.context);
|
const partCreator = new CommandPartCreator(room, this.context);
|
||||||
let parts;
|
let parts;
|
||||||
if (editState.hasEditorState()) {
|
if (editState.hasEditorState()) {
|
||||||
// if restoring state from a previous editor,
|
// if restoring state from a previous editor,
|
||||||
|
|
|
@ -30,6 +30,7 @@ import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
const INITIAL_LOAD_NUM_MEMBERS = 30;
|
||||||
const INITIAL_LOAD_NUM_INVITED = 5;
|
const INITIAL_LOAD_NUM_INVITED = 5;
|
||||||
|
@ -460,7 +461,7 @@ export default class MemberList extends React.Component {
|
||||||
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
const chat = CommunityPrototypeStore.instance.getSelectedCommunityGeneralChat();
|
||||||
if (chat && chat.roomId === this.props.roomId) {
|
if (chat && chat.roomId === this.props.roomId) {
|
||||||
inviteButtonText = _t("Invite to this community");
|
inviteButtonText = _t("Invite to this community");
|
||||||
} else if (room.isSpaceRoom()) {
|
} else if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
|
||||||
inviteButtonText = _t("Invite to this space");
|
inviteButtonText = _t("Invite to this space");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -492,7 +493,7 @@ export default class MemberList extends React.Component {
|
||||||
let previousPhase = RightPanelPhases.RoomSummary;
|
let previousPhase = RightPanelPhases.RoomSummary;
|
||||||
// We have no previousPhase for when viewing a MemberList from a Space
|
// We have no previousPhase for when viewing a MemberList from a Space
|
||||||
let scopeHeader;
|
let scopeHeader;
|
||||||
if (room?.isSpaceRoom()) {
|
if (SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()) {
|
||||||
previousPhase = undefined;
|
previousPhase = undefined;
|
||||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||||
<RoomAvatar room={room} height={32} width={32} />
|
<RoomAvatar room={room} height={32} width={32} />
|
||||||
|
|
|
@ -26,6 +26,7 @@ import {isValid3pidInvite} from "../../../RoomInvite";
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
event: MatrixEvent;
|
event: MatrixEvent;
|
||||||
|
@ -135,7 +136,7 @@ export default class ThirdPartyMemberInfo extends React.Component<IProps, IState
|
||||||
}
|
}
|
||||||
|
|
||||||
let scopeHeader;
|
let scopeHeader;
|
||||||
if (this.room.isSpaceRoom()) {
|
if (SettingsStore.getValue("feature_spaces") && this.room.isSpaceRoom()) {
|
||||||
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
scopeHeader = <div className="mx_RightPanel_scopeHeader">
|
||||||
<RoomAvatar room={this.room} height={32} width={32} />
|
<RoomAvatar room={this.room} height={32} width={32} />
|
||||||
<RoomName room={this.room} />
|
<RoomName room={this.room} />
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import {_t, getCurrentLanguage} from "../../../../../languageHandler";
|
import {_t, getCurrentLanguage} from "../../../../../languageHandler";
|
||||||
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
|
import AccessibleTooltipButton from '../../../elements/AccessibleTooltipButton';
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import createRoom from "../../../../../createRoom";
|
import createRoom from "../../../../../createRoom";
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
|
@ -26,6 +27,9 @@ import PlatformPeg from "../../../../../PlatformPeg";
|
||||||
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
import * as KeyboardShortcuts from "../../../../../accessibility/KeyboardShortcuts";
|
||||||
import UpdateCheckButton from "../../UpdateCheckButton";
|
import UpdateCheckButton from "../../UpdateCheckButton";
|
||||||
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../../utils/replaceableComponent";
|
||||||
|
import { copyPlaintext } from "../../../../../utils/strings";
|
||||||
|
import * as ContextMenu from "../../../../structures/ContextMenu";
|
||||||
|
import { toRightOf } from "../../../../structures/ContextMenu";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
closeSettingsFn: () => {};
|
closeSettingsFn: () => {};
|
||||||
|
@ -38,6 +42,8 @@ interface IState {
|
||||||
|
|
||||||
@replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab")
|
@replaceableComponent("views.settings.tabs.user.HelpUserSettingsTab")
|
||||||
export default class HelpUserSettingsTab extends React.Component<IProps, IState> {
|
export default class HelpUserSettingsTab extends React.Component<IProps, IState> {
|
||||||
|
protected closeCopiedTooltip: () => void;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -56,6 +62,12 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
// if the Copied tooltip is open then get rid of it, there are ways to close the modal which wouldn't close
|
||||||
|
// the tooltip otherwise, such as pressing Escape
|
||||||
|
if (this.closeCopiedTooltip) this.closeCopiedTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
private onClearCacheAndReload = (e) => {
|
private onClearCacheAndReload = (e) => {
|
||||||
if (!PlatformPeg.get()) return;
|
if (!PlatformPeg.get()) return;
|
||||||
|
|
||||||
|
@ -153,6 +165,20 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onAccessTokenCopyClick = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const target = e.target; // copy target before we go async and React throws it away
|
||||||
|
|
||||||
|
const successful = await copyPlaintext(MatrixClientPeg.get().getAccessToken());
|
||||||
|
const buttonRect = target.getBoundingClientRect();
|
||||||
|
const GenericTextContextMenu = sdk.getComponent('context_menus.GenericTextContextMenu');
|
||||||
|
const {close} = ContextMenu.createMenu(GenericTextContextMenu, {
|
||||||
|
...toRightOf(buttonRect, 2),
|
||||||
|
message: successful ? _t('Copied!') : _t('Failed to copy'),
|
||||||
|
});
|
||||||
|
this.closeCopiedTooltip = target.onmouseleave = close;
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
|
|
||||||
|
@ -269,12 +295,20 @@ export default class HelpUserSettingsTab extends React.Component<IProps, IState>
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
|
{_t("Homeserver is")} <code>{MatrixClientPeg.get().getHomeserverUrl()}</code><br />
|
||||||
{_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
|
{_t("Identity Server is")} <code>{MatrixClientPeg.get().getIdentityServerUrl()}</code><br />
|
||||||
{_t("Access Token:") + ' '}
|
<br />
|
||||||
<AccessibleButton element="span" onClick={this.showSpoiler}
|
<details>
|
||||||
data-spoiler={MatrixClientPeg.get().getAccessToken()}
|
<summary>{_t("Access Token")}</summary><br />
|
||||||
>
|
<b>{_t("Your access token gives full access to your account."
|
||||||
<{ _t("click to reveal") }>
|
+ " Do not share it with anyone." )}</b>
|
||||||
</AccessibleButton>
|
<div className="mx_HelpUserSettingsTab_accessToken">
|
||||||
|
<code>{MatrixClientPeg.get().getAccessToken()}</code>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
title={_t("Copy")}
|
||||||
|
onClick={this.onAccessTokenCopyClick}
|
||||||
|
className="mx_HelpUserSettingsTab_accessToken_copy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</details><br />
|
||||||
<div className='mx_HelpUserSettingsTab_debugButton'>
|
<div className='mx_HelpUserSettingsTab_debugButton'>
|
||||||
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
|
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
|
||||||
{_t("Clear cache and reload")}
|
{_t("Clear cache and reload")}
|
||||||
|
|
|
@ -26,13 +26,11 @@ import {SpaceItem} from "./SpaceTreeLevel";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import SpaceStore, {
|
import SpaceStore, {
|
||||||
HOME_SPACE,
|
|
||||||
UPDATE_INVITED_SPACES,
|
UPDATE_INVITED_SPACES,
|
||||||
UPDATE_SELECTED_SPACE,
|
UPDATE_SELECTED_SPACE,
|
||||||
UPDATE_TOP_LEVEL_SPACES,
|
UPDATE_TOP_LEVEL_SPACES,
|
||||||
} from "../../../stores/SpaceStore";
|
} from "../../../stores/SpaceStore";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import {SpaceNotificationState} from "../../../stores/notifications/SpaceNotificationState";
|
|
||||||
import NotificationBadge from "../rooms/NotificationBadge";
|
import NotificationBadge from "../rooms/NotificationBadge";
|
||||||
import {
|
import {
|
||||||
RovingAccessibleButton,
|
RovingAccessibleButton,
|
||||||
|
@ -40,13 +38,15 @@ import {
|
||||||
RovingTabIndexProvider,
|
RovingTabIndexProvider,
|
||||||
} from "../../../accessibility/RovingTabIndex";
|
} from "../../../accessibility/RovingTabIndex";
|
||||||
import {Key} from "../../../Keyboard";
|
import {Key} from "../../../Keyboard";
|
||||||
|
import {RoomNotificationStateStore} from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
|
import {NotificationState} from "../../../stores/notifications/NotificationState";
|
||||||
|
|
||||||
interface IButtonProps {
|
interface IButtonProps {
|
||||||
space?: Room;
|
space?: Room;
|
||||||
className?: string;
|
className?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
tooltip?: string;
|
tooltip?: string;
|
||||||
notificationState?: SpaceNotificationState;
|
notificationState?: NotificationState;
|
||||||
isNarrow?: boolean;
|
isNarrow?: boolean;
|
||||||
onClick(): void;
|
onClick(): void;
|
||||||
}
|
}
|
||||||
|
@ -212,8 +212,8 @@ const SpacePanel = () => {
|
||||||
className="mx_SpaceButton_home"
|
className="mx_SpaceButton_home"
|
||||||
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
onClick={() => SpaceStore.instance.setActiveSpace(null)}
|
||||||
selected={!activeSpace}
|
selected={!activeSpace}
|
||||||
tooltip={_t("Home")}
|
tooltip={_t("All rooms")}
|
||||||
notificationState={SpaceStore.instance.getNotificationState(HOME_SPACE)}
|
notificationState={RoomNotificationStateStore.instance.globalState}
|
||||||
isNarrow={isPanelCollapsed}
|
isNarrow={isPanelCollapsed}
|
||||||
/>
|
/>
|
||||||
{ invites.map(s => <SpaceItem
|
{ invites.map(s => <SpaceItem
|
||||||
|
|
|
@ -15,12 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {IRecordingUpdate, VoiceRecording} from "../../../voice/VoiceRecording";
|
import {IRecordingUpdate, RECORDING_PLAYBACK_SAMPLES, VoiceRecording} from "../../../voice/VoiceRecording";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import {arrayFastResample, arraySeed} from "../../../utils/arrays";
|
import {arrayFastResample, arraySeed} from "../../../utils/arrays";
|
||||||
import {percentageOf} from "../../../utils/numbers";
|
import {percentageOf} from "../../../utils/numbers";
|
||||||
import Waveform from "./Waveform";
|
import Waveform from "./Waveform";
|
||||||
import {PLAYBACK_WAVEFORM_SAMPLES} from "../../../voice/Playback";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
recorder: VoiceRecording;
|
recorder: VoiceRecording;
|
||||||
|
@ -38,14 +37,14 @@ export default class LiveRecordingWaveform extends React.PureComponent<IProps, I
|
||||||
public constructor(props) {
|
public constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {heights: arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES)};
|
this.state = {heights: arraySeed(0, RECORDING_PLAYBACK_SAMPLES)};
|
||||||
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
|
this.props.recorder.liveData.onUpdate(this.onRecordingUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRecordingUpdate = (update: IRecordingUpdate) => {
|
private onRecordingUpdate = (update: IRecordingUpdate) => {
|
||||||
// The waveform and the downsample target are pretty close, so we should be fine to
|
// The waveform and the downsample target are pretty close, so we should be fine to
|
||||||
// do this, despite the docs on arrayFastResample.
|
// do this, despite the docs on arrayFastResample.
|
||||||
const bars = arrayFastResample(Array.from(update.waveform), PLAYBACK_WAVEFORM_SAMPLES);
|
const bars = arrayFastResample(Array.from(update.waveform), RECORDING_PLAYBACK_SAMPLES);
|
||||||
this.setState({
|
this.setState({
|
||||||
// The incoming data is between zero and one, but typically even screaming into a
|
// The incoming data is between zero and one, but typically even screaming into a
|
||||||
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
// microphone won't send you over 0.6, so we artificially adjust the gain for the
|
||||||
|
|
97
src/components/views/voip/AudioFeed.tsx
Normal file
97
src/components/views/voip/AudioFeed.tsx
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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, {createRef} from 'react';
|
||||||
|
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||||
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
|
import CallMediaHandler from "../../../CallMediaHandler";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
feed: CallFeed,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AudioFeed extends React.Component<IProps> {
|
||||||
|
private element = createRef<HTMLAudioElement>();
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||||
|
this.playMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||||
|
this.stopMedia();
|
||||||
|
}
|
||||||
|
|
||||||
|
private playMedia() {
|
||||||
|
const element = this.element.current;
|
||||||
|
const audioOutput = CallMediaHandler.getAudioOutput();
|
||||||
|
|
||||||
|
if (audioOutput) {
|
||||||
|
try {
|
||||||
|
// This seems quite unreliable in Chrome, although I haven't yet managed to make a jsfiddle where
|
||||||
|
// it fails.
|
||||||
|
// It seems reliable if you set the sink ID after setting the srcObject and then set the sink ID
|
||||||
|
// back to the default after the call is over - Dave
|
||||||
|
element.setSinkId(audioOutput);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Couldn't set requested audio output device: using default", e);
|
||||||
|
logger.warn("Couldn't set requested audio output device: using default", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.muted = false;
|
||||||
|
element.srcObject = this.props.feed.stream;
|
||||||
|
element.autoplay = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// A note on calling methods on media elements:
|
||||||
|
// We used to have queues per media element to serialise all calls on those elements.
|
||||||
|
// The reason given for this was that load() and play() were racing. However, we now
|
||||||
|
// never call load() explicitly so this seems unnecessary. However, serialising every
|
||||||
|
// operation was causing bugs where video would not resume because some play command
|
||||||
|
// had got stuck and all media operations were queued up behind it. If necessary, we
|
||||||
|
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||||
|
// them with another load() which will cancel the pending one, but since we don't call
|
||||||
|
// load() explicitly, it shouldn't be a problem. - Dave
|
||||||
|
element.play()
|
||||||
|
} catch (e) {
|
||||||
|
logger.info("Failed to play media element with feed", this.props.feed, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopMedia() {
|
||||||
|
const element = this.element.current;
|
||||||
|
|
||||||
|
element.pause();
|
||||||
|
element.src = null;
|
||||||
|
|
||||||
|
// As per comment in componentDidMount, setting the sink ID back to the
|
||||||
|
// default once the call is over makes setSinkId work reliably. - Dave
|
||||||
|
// Since we are not using the same element anymore, the above doesn't
|
||||||
|
// seem to be necessary - Šimon
|
||||||
|
}
|
||||||
|
|
||||||
|
private onNewStream = () => {
|
||||||
|
this.playMedia();
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<audio ref={this.element} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
60
src/components/views/voip/AudioFeedArrayForCall.tsx
Normal file
60
src/components/views/voip/AudioFeedArrayForCall.tsx
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
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 AudioFeed from "./AudioFeed"
|
||||||
|
import { CallEvent, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import { CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
call: MatrixCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
feeds: Array<CallFeed>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class AudioFeedArrayForCall extends React.Component<IProps, IState> {
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
feeds: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.props.call.addListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.props.call.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
onFeedsChanged = () => {
|
||||||
|
this.setState({
|
||||||
|
feeds: this.props.call.getRemoteFeeds(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return this.state.feeds.map((feed, i) => {
|
||||||
|
return (
|
||||||
|
<AudioFeed feed={feed} key={i} />
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ import React from 'react';
|
||||||
|
|
||||||
import CallView from "./CallView";
|
import CallView from "./CallView";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
import RoomViewStore from '../../../stores/RoomViewStore';
|
||||||
import CallHandler from '../../../CallHandler';
|
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import PersistentApp from "../elements/PersistentApp";
|
import PersistentApp from "../elements/PersistentApp";
|
||||||
|
@ -27,7 +27,6 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallEvent, CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import { Action } from '../../../dispatcher/actions';
|
|
||||||
|
|
||||||
const SHOW_CALL_IN_STATES = [
|
const SHOW_CALL_IN_STATES = [
|
||||||
CallState.Connected,
|
CallState.Connected,
|
||||||
|
@ -110,12 +109,14 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate);
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCalls);
|
||||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||||
if (this.roomStoreToken) {
|
if (this.roomStoreToken) {
|
||||||
this.roomStoreToken.remove();
|
this.roomStoreToken.remove();
|
||||||
|
@ -143,8 +144,14 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
// listen for call state changes to prod the render method, which
|
// listen for call state changes to prod the render method, which
|
||||||
// may hide the global CallView if the call it is tracking is dead
|
// may hide the global CallView if the call it is tracking is dead
|
||||||
case Action.CallChangeRoom:
|
|
||||||
case 'call_state': {
|
case 'call_state': {
|
||||||
|
this.updateCalls();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateCalls = () => {
|
||||||
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
const [primaryCall, secondaryCalls] = getPrimarySecondaryCalls(
|
||||||
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
CallHandler.sharedInstance().getAllActiveCallsNotInRoom(this.state.roomId),
|
||||||
);
|
);
|
||||||
|
@ -153,9 +160,6 @@ export default class CallPreview extends React.Component<IProps, IState> {
|
||||||
primaryCall: primaryCall,
|
primaryCall: primaryCall,
|
||||||
secondaryCall: secondaryCalls[0],
|
secondaryCall: secondaryCalls[0],
|
||||||
});
|
});
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onCallRemoteHold = () => {
|
private onCallRemoteHold = () => {
|
||||||
|
|
|
@ -20,10 +20,9 @@ import dis from '../../../dispatcher/dispatcher';
|
||||||
import CallHandler from '../../../CallHandler';
|
import CallHandler from '../../../CallHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import VideoFeed, { VideoFeedType } from "./VideoFeed";
|
import VideoFeed from './VideoFeed';
|
||||||
import RoomAvatar from "../avatars/RoomAvatar";
|
import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallState, CallType, MatrixCall, CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
|
||||||
|
@ -31,6 +30,7 @@ import {alwaysAboveLeftOf, alwaysAboveRightOf, ChevronFace, ContextMenuButton} f
|
||||||
import CallContextMenu from '../context_menus/CallContextMenu';
|
import CallContextMenu from '../context_menus/CallContextMenu';
|
||||||
import { avatarUrlForMember } from '../../../Avatar';
|
import { avatarUrlForMember } from '../../../Avatar';
|
||||||
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
import DialpadContextMenu from '../context_menus/DialpadContextMenu';
|
||||||
|
import { CallFeed } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -40,11 +40,11 @@ interface IProps {
|
||||||
// Another ongoing call to display information about
|
// Another ongoing call to display information about
|
||||||
secondaryCall?: MatrixCall,
|
secondaryCall?: MatrixCall,
|
||||||
|
|
||||||
// a callback which is called when the content in the callview changes
|
// a callback which is called when the content in the CallView changes
|
||||||
// in a way that is likely to cause a resize.
|
// in a way that is likely to cause a resize.
|
||||||
onResize?: any;
|
onResize?: any;
|
||||||
|
|
||||||
// Whether this call view is for picture-in-pictue mode
|
// Whether this call view is for picture-in-picture mode
|
||||||
// otherwise, it's the larger call view when viewing the room the call is in.
|
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||||
// This is sort of a proxy for a number of things but we currently have no
|
// This is sort of a proxy for a number of things but we currently have no
|
||||||
// need to control those things separately, so this is simpler.
|
// need to control those things separately, so this is simpler.
|
||||||
|
@ -60,6 +60,7 @@ interface IState {
|
||||||
controlsVisible: boolean,
|
controlsVisible: boolean,
|
||||||
showMoreMenu: boolean,
|
showMoreMenu: boolean,
|
||||||
showDialpad: boolean,
|
showDialpad: boolean,
|
||||||
|
feeds: CallFeed[],
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFullScreenElement() {
|
function getFullScreenElement() {
|
||||||
|
@ -115,6 +116,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
controlsVisible: true,
|
controlsVisible: true,
|
||||||
showMoreMenu: false,
|
showMoreMenu: false,
|
||||||
showDialpad: false,
|
showDialpad: false,
|
||||||
|
feeds: this.props.call.getFeeds(),
|
||||||
}
|
}
|
||||||
|
|
||||||
this.updateCallListeners(null, this.props.call);
|
this.updateCallListeners(null, this.props.call);
|
||||||
|
@ -172,11 +174,13 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
oldCall.removeListener(CallEvent.State, this.onCallState);
|
oldCall.removeListener(CallEvent.State, this.onCallState);
|
||||||
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
oldCall.removeListener(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||||
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
oldCall.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||||
|
oldCall.removeListener(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||||
}
|
}
|
||||||
if (newCall) {
|
if (newCall) {
|
||||||
newCall.on(CallEvent.State, this.onCallState);
|
newCall.on(CallEvent.State, this.onCallState);
|
||||||
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
newCall.on(CallEvent.LocalHoldUnhold, this.onCallLocalHoldUnhold);
|
||||||
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
newCall.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHoldUnhold);
|
||||||
|
newCall.on(CallEvent.FeedsChanged, this.onFeedsChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,6 +190,10 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onFeedsChanged = (newFeeds: Array<CallFeed>) => {
|
||||||
|
this.setState({feeds: newFeeds});
|
||||||
|
};
|
||||||
|
|
||||||
private onCallLocalHoldUnhold = () => {
|
private onCallLocalHoldUnhold = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLocalOnHold: this.props.call.isLocalOnHold(),
|
isLocalOnHold: this.props.call.isLocalOnHold(),
|
||||||
|
@ -304,7 +312,7 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
|
||||||
// Note that this assumes we always have a callview on screen at any given time
|
// Note that this assumes we always have a CallView on screen at any given time
|
||||||
// CallHandler would probably be a better place for this
|
// CallHandler would probably be a better place for this
|
||||||
private onNativeKeyDown = ev => {
|
private onNativeKeyDown = ev => {
|
||||||
let handled = false;
|
let handled = false;
|
||||||
|
@ -474,6 +482,8 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
{contextMenuButton}
|
{contextMenuButton}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
||||||
|
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||||
|
|
||||||
// The 'content' for the call, ie. the videos for a video call and profile picture
|
// The 'content' for the call, ie. the videos for a video call and profile picture
|
||||||
// for voice calls (fills the bg)
|
// for voice calls (fills the bg)
|
||||||
let contentView: React.ReactNode;
|
let contentView: React.ReactNode;
|
||||||
|
@ -524,41 +534,85 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is a bit messy. I can't see a reason to have two onHold/transfer screens
|
||||||
|
if (isOnHold || transfereeCall) {
|
||||||
if (this.props.call.type === CallType.Video) {
|
if (this.props.call.type === CallType.Video) {
|
||||||
let localVideoFeed = null;
|
|
||||||
let onHoldBackground = null;
|
|
||||||
const backgroundStyle: CSSProperties = {};
|
|
||||||
const containerClasses = classNames({
|
const containerClasses = classNames({
|
||||||
|
mx_CallView_content: true,
|
||||||
mx_CallView_video: true,
|
mx_CallView_video: true,
|
||||||
mx_CallView_video_hold: isOnHold,
|
mx_CallView_video_hold: isOnHold,
|
||||||
});
|
});
|
||||||
if (isOnHold) {
|
let onHoldBackground = null;
|
||||||
|
const backgroundStyle: CSSProperties = {};
|
||||||
const backgroundAvatarUrl = avatarUrlForMember(
|
const backgroundAvatarUrl = avatarUrlForMember(
|
||||||
// is it worth getting the size of the div to pass here?
|
// is it worth getting the size of the div to pass here?
|
||||||
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
this.props.call.getOpponentMember(), 1024, 1024, 'crop',
|
||||||
);
|
);
|
||||||
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
backgroundStyle.backgroundImage = 'url(' + backgroundAvatarUrl + ')';
|
||||||
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
onHoldBackground = <div className="mx_CallView_video_holdBackground" style={backgroundStyle} />;
|
||||||
}
|
|
||||||
if (!this.state.vidMuted) {
|
|
||||||
localVideoFeed = <VideoFeed type={VideoFeedType.Local} call={this.props.call} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
contentView = (
|
||||||
|
<div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||||
{onHoldBackground}
|
{onHoldBackground}
|
||||||
<VideoFeed type={VideoFeedType.Remote} call={this.props.call} onResize={this.props.onResize} />
|
|
||||||
{localVideoFeed}
|
|
||||||
{holdTransferContent}
|
{holdTransferContent}
|
||||||
{callControls}
|
{callControls}
|
||||||
</div>;
|
</div>
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const avatarSize = this.props.pipMode ? 76 : 160;
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
mx_CallView_content: true,
|
||||||
mx_CallView_voice: true,
|
mx_CallView_voice: true,
|
||||||
mx_CallView_voice_hold: isOnHold,
|
mx_CallView_voice_hold: isOnHold,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
contentView =(
|
||||||
|
<div className={classes} onMouseMove={this.onMouseMove}>
|
||||||
|
<div className="mx_CallView_voice_avatarsContainer">
|
||||||
|
<div
|
||||||
|
className="mx_CallView_voice_avatarContainer"
|
||||||
|
style={{width: avatarSize, height: avatarSize}}
|
||||||
|
>
|
||||||
|
<RoomAvatar
|
||||||
|
room={callRoom}
|
||||||
|
height={avatarSize}
|
||||||
|
width={avatarSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{holdTransferContent}
|
||||||
|
{callControls}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (this.props.call.noIncomingFeeds()) {
|
||||||
|
// Here we're reusing the css classes from voice on hold, because
|
||||||
|
// I am lazy. If this gets merged, the CallView might be subject
|
||||||
|
// to change anyway - I might take an axe to this file in order to
|
||||||
|
// try to get other things working
|
||||||
|
const classes = classNames({
|
||||||
|
mx_CallView_content: true,
|
||||||
|
mx_CallView_voice: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const feeds = this.props.call.getLocalFeeds().map((feed, i) => {
|
||||||
|
// Here we check to hide local audio feeds to achieve the same UI/UX
|
||||||
|
// as before. But once again this might be subject to change
|
||||||
|
if (feed.isVideoMuted()) return;
|
||||||
|
return (
|
||||||
|
<VideoFeed
|
||||||
|
key={i}
|
||||||
|
feed={feed}
|
||||||
|
call={this.props.call}
|
||||||
|
pipMode={this.props.pipMode}
|
||||||
|
onResize={this.props.onResize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Saying "Connecting" here isn't really true, but the best thing
|
||||||
|
// I can come up with, but this might be subject to change as well
|
||||||
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
contentView = <div className={classes} onMouseMove={this.onMouseMove}>
|
||||||
|
{feeds}
|
||||||
<div className="mx_CallView_voice_avatarsContainer">
|
<div className="mx_CallView_voice_avatarsContainer">
|
||||||
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
|
<div className="mx_CallView_voice_avatarContainer" style={{width: avatarSize, height: avatarSize}}>
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
|
@ -568,7 +622,35 @@ export default class CallView extends React.Component<IProps, IState> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{holdTransferContent}
|
<div className="mx_CallView_holdTransferContent">{_t("Connecting")}</div>
|
||||||
|
{callControls}
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
const containerClasses = classNames({
|
||||||
|
mx_CallView_content: true,
|
||||||
|
mx_CallView_video: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: Later the CallView should probably be reworked to support
|
||||||
|
// any number of feeds but now we can always expect there to be two
|
||||||
|
// feeds. This is because the js-sdk ignores any new incoming streams
|
||||||
|
const feeds = this.state.feeds.map((feed, i) => {
|
||||||
|
// Here we check to hide local audio feeds to achieve the same UI/UX
|
||||||
|
// as before. But once again this might be subject to change
|
||||||
|
if (feed.isVideoMuted() && feed.isLocal()) return;
|
||||||
|
return (
|
||||||
|
<VideoFeed
|
||||||
|
key={i}
|
||||||
|
feed={feed}
|
||||||
|
call={this.props.call}
|
||||||
|
pipMode={this.props.pipMode}
|
||||||
|
onResize={this.props.onResize}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
contentView = <div className={containerClasses} ref={this.contentRef} onMouseMove={this.onMouseMove}>
|
||||||
|
{feeds}
|
||||||
{callControls}
|
{callControls}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,13 +16,12 @@ limitations under the License.
|
||||||
|
|
||||||
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CallHandler from '../../../CallHandler';
|
import CallHandler, { CallHandlerEvent } from '../../../CallHandler';
|
||||||
import CallView from './CallView';
|
import CallView from './CallView';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import {Resizable} from "re-resizable";
|
import {Resizable} from "re-resizable";
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import { Action } from '../../../dispatcher/actions';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// What room we should display the call for
|
// What room we should display the call for
|
||||||
|
@ -55,23 +54,28 @@ export default class CallViewForRoom extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
|
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
dis.unregister(this.dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
|
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallChangeRoom, this.updateCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onAction = (payload) => {
|
private onAction = (payload) => {
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
case Action.CallChangeRoom:
|
|
||||||
case 'call_state': {
|
case 'call_state': {
|
||||||
|
this.updateCall();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private updateCall = () => {
|
||||||
const newCall = this.getCall();
|
const newCall = this.getCall();
|
||||||
if (newCall !== this.state.call) {
|
if (newCall !== this.state.call) {
|
||||||
this.setState({call: newCall});
|
this.setState({call: newCall});
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private getCall(): MatrixCall {
|
private getCall(): MatrixCall {
|
||||||
|
|
|
@ -18,52 +18,102 @@ import classnames from 'classnames';
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import React, {createRef} from 'react';
|
import React, {createRef} from 'react';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import { CallFeed, CallFeedEvent } from 'matrix-js-sdk/src/webrtc/callFeed';
|
||||||
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
|
import MemberAvatar from "../avatars/MemberAvatar"
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
export enum VideoFeedType {
|
|
||||||
Local,
|
|
||||||
Remote,
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
call: MatrixCall,
|
call: MatrixCall,
|
||||||
|
|
||||||
type: VideoFeedType,
|
feed: CallFeed,
|
||||||
|
|
||||||
|
// Whether this call view is for picture-in-picture mode
|
||||||
|
// otherwise, it's the larger call view when viewing the room the call is in.
|
||||||
|
// This is sort of a proxy for a number of things but we currently have no
|
||||||
|
// need to control those things separately, so this is simpler.
|
||||||
|
pipMode?: boolean;
|
||||||
|
|
||||||
// a callback which is called when the video element is resized
|
// a callback which is called when the video element is resized
|
||||||
// due to a change in video metadata
|
// due to a change in video metadata
|
||||||
onResize?: (e: Event) => void,
|
onResize?: (e: Event) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
audioMuted: boolean;
|
||||||
|
videoMuted: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.voip.VideoFeed")
|
@replaceableComponent("views.voip.VideoFeed")
|
||||||
export default class VideoFeed extends React.Component<IProps> {
|
export default class VideoFeed extends React.Component<IProps, IState> {
|
||||||
private vid = createRef<HTMLVideoElement>();
|
private element = createRef<HTMLVideoElement>();
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
audioMuted: this.props.feed.isAudioMuted(),
|
||||||
|
videoMuted: this.props.feed.isVideoMuted(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.vid.current.addEventListener('resize', this.onResize);
|
this.props.feed.addListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||||
this.setVideoElement();
|
this.playMedia();
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
|
||||||
if (this.props.call !== prevProps.call) {
|
|
||||||
this.setVideoElement();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this.vid.current.removeEventListener('resize', this.onResize);
|
this.props.feed.removeListener(CallFeedEvent.NewStream, this.onNewStream);
|
||||||
|
this.element.current?.removeEventListener('resize', this.onResize);
|
||||||
|
this.stopMedia();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setVideoElement() {
|
private playMedia() {
|
||||||
if (this.props.type === VideoFeedType.Local) {
|
const element = this.element.current;
|
||||||
this.props.call.setLocalVideoElement(this.vid.current);
|
if (!element) return;
|
||||||
} else {
|
// We play audio in AudioFeed, not here
|
||||||
this.props.call.setRemoteVideoElement(this.vid.current);
|
element.muted = true;
|
||||||
|
element.srcObject = this.props.feed.stream;
|
||||||
|
element.autoplay = true;
|
||||||
|
try {
|
||||||
|
// A note on calling methods on media elements:
|
||||||
|
// We used to have queues per media element to serialise all calls on those elements.
|
||||||
|
// The reason given for this was that load() and play() were racing. However, we now
|
||||||
|
// never call load() explicitly so this seems unnecessary. However, serialising every
|
||||||
|
// operation was causing bugs where video would not resume because some play command
|
||||||
|
// had got stuck and all media operations were queued up behind it. If necessary, we
|
||||||
|
// should serialise the ones that need to be serialised but then be able to interrupt
|
||||||
|
// them with another load() which will cancel the pending one, but since we don't call
|
||||||
|
// load() explicitly, it shouldn't be a problem. - Dave
|
||||||
|
element.play()
|
||||||
|
} catch (e) {
|
||||||
|
logger.info("Failed to play media element with feed", this.props.feed, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onResize = (e) => {
|
private stopMedia() {
|
||||||
if (this.props.onResize) {
|
const element = this.element.current;
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
element.pause();
|
||||||
|
element.src = null;
|
||||||
|
|
||||||
|
// As per comment in componentDidMount, setting the sink ID back to the
|
||||||
|
// default once the call is over makes setSinkId work reliably. - Dave
|
||||||
|
// Since we are not using the same element anymore, the above doesn't
|
||||||
|
// seem to be necessary - Šimon
|
||||||
|
}
|
||||||
|
|
||||||
|
private onNewStream = () => {
|
||||||
|
this.setState({
|
||||||
|
audioMuted: this.props.feed.isAudioMuted(),
|
||||||
|
videoMuted: this.props.feed.isVideoMuted(),
|
||||||
|
});
|
||||||
|
this.playMedia();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onResize = (e) => {
|
||||||
|
if (this.props.onResize && !this.props.feed.isLocal()) {
|
||||||
this.props.onResize(e);
|
this.props.onResize(e);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -71,14 +121,33 @@ export default class VideoFeed extends React.Component<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const videoClasses = {
|
const videoClasses = {
|
||||||
mx_VideoFeed: true,
|
mx_VideoFeed: true,
|
||||||
mx_VideoFeed_local: this.props.type === VideoFeedType.Local,
|
mx_VideoFeed_local: this.props.feed.isLocal(),
|
||||||
mx_VideoFeed_remote: this.props.type === VideoFeedType.Remote,
|
mx_VideoFeed_remote: !this.props.feed.isLocal(),
|
||||||
|
mx_VideoFeed_voice: this.state.videoMuted,
|
||||||
|
mx_VideoFeed_video: !this.state.videoMuted,
|
||||||
mx_VideoFeed_mirror: (
|
mx_VideoFeed_mirror: (
|
||||||
this.props.type === VideoFeedType.Local &&
|
this.props.feed.isLocal() &&
|
||||||
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
SettingsStore.getValue('VideoView.flipVideoHorizontally')
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
return <video className={classnames(videoClasses)} ref={this.vid} />;
|
if (this.state.videoMuted) {
|
||||||
|
const member = this.props.feed.getMember();
|
||||||
|
const avatarSize = this.props.pipMode ? 76 : 160;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classnames(videoClasses)} >
|
||||||
|
<MemberAvatar
|
||||||
|
member={member}
|
||||||
|
height={avatarSize}
|
||||||
|
width={avatarSize}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<video className={classnames(videoClasses)} ref={this.element} />
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -114,9 +114,6 @@ export enum Action {
|
||||||
*/
|
*/
|
||||||
VirtualRoomSupportUpdated = "virtual_room_support_updated",
|
VirtualRoomSupportUpdated = "virtual_room_support_updated",
|
||||||
|
|
||||||
// Probably would be better to have a VoIP states in a store and have the store emit changes
|
|
||||||
CallChangeRoom = "call_change_room",
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fired when an upload has started. Should be used with UploadStartedPayload.
|
* Fired when an upload has started. Should be used with UploadStartedPayload.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -833,7 +833,7 @@
|
||||||
"Match system theme": "Match system theme",
|
"Match system theme": "Match system theme",
|
||||||
"Use a system font": "Use a system font",
|
"Use a system font": "Use a system font",
|
||||||
"System font name": "System font name",
|
"System font name": "System font name",
|
||||||
"Allow Peer-to-Peer for 1:1 calls": "Allow Peer-to-Peer for 1:1 calls",
|
"Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)": "Allow Peer-to-Peer for 1:1 calls (if you enable this, the other party might be able to see your IP address)",
|
||||||
"Send analytics data": "Send analytics data",
|
"Send analytics data": "Send analytics data",
|
||||||
"Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session",
|
"Never send encrypted messages to unverified sessions from this session": "Never send encrypted messages to unverified sessions from this session",
|
||||||
"Never send encrypted messages to unverified sessions in this room from this session": "Never send encrypted messages to unverified sessions in this room from this session",
|
"Never send encrypted messages to unverified sessions in this room from this session": "Never send encrypted messages to unverified sessions in this room from this session",
|
||||||
|
@ -885,6 +885,7 @@
|
||||||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||||
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||||
|
"Connecting": "Connecting",
|
||||||
"Video Call": "Video Call",
|
"Video Call": "Video Call",
|
||||||
"Voice Call": "Voice Call",
|
"Voice Call": "Voice Call",
|
||||||
"Fill Screen": "Fill Screen",
|
"Fill Screen": "Fill Screen",
|
||||||
|
@ -1011,7 +1012,7 @@
|
||||||
"Create": "Create",
|
"Create": "Create",
|
||||||
"Expand space panel": "Expand space panel",
|
"Expand space panel": "Expand space panel",
|
||||||
"Collapse space panel": "Collapse space panel",
|
"Collapse space panel": "Collapse space panel",
|
||||||
"Home": "Home",
|
"All rooms": "All rooms",
|
||||||
"Click to copy": "Click to copy",
|
"Click to copy": "Click to copy",
|
||||||
"Copied!": "Copied!",
|
"Copied!": "Copied!",
|
||||||
"Failed to copy": "Failed to copy",
|
"Failed to copy": "Failed to copy",
|
||||||
|
@ -1252,8 +1253,9 @@
|
||||||
"olm version:": "olm version:",
|
"olm version:": "olm version:",
|
||||||
"Homeserver is": "Homeserver is",
|
"Homeserver is": "Homeserver is",
|
||||||
"Identity Server is": "Identity Server is",
|
"Identity Server is": "Identity Server is",
|
||||||
"Access Token:": "Access Token:",
|
"Access Token": "Access Token",
|
||||||
"click to reveal": "click to reveal",
|
"Your access token gives full access to your account. Do not share it with anyone.": "Your access token gives full access to your account. Do not share it with anyone.",
|
||||||
|
"Copy": "Copy",
|
||||||
"Clear cache and reload": "Clear cache and reload",
|
"Clear cache and reload": "Clear cache and reload",
|
||||||
"Labs": "Labs",
|
"Labs": "Labs",
|
||||||
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.",
|
"Customise your experience with experimental labs features. <a>Learn more</a>.": "Customise your experience with experimental labs features. <a>Learn more</a>.",
|
||||||
|
@ -1439,6 +1441,13 @@
|
||||||
"Someone is using an unknown session": "Someone is using an unknown session",
|
"Someone is using an unknown session": "Someone is using an unknown session",
|
||||||
"This room is end-to-end encrypted": "This room is end-to-end encrypted",
|
"This room is end-to-end encrypted": "This room is end-to-end encrypted",
|
||||||
"Everyone in this room is verified": "Everyone in this room is verified",
|
"Everyone in this room is verified": "Everyone in this room is verified",
|
||||||
|
"Server error": "Server error",
|
||||||
|
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
||||||
|
"Unknown Command": "Unknown Command",
|
||||||
|
"Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s",
|
||||||
|
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
|
||||||
|
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
|
||||||
|
"Send as message": "Send as message",
|
||||||
"Edit message": "Edit message",
|
"Edit message": "Edit message",
|
||||||
"Mod": "Mod",
|
"Mod": "Mod",
|
||||||
"This event could not be displayed": "This event could not be displayed",
|
"This event could not be displayed": "This event could not be displayed",
|
||||||
|
@ -1629,13 +1638,6 @@
|
||||||
"This Room": "This Room",
|
"This Room": "This Room",
|
||||||
"All Rooms": "All Rooms",
|
"All Rooms": "All Rooms",
|
||||||
"Search…": "Search…",
|
"Search…": "Search…",
|
||||||
"Server error": "Server error",
|
|
||||||
"Server unavailable, overloaded, or something else went wrong.": "Server unavailable, overloaded, or something else went wrong.",
|
|
||||||
"Unknown Command": "Unknown Command",
|
|
||||||
"Unrecognised command: %(commandText)s": "Unrecognised command: %(commandText)s",
|
|
||||||
"You can use <code>/help</code> to list available commands. Did you mean to send this as a message?": "You can use <code>/help</code> to list available commands. Did you mean to send this as a message?",
|
|
||||||
"Hint: Begin your message with <code>//</code> to start it with a slash.": "Hint: Begin your message with <code>//</code> to start it with a slash.",
|
|
||||||
"Send as message": "Send as message",
|
|
||||||
"Failed to connect to integration manager": "Failed to connect to integration manager",
|
"Failed to connect to integration manager": "Failed to connect to integration manager",
|
||||||
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
"You don't currently have any stickerpacks enabled": "You don't currently have any stickerpacks enabled",
|
||||||
"Add some now": "Add some now",
|
"Add some now": "Add some now",
|
||||||
|
@ -2014,10 +2016,10 @@
|
||||||
"Continue with %(provider)s": "Continue with %(provider)s",
|
"Continue with %(provider)s": "Continue with %(provider)s",
|
||||||
"Sign in with single sign-on": "Sign in with single sign-on",
|
"Sign in with single sign-on": "Sign in with single sign-on",
|
||||||
"And %(count)s more...|other": "And %(count)s more...",
|
"And %(count)s more...|other": "And %(count)s more...",
|
||||||
|
"Home": "Home",
|
||||||
"Enter a server name": "Enter a server name",
|
"Enter a server name": "Enter a server name",
|
||||||
"Looks good": "Looks good",
|
"Looks good": "Looks good",
|
||||||
"Can't find this server or its room list": "Can't find this server or its room list",
|
"Can't find this server or its room list": "Can't find this server or its room list",
|
||||||
"All rooms": "All rooms",
|
|
||||||
"Your server": "Your server",
|
"Your server": "Your server",
|
||||||
"Are you sure you want to remove <b>%(serverName)s</b>": "Are you sure you want to remove <b>%(serverName)s</b>",
|
"Are you sure you want to remove <b>%(serverName)s</b>": "Are you sure you want to remove <b>%(serverName)s</b>",
|
||||||
"Remove server": "Remove server",
|
"Remove server": "Remove server",
|
||||||
|
@ -2033,10 +2035,11 @@
|
||||||
"Direct Messages": "Direct Messages",
|
"Direct Messages": "Direct Messages",
|
||||||
"Space selection": "Space selection",
|
"Space selection": "Space selection",
|
||||||
"Add existing rooms": "Add existing rooms",
|
"Add existing rooms": "Add existing rooms",
|
||||||
"Don't want to add an existing room?": "Don't want to add an existing room?",
|
"Not all selected were added": "Not all selected were added",
|
||||||
|
"Adding rooms... (%(progress)s out of %(count)s)|other": "Adding rooms... (%(progress)s out of %(count)s)",
|
||||||
|
"Adding rooms... (%(progress)s out of %(count)s)|one": "Adding room...",
|
||||||
|
"Want to add a new room instead?": "Want to add a new room instead?",
|
||||||
"Create a new room": "Create a new room",
|
"Create a new room": "Create a new room",
|
||||||
"Failed to add rooms to space": "Failed to add rooms to space",
|
|
||||||
"Adding...": "Adding...",
|
|
||||||
"Matrix ID": "Matrix ID",
|
"Matrix ID": "Matrix ID",
|
||||||
"Matrix Room ID": "Matrix Room ID",
|
"Matrix Room ID": "Matrix Room ID",
|
||||||
"email address": "email address",
|
"email address": "email address",
|
||||||
|
@ -2347,7 +2350,6 @@
|
||||||
"Share Community": "Share Community",
|
"Share Community": "Share Community",
|
||||||
"Share Room Message": "Share Room Message",
|
"Share Room Message": "Share Room Message",
|
||||||
"Link to selected message": "Link to selected message",
|
"Link to selected message": "Link to selected message",
|
||||||
"Copy": "Copy",
|
|
||||||
"Command Help": "Command Help",
|
"Command Help": "Command Help",
|
||||||
"Failed to save space settings.": "Failed to save space settings.",
|
"Failed to save space settings.": "Failed to save space settings.",
|
||||||
"Space settings": "Space settings",
|
"Space settings": "Space settings",
|
||||||
|
@ -2655,6 +2657,7 @@
|
||||||
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces",
|
"%(count)s rooms and %(numSpaces)s spaces|one": "%(count)s room and %(numSpaces)s spaces",
|
||||||
"%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space",
|
"%(count)s rooms and 1 space|other": "%(count)s rooms and 1 space",
|
||||||
"%(count)s rooms and 1 space|one": "%(count)s room and 1 space",
|
"%(count)s rooms and 1 space|one": "%(count)s room and 1 space",
|
||||||
|
"Select a room below first": "Select a room below first",
|
||||||
"Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later",
|
"Failed to remove some rooms. Try again later": "Failed to remove some rooms. Try again later",
|
||||||
"Removing...": "Removing...",
|
"Removing...": "Removing...",
|
||||||
"Mark as not suggested": "Mark as not suggested",
|
"Mark as not suggested": "Mark as not suggested",
|
||||||
|
@ -2667,7 +2670,6 @@
|
||||||
"Public space": "Public space",
|
"Public space": "Public space",
|
||||||
"Private space": "Private space",
|
"Private space": "Private space",
|
||||||
"<inviter/> invites you": "<inviter/> invites you",
|
"<inviter/> invites you": "<inviter/> invites you",
|
||||||
"Add existing rooms & spaces": "Add existing rooms & spaces",
|
|
||||||
"Welcome to <name/>": "Welcome to <name/>",
|
"Welcome to <name/>": "Welcome to <name/>",
|
||||||
"Random": "Random",
|
"Random": "Random",
|
||||||
"Support": "Support",
|
"Support": "Support",
|
||||||
|
@ -2675,6 +2677,8 @@
|
||||||
"Failed to create initial space rooms": "Failed to create initial space rooms",
|
"Failed to create initial space rooms": "Failed to create initial space rooms",
|
||||||
"Skip for now": "Skip for now",
|
"Skip for now": "Skip for now",
|
||||||
"Creating rooms...": "Creating rooms...",
|
"Creating rooms...": "Creating rooms...",
|
||||||
|
"Failed to add rooms to space": "Failed to add rooms to space",
|
||||||
|
"Adding...": "Adding...",
|
||||||
"What do you want to organise?": "What do you want to organise?",
|
"What do you want to organise?": "What do you want to organise?",
|
||||||
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
|
"Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.": "Pick rooms or conversations to add. This is just a space for you, no one will be informed. You can add more later.",
|
||||||
"Share %(name)s": "Share %(name)s",
|
"Share %(name)s": "Share %(name)s",
|
||||||
|
|
|
@ -438,7 +438,10 @@ export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
},
|
},
|
||||||
"webRtcAllowPeerToPeer": {
|
"webRtcAllowPeerToPeer": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
displayName: _td('Allow Peer-to-Peer for 1:1 calls'),
|
displayName: _td(
|
||||||
|
"Allow Peer-to-Peer for 1:1 calls " +
|
||||||
|
"(if you enable this, the other party might be able to see your IP address)",
|
||||||
|
),
|
||||||
default: true,
|
default: true,
|
||||||
invertedSettingName: 'webRtcForceTURN',
|
invertedSettingName: 'webRtcForceTURN',
|
||||||
},
|
},
|
||||||
|
|
|
@ -122,7 +122,7 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async appendRoom(room: Room) {
|
private async appendRoom(room: Room) {
|
||||||
if (room.isSpaceRoom() && SettingsStore.getValue("feature_spaces")) return; // hide space rooms
|
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) return; // hide space rooms
|
||||||
let updated = false;
|
let updated = false;
|
||||||
const rooms = (this.state.rooms || []).slice(); // cheap clone
|
const rooms = (this.state.rooms || []).slice(); // cheap clone
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {sortBy, throttle} from "lodash";
|
import {ListIteratee, Many, sortBy, throttle} from "lodash";
|
||||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
@ -31,28 +31,23 @@ import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateS
|
||||||
import {DefaultTagID} from "./room-list/models";
|
import {DefaultTagID} from "./room-list/models";
|
||||||
import {EnhancedMap, mapDiff} from "../utils/maps";
|
import {EnhancedMap, mapDiff} from "../utils/maps";
|
||||||
import {setHasDiff} from "../utils/sets";
|
import {setHasDiff} from "../utils/sets";
|
||||||
import {objectDiff} from "../utils/objects";
|
|
||||||
import {arrayHasDiff} from "../utils/arrays";
|
|
||||||
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
|
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
|
||||||
import RoomViewStore from "./RoomViewStore";
|
import RoomViewStore from "./RoomViewStore";
|
||||||
|
|
||||||
type SpaceKey = string | symbol;
|
|
||||||
|
|
||||||
interface IState {}
|
interface IState {}
|
||||||
|
|
||||||
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
const ACTIVE_SPACE_LS_KEY = "mx_active_space";
|
||||||
|
|
||||||
export const HOME_SPACE = Symbol("home-space");
|
|
||||||
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
|
export const SUGGESTED_ROOMS = Symbol("suggested-rooms");
|
||||||
|
|
||||||
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
export const UPDATE_TOP_LEVEL_SPACES = Symbol("top-level-spaces");
|
||||||
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
|
export const UPDATE_INVITED_SPACES = Symbol("invited-spaces");
|
||||||
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
export const UPDATE_SELECTED_SPACE = Symbol("selected-space");
|
||||||
// Space Room ID/HOME_SPACE will be emitted when a Space's children change
|
// Space Room ID will be emitted when a Space's children change
|
||||||
|
|
||||||
const MAX_SUGGESTED_ROOMS = 20;
|
const MAX_SUGGESTED_ROOMS = 20;
|
||||||
|
|
||||||
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "home_space"}`;
|
const getSpaceContextKey = (space?: Room) => `mx_space_context_${space?.roomId || "ALL_ROOMS"}`;
|
||||||
|
|
||||||
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
|
const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces, rooms]
|
||||||
return arr.reduce((result, room: Room) => {
|
return arr.reduce((result, room: Room) => {
|
||||||
|
@ -61,15 +56,18 @@ const partitionSpacesAndRooms = (arr: Room[]): [Room[], Room[]] => { // [spaces,
|
||||||
}, [[], []]);
|
}, [[], []]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getOrder = (ev: MatrixEvent): string | null => {
|
// For sorting space children using a validated `order`, `m.room.create`'s `origin_server_ts`, `room_id`
|
||||||
const content = ev.getContent();
|
export const getOrder = (order: string, creationTs: number, roomId: string): Array<Many<ListIteratee<any>>> => {
|
||||||
if (typeof content.order === "string" && Array.from(content.order).every((c: string) => {
|
let validatedOrder: string = null;
|
||||||
|
|
||||||
|
if (typeof order === "string" && Array.from(order).every((c: string) => {
|
||||||
const charCode = c.charCodeAt(0);
|
const charCode = c.charCodeAt(0);
|
||||||
return charCode >= 0x20 && charCode <= 0x7F;
|
return charCode >= 0x20 && charCode <= 0x7F;
|
||||||
})) {
|
})) {
|
||||||
return content.order;
|
validatedOrder = order;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
return [validatedOrder, creationTs, roomId];
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRoomFn: FetchRoomFn = (room: Room) => {
|
const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||||
|
@ -83,15 +81,13 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
// The spaces representing the roots of the various tree-like hierarchies
|
// The spaces representing the roots of the various tree-like hierarchies
|
||||||
private rootSpaces: Room[] = [];
|
private rootSpaces: Room[] = [];
|
||||||
// The list of rooms not present in any currently joined spaces
|
|
||||||
private orphanedRooms = new Set<string>();
|
|
||||||
// Map from room ID to set of spaces which list it as a child
|
// Map from room ID to set of spaces which list it as a child
|
||||||
private parentMap = new EnhancedMap<string, Set<string>>();
|
private parentMap = new EnhancedMap<string, Set<string>>();
|
||||||
// Map from space key to SpaceNotificationState instance representing that space
|
// Map from spaceId to SpaceNotificationState instance representing that space
|
||||||
private notificationStateMap = new Map<SpaceKey, SpaceNotificationState>();
|
private notificationStateMap = new Map<string, SpaceNotificationState>();
|
||||||
// Map from space key to Set of room IDs that should be shown as part of that space's filter
|
// Map from space key to Set of room IDs that should be shown as part of that space's filter
|
||||||
private spaceFilteredRooms = new Map<string | symbol, Set<string>>();
|
private spaceFilteredRooms = new Map<string, Set<string>>();
|
||||||
// The space currently selected in the Space Panel - if null then `Home` is selected
|
// The space currently selected in the Space Panel - if null then All Rooms is selected
|
||||||
private _activeSpace?: Room = null;
|
private _activeSpace?: Room = null;
|
||||||
private _suggestedRooms: ISpaceSummaryRoom[] = [];
|
private _suggestedRooms: ISpaceSummaryRoom[] = [];
|
||||||
private _invitedSpaces = new Set<Room>();
|
private _invitedSpaces = new Set<Room>();
|
||||||
|
@ -212,9 +208,16 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private getChildren(spaceId: string): Room[] {
|
private getChildren(spaceId: string): Room[] {
|
||||||
const room = this.matrixClient?.getRoom(spaceId);
|
const room = this.matrixClient?.getRoom(spaceId);
|
||||||
const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
|
const childEvents = room?.currentState.getStateEvents(EventType.SpaceChild).filter(ev => ev.getContent()?.via);
|
||||||
return sortBy(childEvents, getOrder)
|
return sortBy(childEvents, ev => {
|
||||||
.map(ev => this.matrixClient.getRoom(ev.getStateKey()))
|
const roomId = ev.getStateKey();
|
||||||
.filter(room => room?.getMyMembership() === "join" || room?.getMyMembership() === "invite") || [];
|
const childRoom = this.matrixClient?.getRoom(roomId);
|
||||||
|
const createTs = childRoom?.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs();
|
||||||
|
return getOrder(ev.getContent().order, createTs, roomId);
|
||||||
|
}).map(ev => {
|
||||||
|
return this.matrixClient.getRoom(ev.getStateKey());
|
||||||
|
}).filter(room => {
|
||||||
|
return room?.getMyMembership() === "join" || room?.getMyMembership() === "invite";
|
||||||
|
}) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public getChildRooms(spaceId: string): Room[] {
|
public getChildRooms(spaceId: string): Room[] {
|
||||||
|
@ -246,7 +249,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
public getSpaceFilteredRoomIds = (space: Room | null): Set<string> => {
|
||||||
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
|
if (!space) {
|
||||||
|
return new Set(this.matrixClient.getVisibleRooms().map(r => r.roomId));
|
||||||
|
}
|
||||||
|
return this.spaceFilteredRooms.get(space.roomId) || new Set();
|
||||||
};
|
};
|
||||||
|
|
||||||
private rebuild = throttle(() => {
|
private rebuild = throttle(() => {
|
||||||
|
@ -277,7 +283,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
|
const [rootSpaces] = partitionSpacesAndRooms(Array.from(unseenChildren));
|
||||||
|
|
||||||
// somewhat algorithm to handle full-cycles
|
// somewhat algorithm to handle full-cycles
|
||||||
const detachedNodes = new Set<Room>(spaces);
|
const detachedNodes = new Set<Room>(spaces);
|
||||||
|
@ -318,7 +324,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// rootSpaces.push(space);
|
// rootSpaces.push(space);
|
||||||
// });
|
// });
|
||||||
|
|
||||||
this.orphanedRooms = new Set(orphanedRooms);
|
|
||||||
this.rootSpaces = rootSpaces;
|
this.rootSpaces = rootSpaces;
|
||||||
this.parentMap = backrefs;
|
this.parentMap = backrefs;
|
||||||
|
|
||||||
|
@ -339,25 +344,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.rebuild();
|
this.rebuild();
|
||||||
}
|
}
|
||||||
|
|
||||||
private showInHomeSpace = (room: Room) => {
|
|
||||||
if (room.isSpaceRoom()) return false;
|
|
||||||
return !this.parentMap.get(room.roomId)?.size // put all orphaned rooms in the Home Space
|
|
||||||
|| DMRoomMap.shared().getUserIdForRoomId(room.roomId) // put all DMs in the Home Space
|
|
||||||
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite) // show all favourites
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update a given room due to its tag changing (e.g DM-ness or Fav-ness)
|
|
||||||
// This can only change whether it shows up in the HOME_SPACE or not
|
|
||||||
private onRoomUpdate = (room: Room) => {
|
|
||||||
if (this.showInHomeSpace(room)) {
|
|
||||||
this.spaceFilteredRooms.get(HOME_SPACE)?.add(room.roomId);
|
|
||||||
this.emit(HOME_SPACE);
|
|
||||||
} else if (!this.orphanedRooms.has(room.roomId)) {
|
|
||||||
this.spaceFilteredRooms.get(HOME_SPACE)?.delete(room.roomId);
|
|
||||||
this.emit(HOME_SPACE);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private onSpaceMembersChange = (ev: MatrixEvent) => {
|
private onSpaceMembersChange = (ev: MatrixEvent) => {
|
||||||
// skip this update if we do not have a DM with this user
|
// skip this update if we do not have a DM with this user
|
||||||
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
|
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
|
||||||
|
@ -371,16 +357,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
const oldFilteredRooms = this.spaceFilteredRooms;
|
const oldFilteredRooms = this.spaceFilteredRooms;
|
||||||
this.spaceFilteredRooms = new Map();
|
this.spaceFilteredRooms = new Map();
|
||||||
|
|
||||||
// put all room invites in the Home Space
|
|
||||||
const invites = visibleRooms.filter(r => !r.isSpaceRoom() && r.getMyMembership() === "invite");
|
|
||||||
this.spaceFilteredRooms.set(HOME_SPACE, new Set<string>(invites.map(room => room.roomId)));
|
|
||||||
|
|
||||||
visibleRooms.forEach(room => {
|
|
||||||
if (this.showInHomeSpace(room)) {
|
|
||||||
this.spaceFilteredRooms.get(HOME_SPACE).add(room.roomId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.rootSpaces.forEach(s => {
|
this.rootSpaces.forEach(s => {
|
||||||
// traverse each space tree in DFS to build up the supersets as you go up,
|
// traverse each space tree in DFS to build up the supersets as you go up,
|
||||||
// reusing results from like subtrees.
|
// reusing results from like subtrees.
|
||||||
|
@ -425,7 +401,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
this.spaceFilteredRooms.forEach((roomIds, s) => {
|
this.spaceFilteredRooms.forEach((roomIds, s) => {
|
||||||
// Update NotificationStates
|
// Update NotificationStates
|
||||||
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => roomIds.has(room.roomId)));
|
this.getNotificationState(s)?.setRooms(visibleRooms.filter(room => {
|
||||||
|
if (roomIds.has(room.roomId)) {
|
||||||
|
return !DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|
||||||
|
|| RoomListStore.instance.getTagsForRoom(room).includes(DefaultTagID.Favourite);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
}, 100, {trailing: true, leading: true});
|
}, 100, {trailing: true, leading: true});
|
||||||
|
|
||||||
|
@ -503,8 +486,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// TODO confirm this after implementing parenting behaviour
|
// TODO confirm this after implementing parenting behaviour
|
||||||
if (room.isSpaceRoom()) {
|
if (room.isSpaceRoom()) {
|
||||||
this.onSpaceUpdate();
|
this.onSpaceUpdate();
|
||||||
} else {
|
|
||||||
this.onRoomUpdate(room);
|
|
||||||
}
|
}
|
||||||
this.emit(room.roomId);
|
this.emit(room.roomId);
|
||||||
break;
|
break;
|
||||||
|
@ -517,38 +498,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRoomAccountData = (ev: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => {
|
|
||||||
if (ev.getType() === EventType.Tag && !room.isSpaceRoom()) {
|
|
||||||
// If the room was in favourites and now isn't or the opposite then update its position in the trees
|
|
||||||
const oldTags = lastEvent?.getContent()?.tags || {};
|
|
||||||
const newTags = ev.getContent()?.tags || {};
|
|
||||||
if (!!oldTags[DefaultTagID.Favourite] !== !!newTags[DefaultTagID.Favourite]) {
|
|
||||||
this.onRoomUpdate(room);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAccountData = (ev: MatrixEvent, lastEvent: MatrixEvent) => {
|
|
||||||
if (ev.getType() === EventType.Direct) {
|
|
||||||
const lastContent = lastEvent.getContent();
|
|
||||||
const content = ev.getContent();
|
|
||||||
|
|
||||||
const diff = objectDiff<Record<string, string[]>>(lastContent, content);
|
|
||||||
// filter out keys which changed by reference only by checking whether the sets differ
|
|
||||||
const changed = diff.changed.filter(k => arrayHasDiff(lastContent[k], content[k]));
|
|
||||||
// DM tag changes, refresh relevant rooms
|
|
||||||
new Set([...diff.added, ...diff.removed, ...changed]).forEach(roomId => {
|
|
||||||
const room = this.matrixClient?.getRoom(roomId);
|
|
||||||
if (room) {
|
|
||||||
this.onRoomUpdate(room);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
protected async reset() {
|
protected async reset() {
|
||||||
this.rootSpaces = [];
|
this.rootSpaces = [];
|
||||||
this.orphanedRooms = new Set();
|
|
||||||
this.parentMap = new EnhancedMap();
|
this.parentMap = new EnhancedMap();
|
||||||
this.notificationStateMap = new Map();
|
this.notificationStateMap = new Map();
|
||||||
this.spaceFilteredRooms = new Map();
|
this.spaceFilteredRooms = new Map();
|
||||||
|
@ -563,8 +514,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.matrixClient.removeListener("Room", this.onRoom);
|
this.matrixClient.removeListener("Room", this.onRoom);
|
||||||
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
|
this.matrixClient.removeListener("Room.myMembership", this.onRoom);
|
||||||
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
|
this.matrixClient.removeListener("RoomState.events", this.onRoomState);
|
||||||
this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData);
|
|
||||||
this.matrixClient.removeListener("accountData", this.onAccountData);
|
|
||||||
}
|
}
|
||||||
await this.reset();
|
await this.reset();
|
||||||
}
|
}
|
||||||
|
@ -574,8 +523,6 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.matrixClient.on("Room", this.onRoom);
|
this.matrixClient.on("Room", this.onRoom);
|
||||||
this.matrixClient.on("Room.myMembership", this.onRoom);
|
this.matrixClient.on("Room.myMembership", this.onRoom);
|
||||||
this.matrixClient.on("RoomState.events", this.onRoomState);
|
this.matrixClient.on("RoomState.events", this.onRoomState);
|
||||||
this.matrixClient.on("Room.accountData", this.onRoomAccountData);
|
|
||||||
this.matrixClient.on("accountData", this.onAccountData);
|
|
||||||
|
|
||||||
await this.onSpaceUpdate(); // trigger an initial update
|
await this.onSpaceUpdate(); // trigger an initial update
|
||||||
|
|
||||||
|
@ -600,7 +547,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
// Don't context switch when navigating to the space room
|
// Don't context switch when navigating to the space room
|
||||||
// as it will cause you to end up in the wrong room
|
// as it will cause you to end up in the wrong room
|
||||||
this.setActiveSpace(room, false);
|
this.setActiveSpace(room, false);
|
||||||
} else if (!this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
|
} else if (this.activeSpace && !this.getSpaceFilteredRoomIds(this.activeSpace).has(roomId)) {
|
||||||
this.switchToRelatedSpace(roomId);
|
this.switchToRelatedSpace(roomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -618,7 +565,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getNotificationState(key: SpaceKey): SpaceNotificationState {
|
public getNotificationState(key: string): SpaceNotificationState {
|
||||||
if (this.notificationStateMap.has(key)) {
|
if (this.notificationStateMap.has(key)) {
|
||||||
return this.notificationStateMap.get(key);
|
return this.notificationStateMap.get(key);
|
||||||
}
|
}
|
||||||
|
|
|
@ -668,7 +668,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
* and thus might not cause an update to the store immediately.
|
* and thus might not cause an update to the store immediately.
|
||||||
* @param {IFilterCondition} filter The filter condition to add.
|
* @param {IFilterCondition} filter The filter condition to add.
|
||||||
*/
|
*/
|
||||||
public addFilter(filter: IFilterCondition): void {
|
public async addFilter(filter: IFilterCondition): Promise<void> {
|
||||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log("Adding filter condition:", filter);
|
console.log("Adding filter condition:", filter);
|
||||||
|
@ -680,13 +680,15 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
promise = this.recalculatePrefiltering();
|
promise = this.recalculatePrefiltering();
|
||||||
} else {
|
} else {
|
||||||
this.filterConditions.push(filter);
|
this.filterConditions.push(filter);
|
||||||
|
// Runtime filters with spaces disable prefiltering for the search all spaces feature
|
||||||
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
|
// this has to be awaited so that `setKnownRooms` is called in time for the `addFilterCondition` below
|
||||||
|
// this way the runtime filters are only evaluated on one dataset and not both.
|
||||||
|
await this.recalculatePrefiltering();
|
||||||
|
}
|
||||||
if (this.algorithm) {
|
if (this.algorithm) {
|
||||||
this.algorithm.addFilterCondition(filter);
|
this.algorithm.addFilterCondition(filter);
|
||||||
}
|
}
|
||||||
// Runtime filters with spaces disable prefiltering for the search all spaces effect
|
|
||||||
if (SettingsStore.getValue("feature_spaces")) {
|
|
||||||
promise = this.recalculatePrefiltering();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
promise.then(() => this.updateFn.trigger());
|
promise.then(() => this.updateFn.trigger());
|
||||||
}
|
}
|
||||||
|
@ -710,12 +712,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
if (this.algorithm) {
|
if (this.algorithm) {
|
||||||
this.algorithm.removeFilterCondition(filter);
|
this.algorithm.removeFilterCondition(filter);
|
||||||
// Runtime filters with spaces disable prefiltering for the search all spaces effect
|
}
|
||||||
|
// Runtime filters with spaces disable prefiltering for the search all spaces feature
|
||||||
if (SettingsStore.getValue("feature_spaces")) {
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
promise = this.recalculatePrefiltering();
|
promise = this.recalculatePrefiltering();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
idx = this.prefilterConditions.indexOf(filter);
|
idx = this.prefilterConditions.indexOf(filter);
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
filter.off(FILTER_CHANGED, this.onPrefilterUpdated);
|
filter.off(FILTER_CHANGED, this.onPrefilterUpdated);
|
||||||
|
|
|
@ -24,26 +24,34 @@ import SpaceStore, { UPDATE_SELECTED_SPACE } from "../SpaceStore";
|
||||||
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
* Watches for changes in spaces to manage the filter on the provided RoomListStore
|
||||||
*/
|
*/
|
||||||
export class SpaceWatcher {
|
export class SpaceWatcher {
|
||||||
private filter = new SpaceFilterCondition();
|
private filter: SpaceFilterCondition;
|
||||||
private activeSpace: Room = SpaceStore.instance.activeSpace;
|
private activeSpace: Room = SpaceStore.instance.activeSpace;
|
||||||
|
|
||||||
constructor(private store: RoomListStoreClass) {
|
constructor(private store: RoomListStoreClass) {
|
||||||
this.updateFilter(); // get the filter into a consistent state
|
|
||||||
store.addFilter(this.filter);
|
|
||||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSelectedSpaceUpdated = (activeSpace: Room) => {
|
private onSelectedSpaceUpdated = (activeSpace?: Room) => {
|
||||||
this.activeSpace = activeSpace;
|
this.activeSpace = activeSpace;
|
||||||
|
|
||||||
|
if (this.filter) {
|
||||||
|
if (activeSpace) {
|
||||||
this.updateFilter();
|
this.updateFilter();
|
||||||
|
} else {
|
||||||
|
this.store.removeFilter(this.filter);
|
||||||
|
this.filter = null;
|
||||||
|
}
|
||||||
|
} else if (activeSpace) {
|
||||||
|
this.filter = new SpaceFilterCondition();
|
||||||
|
this.updateFilter();
|
||||||
|
this.store.addFilter(this.filter);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateFilter = () => {
|
private updateFilter = () => {
|
||||||
if (this.activeSpace) {
|
|
||||||
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
|
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
|
||||||
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
this.filter.updateSpace(this.activeSpace);
|
this.filter.updateSpace(this.activeSpace);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -199,8 +199,10 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doUpdateStickyRoom(val: Room) {
|
private async doUpdateStickyRoom(val: Room) {
|
||||||
|
if (SettingsStore.getValue("feature_spaces") && val?.isSpaceRoom() && val.getMyMembership() !== "invite") {
|
||||||
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
// no-op sticky rooms for spaces - they're effectively virtual rooms
|
||||||
if (val?.isSpaceRoom() && val.getMyMembership() !== "invite") val = null;
|
val = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
// Note throughout: We need async so we can wait for handleRoomUpdate() to do its thing,
|
||||||
// otherwise we risk duplicating rooms.
|
// otherwise we risk duplicating rooms.
|
||||||
|
@ -577,9 +579,8 @@ export class Algorithm extends EventEmitter {
|
||||||
|
|
||||||
await this.generateFreshTags(newTags);
|
await this.generateFreshTags(newTags);
|
||||||
|
|
||||||
this.cachedRooms = newTags;
|
this.cachedRooms = newTags; // this recalculates the filtered rooms for us
|
||||||
this.updateTagsFromCache();
|
this.updateTagsFromCache();
|
||||||
this.recalculateFilteredRooms();
|
|
||||||
|
|
||||||
// Now that we've finished generation, we need to update the sticky room to what
|
// Now that we've finished generation, we need to update the sticky room to what
|
||||||
// it was. It's entirely possible that it changed lists though, so if it did then
|
// it was. It's entirely possible that it changed lists though, so if it did then
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
|
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
|
||||||
import { IDestroyable } from "../../../utils/IDestroyable";
|
import { IDestroyable } from "../../../utils/IDestroyable";
|
||||||
import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
|
import SpaceStore from "../../SpaceStore";
|
||||||
import { setHasDiff } from "../../../utils/sets";
|
import { setHasDiff } from "../../../utils/sets";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -55,10 +55,12 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private getSpaceEventKey = (space: Room | null) => space ? space.roomId : HOME_SPACE;
|
private getSpaceEventKey = (space: Room) => space.roomId;
|
||||||
|
|
||||||
public updateSpace(space: Room) {
|
public updateSpace(space: Room) {
|
||||||
|
if (this.space) {
|
||||||
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
SpaceStore.instance.off(this.getSpaceEventKey(this.space), this.onStoreUpdate);
|
||||||
|
}
|
||||||
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
|
SpaceStore.instance.on(this.getSpaceEventKey(this.space = space), this.onStoreUpdate);
|
||||||
this.onStoreUpdate(); // initial update from the change to the space
|
this.onStoreUpdate(); // initial update from the change to the space
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,7 +50,7 @@ export class VisibilityProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
// hide space rooms as they'll be shown in the SpacePanel
|
// hide space rooms as they'll be shown in the SpacePanel
|
||||||
if (room.isSpaceRoom() && SettingsStore.getValue("feature_spaces")) {
|
if (SettingsStore.getValue("feature_spaces") && room.isSpaceRoom()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,63 +17,8 @@ limitations under the License.
|
||||||
// Pull in the encryption lib so that we can decrypt attachments.
|
// Pull in the encryption lib so that we can decrypt attachments.
|
||||||
import encrypt from 'browser-encrypt-attachment';
|
import encrypt from 'browser-encrypt-attachment';
|
||||||
import {mediaFromContent} from "../customisations/Media";
|
import {mediaFromContent} from "../customisations/Media";
|
||||||
import {IEncryptedFile} from "../customisations/models/IMediaEventContent";
|
import { IEncryptedFile } from "../customisations/models/IMediaEventContent";
|
||||||
|
import { getBlobSafeMimeType } from "./blobs";
|
||||||
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
|
||||||
// as for performance reasons these are now rendered via URL.createObjectURL()
|
|
||||||
// rather than by converting into data: URIs.
|
|
||||||
//
|
|
||||||
// This means that the content is rendered using the origin of the script which
|
|
||||||
// called createObjectURL(), and so if the content contains any scripting then it
|
|
||||||
// will pose a XSS vulnerability when the browser renders it. This is particularly
|
|
||||||
// bad if the user right-clicks the URI and pastes it into a new window or tab,
|
|
||||||
// as the blob will then execute with access to Element's full JS environment(!)
|
|
||||||
//
|
|
||||||
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
|
|
||||||
// for details.
|
|
||||||
//
|
|
||||||
// We mitigate this by only allowing mime-types into blobs which we know don't
|
|
||||||
// contain any scripting, and instantiate all others as application/octet-stream
|
|
||||||
// regardless of what mime-type the event claimed. Even if the payload itself
|
|
||||||
// is some malicious HTML, the fact we instantiate it with a media mimetype or
|
|
||||||
// application/octet-stream means the browser doesn't try to render it as such.
|
|
||||||
//
|
|
||||||
// One interesting edge case is image/svg+xml, which empirically *is* rendered
|
|
||||||
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
|
|
||||||
// *even if the mimetype is application/octet-stream*. However, empirically JS
|
|
||||||
// in the SVG isn't executed in this scenario, so we seem to be okay.
|
|
||||||
//
|
|
||||||
// Tested on Chrome 65 and Firefox 60
|
|
||||||
//
|
|
||||||
// The list below is taken mainly from
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
|
||||||
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
|
|
||||||
// events, so we pick the ones which HTML5 browsers should be able to display
|
|
||||||
//
|
|
||||||
// For the record, mime-types which must NEVER enter this list below include:
|
|
||||||
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
|
||||||
|
|
||||||
const ALLOWED_BLOB_MIMETYPES = [
|
|
||||||
'image/jpeg',
|
|
||||||
'image/gif',
|
|
||||||
'image/png',
|
|
||||||
|
|
||||||
'video/mp4',
|
|
||||||
'video/webm',
|
|
||||||
'video/ogg',
|
|
||||||
|
|
||||||
'audio/mp4',
|
|
||||||
'audio/webm',
|
|
||||||
'audio/aac',
|
|
||||||
'audio/mpeg',
|
|
||||||
'audio/ogg',
|
|
||||||
'audio/wave',
|
|
||||||
'audio/wav',
|
|
||||||
'audio/x-wav',
|
|
||||||
'audio/x-pn-wav',
|
|
||||||
'audio/flac',
|
|
||||||
'audio/x-flac',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decrypt a file attached to a matrix event.
|
* Decrypt a file attached to a matrix event.
|
||||||
|
@ -100,9 +45,7 @@ export function decryptFile(file: IEncryptedFile): Promise<Blob> {
|
||||||
// browser (e.g. by copying the URI into a new tab or window.)
|
// browser (e.g. by copying the URI into a new tab or window.)
|
||||||
// See warning at top of file.
|
// See warning at top of file.
|
||||||
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
|
let mimetype = file.mimetype ? file.mimetype.split(";")[0].trim() : '';
|
||||||
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
mimetype = getBlobSafeMimeType(mimetype);
|
||||||
mimetype = 'application/octet-stream';
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Blob([dataArray], {type: mimetype});
|
return new Blob([dataArray], {type: mimetype});
|
||||||
});
|
});
|
||||||
|
|
78
src/utils/blobs.ts
Normal file
78
src/utils/blobs.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// WARNING: We have to be very careful about what mime-types we allow into blobs,
|
||||||
|
// as for performance reasons these are now rendered via URL.createObjectURL()
|
||||||
|
// rather than by converting into data: URIs.
|
||||||
|
//
|
||||||
|
// This means that the content is rendered using the origin of the script which
|
||||||
|
// called createObjectURL(), and so if the content contains any scripting then it
|
||||||
|
// will pose a XSS vulnerability when the browser renders it. This is particularly
|
||||||
|
// bad if the user right-clicks the URI and pastes it into a new window or tab,
|
||||||
|
// as the blob will then execute with access to Element's full JS environment(!)
|
||||||
|
//
|
||||||
|
// See https://github.com/matrix-org/matrix-react-sdk/pull/1820#issuecomment-385210647
|
||||||
|
// for details.
|
||||||
|
//
|
||||||
|
// We mitigate this by only allowing mime-types into blobs which we know don't
|
||||||
|
// contain any scripting, and instantiate all others as application/octet-stream
|
||||||
|
// regardless of what mime-type the event claimed. Even if the payload itself
|
||||||
|
// is some malicious HTML, the fact we instantiate it with a media mimetype or
|
||||||
|
// application/octet-stream means the browser doesn't try to render it as such.
|
||||||
|
//
|
||||||
|
// One interesting edge case is image/svg+xml, which empirically *is* rendered
|
||||||
|
// correctly if the blob is set to the src attribute of an img tag (for thumbnails)
|
||||||
|
// *even if the mimetype is application/octet-stream*. However, empirically JS
|
||||||
|
// in the SVG isn't executed in this scenario, so we seem to be okay.
|
||||||
|
//
|
||||||
|
// Tested on Chrome 65 and Firefox 60
|
||||||
|
//
|
||||||
|
// The list below is taken mainly from
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
|
||||||
|
// N.B. Matrix doesn't currently specify which mimetypes are valid in given
|
||||||
|
// events, so we pick the ones which HTML5 browsers should be able to display
|
||||||
|
//
|
||||||
|
// For the record, mime-types which must NEVER enter this list below include:
|
||||||
|
// text/html, text/xhtml, image/svg, image/svg+xml, image/pdf, and similar.
|
||||||
|
|
||||||
|
const ALLOWED_BLOB_MIMETYPES = [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/gif',
|
||||||
|
'image/png',
|
||||||
|
|
||||||
|
'video/mp4',
|
||||||
|
'video/webm',
|
||||||
|
'video/ogg',
|
||||||
|
|
||||||
|
'audio/mp4',
|
||||||
|
'audio/webm',
|
||||||
|
'audio/aac',
|
||||||
|
'audio/mpeg',
|
||||||
|
'audio/ogg',
|
||||||
|
'audio/wave',
|
||||||
|
'audio/wav',
|
||||||
|
'audio/x-wav',
|
||||||
|
'audio/x-pn-wav',
|
||||||
|
'audio/flac',
|
||||||
|
'audio/x-flac',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getBlobSafeMimeType(mimetype: string): string {
|
||||||
|
if (!ALLOWED_BLOB_MIMETYPES.includes(mimetype)) {
|
||||||
|
return 'application/octet-stream';
|
||||||
|
}
|
||||||
|
return mimetype;
|
||||||
|
}
|
|
@ -83,6 +83,7 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
|
||||||
if (shouldCreate) {
|
if (shouldCreate) {
|
||||||
await createRoom(opts);
|
await createRoom(opts);
|
||||||
}
|
}
|
||||||
|
return shouldCreate;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const showSpaceInvite = (space: Room, initialText = "") => {
|
export const showSpaceInvite = (space: Room, initialText = "") => {
|
||||||
|
|
|
@ -29,7 +29,7 @@ export enum PlaybackState {
|
||||||
Playing = "playing", // active progress through timeline
|
Playing = "playing", // active progress through timeline
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PLAYBACK_WAVEFORM_SAMPLES = 35;
|
export const PLAYBACK_WAVEFORM_SAMPLES = 39;
|
||||||
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
|
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
|
||||||
|
|
||||||
export class Playback extends EventEmitter implements IDestroyable {
|
export class Playback extends EventEmitter implements IDestroyable {
|
||||||
|
|
|
@ -33,6 +33,8 @@ const BITRATE = 24000; // 24kbps is pretty high quality for our use case in opus
|
||||||
const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files.
|
const TARGET_MAX_LENGTH = 120; // 2 minutes in seconds. Somewhat arbitrary, though longer == larger files.
|
||||||
const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary.
|
const TARGET_WARN_TIME_LEFT = 10; // 10 seconds, also somewhat arbitrary.
|
||||||
|
|
||||||
|
export const RECORDING_PLAYBACK_SAMPLES = 44;
|
||||||
|
|
||||||
export interface IRecordingUpdate {
|
export interface IRecordingUpdate {
|
||||||
waveform: number[]; // floating points between 0 (low) and 1 (high).
|
waveform: number[]; // floating points between 0 (low) and 1 (high).
|
||||||
timeSeconds: number; // float
|
timeSeconds: number; // float
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import './skinned-sdk';
|
import './skinned-sdk';
|
||||||
|
|
||||||
import CallHandler, { PlaceCallType } from '../src/CallHandler';
|
import CallHandler, { PlaceCallType, CallHandlerEvent } from '../src/CallHandler';
|
||||||
import { stubClient, mkStubRoom } from './test-utils';
|
import { stubClient, mkStubRoom } from './test-utils';
|
||||||
import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
||||||
import dis from '../src/dispatcher/dispatcher';
|
import dis from '../src/dispatcher/dispatcher';
|
||||||
|
@ -172,11 +172,9 @@ describe('CallHandler', () => {
|
||||||
|
|
||||||
let callRoomChangeEventCount = 0;
|
let callRoomChangeEventCount = 0;
|
||||||
const roomChangePromise = new Promise<void>(resolve => {
|
const roomChangePromise = new Promise<void>(resolve => {
|
||||||
dispatchHandle = dis.register(payload => {
|
callHandler.addListener(CallHandlerEvent.CallChangeRoom, () => {
|
||||||
if (payload.action === Action.CallChangeRoom) {
|
|
||||||
++callRoomChangeEventCount;
|
++callRoomChangeEventCount;
|
||||||
resolve();
|
resolve();
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -201,7 +199,7 @@ describe('CallHandler', () => {
|
||||||
fakeCall.emit(CallEvent.AssertedIdentityChanged);
|
fakeCall.emit(CallEvent.AssertedIdentityChanged);
|
||||||
|
|
||||||
await roomChangePromise;
|
await roomChangePromise;
|
||||||
dis.unregister(dispatchHandle);
|
callHandler.removeAllListeners();
|
||||||
|
|
||||||
// If everything's gone well, we should have seen only one room change
|
// If everything's gone well, we should have seen only one room change
|
||||||
// event and the call should now be in user 3's room.
|
// event and the call should now be in user 3's room.
|
||||||
|
|
|
@ -77,7 +77,7 @@ describe('MessagePanel', function() {
|
||||||
DMRoomMap.makeShared();
|
DMRoomMap.makeShared();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function() {
|
afterEach(function () {
|
||||||
clock.uninstall();
|
clock.uninstall();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -88,7 +88,21 @@ describe('MessagePanel', function() {
|
||||||
events.push(test_utils.mkMessage(
|
events.push(test_utils.mkMessage(
|
||||||
{
|
{
|
||||||
event: true, room: "!room:id", user: "@user:id",
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
ts: ts0 + i*1000,
|
ts: ts0 + i * 1000,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return events;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Just to avoid breaking Dateseparator tests that might run at 00hrs
|
||||||
|
function mkOneDayEvents() {
|
||||||
|
const events = [];
|
||||||
|
const ts0 = Date.parse('09 May 2004 00:12:00 GMT');
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
events.push(test_utils.mkMessage(
|
||||||
|
{
|
||||||
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
|
ts: ts0 + i * 1000,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
return events;
|
return events;
|
||||||
|
@ -104,7 +118,7 @@ describe('MessagePanel', function() {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
events.push(test_utils.mkMessage({
|
events.push(test_utils.mkMessage({
|
||||||
event: true, room: "!room:id", user: "@user:id",
|
event: true, room: "!room:id", user: "@user:id",
|
||||||
ts: ts0 + ++i*1000,
|
ts: ts0 + ++i * 1000,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
for (i = 0; i < 10; i++) {
|
for (i = 0; i < 10; i++) {
|
||||||
|
@ -151,7 +165,7 @@ describe('MessagePanel', function() {
|
||||||
},
|
},
|
||||||
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
getMxcAvatarUrl: () => 'mxc://avatar.url/image.png',
|
||||||
},
|
},
|
||||||
ts: ts0 + i*1000,
|
ts: ts0 + i * 1000,
|
||||||
mship: 'join',
|
mship: 'join',
|
||||||
prevMship: 'join',
|
prevMship: 'join',
|
||||||
name: 'A user',
|
name: 'A user',
|
||||||
|
@ -250,7 +264,6 @@ describe('MessagePanel', function() {
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isReadMarkerVisible(rmContainer) {
|
function isReadMarkerVisible(rmContainer) {
|
||||||
return rmContainer && rmContainer.children.length > 0;
|
return rmContainer && rmContainer.children.length > 0;
|
||||||
}
|
}
|
||||||
|
@ -437,4 +450,17 @@ describe('MessagePanel', function() {
|
||||||
// read marker should be hidden given props and at the last event
|
// read marker should be hidden given props and at the last event
|
||||||
expect(isReadMarkerVisible(rm)).toBeFalsy();
|
expect(isReadMarkerVisible(rm)).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should render Date separators for the events', function () {
|
||||||
|
const events = mkOneDayEvents();
|
||||||
|
const res = mount(
|
||||||
|
<WrappedMessagePanel
|
||||||
|
className="cls"
|
||||||
|
events={events}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const Dates = res.find(sdk.getComponent('messages.DateSeparator'));
|
||||||
|
|
||||||
|
expect(Dates.length).toEqual(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -435,9 +435,9 @@ jsprim@^1.2.2:
|
||||||
verror "1.10.0"
|
verror "1.10.0"
|
||||||
|
|
||||||
lodash@^4.15.0, lodash@^4.17.11:
|
lodash@^4.15.0, lodash@^4.17.11:
|
||||||
version "4.17.19"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
mime-db@~1.38.0:
|
mime-db@~1.38.0:
|
||||||
version "1.38.0"
|
version "1.38.0"
|
||||||
|
|
|
@ -101,6 +101,7 @@ const invite1 = "!invite1:server";
|
||||||
const invite2 = "!invite2:server";
|
const invite2 = "!invite2:server";
|
||||||
const room1 = "!room1:server";
|
const room1 = "!room1:server";
|
||||||
const room2 = "!room2:server";
|
const room2 = "!room2:server";
|
||||||
|
const room3 = "!room3:server";
|
||||||
const space1 = "!space1:server";
|
const space1 = "!space1:server";
|
||||||
const space2 = "!space2:server";
|
const space2 = "!space2:server";
|
||||||
const space3 = "!space3:server";
|
const space3 = "!space3:server";
|
||||||
|
@ -361,8 +362,8 @@ describe("SpaceStore", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy();
|
expect(store.getSpaceFilteredRoomIds(null).has(invite2)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("home space does not contain rooms/low priority from rooms within spaces", () => {
|
it("home space does contain rooms/low priority even if they are also shown in a space", () => {
|
||||||
expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeFalsy();
|
expect(store.getSpaceFilteredRoomIds(null).has(room1)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("space contains child rooms", () => {
|
it("space contains child rooms", () => {
|
||||||
|
@ -614,8 +615,8 @@ describe("SpaceStore", () => {
|
||||||
|
|
||||||
describe("space auto switching tests", () => {
|
describe("space auto switching tests", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
[room1, room2, orphan1].forEach(mkRoom);
|
[room1, room2, room3, orphan1].forEach(mkRoom);
|
||||||
mkSpace(space1, [room1, room2]);
|
mkSpace(space1, [room1, room2, room3]);
|
||||||
mkSpace(space2, [room1, room2]);
|
mkSpace(space2, [room1, room2]);
|
||||||
|
|
||||||
client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([
|
client.getRoom(room2).currentState.getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||||
|
@ -641,15 +642,15 @@ describe("SpaceStore", () => {
|
||||||
|
|
||||||
it("switch to canonical parent space for room", async () => {
|
it("switch to canonical parent space for room", async () => {
|
||||||
viewRoom(room1);
|
viewRoom(room1);
|
||||||
await store.setActiveSpace(null, false);
|
await store.setActiveSpace(client.getRoom(space2), false);
|
||||||
viewRoom(room2);
|
viewRoom(room2);
|
||||||
expect(store.activeSpace).toBe(client.getRoom(space2));
|
expect(store.activeSpace).toBe(client.getRoom(space2));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("switch to first containing space for room", async () => {
|
it("switch to first containing space for room", async () => {
|
||||||
viewRoom(room2);
|
viewRoom(room2);
|
||||||
await store.setActiveSpace(null, false);
|
await store.setActiveSpace(client.getRoom(space2), false);
|
||||||
viewRoom(room1);
|
viewRoom(room3);
|
||||||
expect(store.activeSpace).toBe(client.getRoom(space1));
|
expect(store.activeSpace).toBe(client.getRoom(space1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -659,6 +660,13 @@ describe("SpaceStore", () => {
|
||||||
viewRoom(orphan1);
|
viewRoom(orphan1);
|
||||||
expect(store.activeSpace).toBeNull();
|
expect(store.activeSpace).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("when switching rooms in the all rooms home space don't switch to related space", async () => {
|
||||||
|
viewRoom(room2);
|
||||||
|
await store.setActiveSpace(null, false);
|
||||||
|
viewRoom(room1);
|
||||||
|
expect(store.activeSpace).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("traverseSpace", () => {
|
describe("traverseSpace", () => {
|
||||||
|
|
48
yarn.lock
48
yarn.lock
|
@ -1594,6 +1594,11 @@
|
||||||
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
|
||||||
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==
|
||||||
|
|
||||||
|
"@types/parse5@^6.0.0":
|
||||||
|
version "6.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.0.tgz#38590dc2c3cf5717154064e3ee9b6947ee21b299"
|
||||||
|
integrity sha512-oPwPSj4a1wu9rsXTEGIJz91ISU725t0BmSnUhb57sI+M8XEmvUop84lzuiYdq0Y5M6xLY8DBPg0C2xEQKLyvBA==
|
||||||
|
|
||||||
"@types/prettier@^2.0.0":
|
"@types/prettier@^2.0.0":
|
||||||
version "2.1.6"
|
version "2.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.6.tgz#f4b1efa784e8db479cdb8b14403e2144b1e9ff03"
|
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.1.6.tgz#f4b1efa784e8db479cdb8b14403e2144b1e9ff03"
|
||||||
|
@ -4221,9 +4226,9 @@ hoist-non-react-statics@^3.3.0:
|
||||||
react-is "^16.7.0"
|
react-is "^16.7.0"
|
||||||
|
|
||||||
hosted-git-info@^2.1.4:
|
hosted-git-info@^2.1.4:
|
||||||
version "2.8.8"
|
version "2.8.9"
|
||||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
|
||||||
integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==
|
integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==
|
||||||
|
|
||||||
hosted-git-info@^3.0.6:
|
hosted-git-info@^3.0.6:
|
||||||
version "3.0.7"
|
version "3.0.7"
|
||||||
|
@ -5580,9 +5585,9 @@ lodash.sortby@^4.7.0:
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1:
|
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.2.1:
|
||||||
version "4.17.20"
|
version "4.17.21"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||||
|
|
||||||
log-symbols@^4.0.0:
|
log-symbols@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
|
@ -5672,8 +5677,8 @@ mathml-tag-names@^2.1.3:
|
||||||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
||||||
version "10.0.0"
|
version "10.1.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/c8f69c0b7937b9064938c134d708c4d064b71315"
|
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2d73805ca3d8c5a140fe05e574f826696de1656a"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
|
@ -6302,6 +6307,11 @@ parse-json@^5.0.0:
|
||||||
json-parse-even-better-errors "^2.3.0"
|
json-parse-even-better-errors "^2.3.0"
|
||||||
lines-and-columns "^1.1.6"
|
lines-and-columns "^1.1.6"
|
||||||
|
|
||||||
|
parse-srcset@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
||||||
|
integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=
|
||||||
|
|
||||||
parse5-htmlparser2-tree-adapter@^6.0.0:
|
parse5-htmlparser2-tree-adapter@^6.0.0:
|
||||||
version "6.0.1"
|
version "6.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
|
||||||
|
@ -7243,17 +7253,18 @@ sane@^4.0.3:
|
||||||
minimist "^1.1.1"
|
minimist "^1.1.1"
|
||||||
walker "~1.0.5"
|
walker "~1.0.5"
|
||||||
|
|
||||||
"sanitize-html@github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db":
|
sanitize-html@^2.3.2:
|
||||||
version "2.0.0-rc.3"
|
version "2.3.3"
|
||||||
resolved "https://codeload.github.com/apostrophecms/sanitize-html/tar.gz/3c7f93f2058f696f5359e3e58d464161647226db"
|
resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.3.3.tgz#3db382c9a621cce4c46d90f10c64f1e9da9e8353"
|
||||||
|
integrity sha512-DCFXPt7Di0c6JUnlT90eIgrjs6TsJl/8HYU3KLdmrVclFN4O0heTcVbJiMa23OKVr6aR051XYtsgd8EWwEBwUA==
|
||||||
dependencies:
|
dependencies:
|
||||||
deepmerge "^4.2.2"
|
deepmerge "^4.2.2"
|
||||||
escape-string-regexp "^4.0.0"
|
escape-string-regexp "^4.0.0"
|
||||||
htmlparser2 "^4.1.0"
|
htmlparser2 "^6.0.0"
|
||||||
is-plain-object "^5.0.0"
|
is-plain-object "^5.0.0"
|
||||||
klona "^2.0.3"
|
klona "^2.0.3"
|
||||||
|
parse-srcset "^1.0.2"
|
||||||
postcss "^8.0.2"
|
postcss "^8.0.2"
|
||||||
srcset "^3.0.0"
|
|
||||||
|
|
||||||
saxes@^5.0.0:
|
saxes@^5.0.0:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
|
@ -7510,11 +7521,6 @@ sprintf-js@~1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
|
||||||
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=
|
||||||
|
|
||||||
srcset@^3.0.0:
|
|
||||||
version "3.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/srcset/-/srcset-3.0.0.tgz#8afd8b971362dfc129ae9c1a99b3897301ce6441"
|
|
||||||
integrity sha512-D59vF08Qzu/C4GAOXVgMTLfgryt5fyWo93FZyhEWANo0PokFz/iWdDe13mX3O5TRf6l8vMTqckAfR4zPiaH0yQ==
|
|
||||||
|
|
||||||
sshpk@^1.7.0:
|
sshpk@^1.7.0:
|
||||||
version "1.16.1"
|
version "1.16.1"
|
||||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877"
|
||||||
|
@ -8070,9 +8076,9 @@ typescript@^4.1.3:
|
||||||
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
integrity sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==
|
||||||
|
|
||||||
ua-parser-js@^0.7.18:
|
ua-parser-js@^0.7.18:
|
||||||
version "0.7.23"
|
version "0.7.28"
|
||||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.23.tgz#704d67f951e13195fbcd3d78818577f5bc1d547b"
|
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
|
||||||
integrity sha512-m4hvMLxgGHXG3O3fQVAyyAQpZzDOvwnhOTjYz5Xmr7r/+LpkNy3vJXdVRWgd1TkAb7NGROZuSy96CrlNVjA7KA==
|
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
|
||||||
|
|
||||||
unhomoglyph@^1.0.6:
|
unhomoglyph@^1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
|
|
Loading…
Reference in a new issue