Merge branch 'develop' into travis/voice-messages/waveform

This commit is contained in:
Travis Ralston 2021-03-29 22:59:51 -06:00
commit 54412878a1
77 changed files with 2325 additions and 978 deletions

View file

@ -1,3 +1,96 @@
Changes in [3.17.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.17.0) (2021-03-29)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.17.0-rc.1...v3.17.0)
* Upgrade to JS SDK 9.10.0
* [Release] Tweak cross-signing copy
[\#5808](https://github.com/matrix-org/matrix-react-sdk/pull/5808)
* [Release] Fix crash on login when using social login
[\#5809](https://github.com/matrix-org/matrix-react-sdk/pull/5809)
* [Release] Fix edge case with redaction grouper messing up continuations
[\#5799](https://github.com/matrix-org/matrix-react-sdk/pull/5799)
Changes in [3.17.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.17.0-rc.1) (2021-03-25)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0...v3.17.0-rc.1)
* Upgrade to JS SDK 9.10.0-rc.1
* Translations update from Weblate
[\#5788](https://github.com/matrix-org/matrix-react-sdk/pull/5788)
* Track next event [tile] over group boundaries
[\#5784](https://github.com/matrix-org/matrix-react-sdk/pull/5784)
* Fixing the minor UI issues in the email discovery
[\#5780](https://github.com/matrix-org/matrix-react-sdk/pull/5780)
* Don't overwrite callback with undefined if no customization provided
[\#5783](https://github.com/matrix-org/matrix-react-sdk/pull/5783)
* Fix redaction event list summaries breaking sender profiles
[\#5781](https://github.com/matrix-org/matrix-react-sdk/pull/5781)
* Fix CIDER formatting buttons on Safari
[\#5782](https://github.com/matrix-org/matrix-react-sdk/pull/5782)
* Improve discovery of rooms in a space
[\#5776](https://github.com/matrix-org/matrix-react-sdk/pull/5776)
* Spaces improve creation journeys
[\#5777](https://github.com/matrix-org/matrix-react-sdk/pull/5777)
* Make buttons in verify dialog respect the system font
[\#5778](https://github.com/matrix-org/matrix-react-sdk/pull/5778)
* Collapse redactions into an event list summary
[\#5728](https://github.com/matrix-org/matrix-react-sdk/pull/5728)
* Added invite option to room's context menu
[\#5648](https://github.com/matrix-org/matrix-react-sdk/pull/5648)
* Add an optional config option to make the welcome page the login page
[\#5658](https://github.com/matrix-org/matrix-react-sdk/pull/5658)
* Fix username showing instead of display name in Jitsi widgets
[\#5770](https://github.com/matrix-org/matrix-react-sdk/pull/5770)
* Convert a bunch more js-sdk imports to absolute paths
[\#5774](https://github.com/matrix-org/matrix-react-sdk/pull/5774)
* Remove forgotten rooms from the room list once forgotten
[\#5775](https://github.com/matrix-org/matrix-react-sdk/pull/5775)
* Log error when failing to list usermedia devices
[\#5771](https://github.com/matrix-org/matrix-react-sdk/pull/5771)
* Fix weird timeline jumps
[\#5772](https://github.com/matrix-org/matrix-react-sdk/pull/5772)
* Replace type declaration in Registration.tsx
[\#5773](https://github.com/matrix-org/matrix-react-sdk/pull/5773)
* Add possibility to delay rageshake persistence in app startup
[\#5767](https://github.com/matrix-org/matrix-react-sdk/pull/5767)
* Fix left panel resizing and lower min-width improving flexibility
[\#5764](https://github.com/matrix-org/matrix-react-sdk/pull/5764)
* Work around more cases where a rageshake server might not be present
[\#5766](https://github.com/matrix-org/matrix-react-sdk/pull/5766)
* Iterate space panel visually and functionally
[\#5761](https://github.com/matrix-org/matrix-react-sdk/pull/5761)
* Make some dispatches async
[\#5765](https://github.com/matrix-org/matrix-react-sdk/pull/5765)
* fix: make room directory correct when using a homeserver with explicit port
[\#5762](https://github.com/matrix-org/matrix-react-sdk/pull/5762)
* Hangup all calls on logout
[\#5756](https://github.com/matrix-org/matrix-react-sdk/pull/5756)
* Remove now-unused assets and CSS from CompleteSecurity step
[\#5757](https://github.com/matrix-org/matrix-react-sdk/pull/5757)
* Add details and summary to allowed HTML tags
[\#5760](https://github.com/matrix-org/matrix-react-sdk/pull/5760)
* Support a media handling customisation endpoint
[\#5714](https://github.com/matrix-org/matrix-react-sdk/pull/5714)
* Edit button on View Source dialog that takes you to devtools ->
SendCustomEvent
[\#5718](https://github.com/matrix-org/matrix-react-sdk/pull/5718)
* Show room alias in plain/formatted body
[\#5748](https://github.com/matrix-org/matrix-react-sdk/pull/5748)
* Allow pills on the beginning of a part string
[\#5754](https://github.com/matrix-org/matrix-react-sdk/pull/5754)
* [SK-3] Decorate easy components with replaceableComponent
[\#5734](https://github.com/matrix-org/matrix-react-sdk/pull/5734)
* Use fsync in reskindex to ensure file is written to disk
[\#5753](https://github.com/matrix-org/matrix-react-sdk/pull/5753)
* Remove unused common CSS classes
[\#5752](https://github.com/matrix-org/matrix-react-sdk/pull/5752)
* Rebuild space previews with new designs
[\#5751](https://github.com/matrix-org/matrix-react-sdk/pull/5751)
* Rework cross-signing login flow
[\#5727](https://github.com/matrix-org/matrix-react-sdk/pull/5727)
* Change read receipt drift to be non-fractional
[\#5745](https://github.com/matrix-org/matrix-react-sdk/pull/5745)
Changes in [3.16.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0) (2021-03-15) Changes in [3.16.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.16.0) (2021-03-15)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.2...v3.16.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.16.0-rc.2...v3.16.0)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.16.0", "version": "3.17.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {

View file

@ -117,6 +117,7 @@
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_ErrorBoundary.scss";
@import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_EventListSummary.scss";
@import "./views/elements/_FacePile.scss";
@import "./views/elements/_Field.scss"; @import "./views/elements/_Field.scss";
@import "./views/elements/_FormButton.scss"; @import "./views/elements/_FormButton.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";

View file

@ -130,6 +130,10 @@ $roomListCollapsedWidth: 68px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
background: $secondary-fg-color; background: $secondary-fg-color;
} }
&.mx_LeftPanel_exploreButton_space::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
}
} }
} }

View file

@ -22,7 +22,7 @@ limitations under the License.
// keep border thickness consistent to prevent movement // keep border thickness consistent to prevent movement
border: 1px solid transparent; border: 1px solid transparent;
height: 28px; height: 28px;
padding: 2px; padding: 1px;
// Create a flexbox for the icons (easier to manage) // Create a flexbox for the icons (easier to manage)
display: flex; display: flex;

View file

@ -146,9 +146,6 @@ $activeBorderColor: $secondary-fg-color;
.mx_SpaceButton_toggleCollapse { .mx_SpaceButton_toggleCollapse {
width: $gutterSize; width: $gutterSize;
// negative margin to place it correctly even with the complex
// 4px selection border each space button has when active
margin-right: -4px;
height: 20px; height: 20px;
mask-position: center; mask-position: center;
mask-size: 20px; mask-size: 20px;
@ -333,20 +330,20 @@ $activeBorderColor: $secondary-fg-color;
mask-image: url('$(res)/img/element-icons/leave.svg'); mask-image: url('$(res)/img/element-icons/leave.svg');
} }
.mx_SpacePanel_iconHome::before {
mask-image: url('$(res)/img/element-icons/roomlist/home.svg');
}
.mx_SpacePanel_iconMembers::before { .mx_SpacePanel_iconMembers::before {
mask-image: url('$(res)/img/element-icons/room/members.svg'); mask-image: url('$(res)/img/element-icons/room/members.svg');
} }
.mx_SpacePanel_iconPlus::before { .mx_SpacePanel_iconPlus::before {
mask-image: url('$(res)/img/element-icons/plus.svg'); mask-image: url('$(res)/img/element-icons/roomlist/plus-circle.svg');
}
.mx_SpacePanel_iconHash::before {
mask-image: url('$(res)/img/element-icons/roomlist/hash-circle.svg');
} }
.mx_SpacePanel_iconExplore::before { .mx_SpacePanel_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
} }
} }

View file

@ -182,7 +182,7 @@ limitations under the License.
.mx_SpaceRoomDirectory_roomTile { .mx_SpaceRoomDirectory_roomTile {
position: relative; position: relative;
padding: 6px 16px; padding: 8px 16px;
border-radius: 8px; border-radius: 8px;
min-height: 56px; min-height: 56px;
box-sizing: border-box; box-sizing: border-box;
@ -190,6 +190,7 @@ limitations under the License.
display: grid; display: grid;
grid-template-columns: 20px auto max-content; grid-template-columns: 20px auto max-content;
grid-column-gap: 8px; grid-column-gap: 8px;
grid-row-gap: 6px;
align-items: center; align-items: center;
.mx_BaseAvatar { .mx_BaseAvatar {
@ -213,16 +214,28 @@ limitations under the License.
.mx_InfoTooltip_icon { .mx_InfoTooltip_icon {
margin-right: 4px; margin-right: 4px;
position: relative;
vertical-align: text-top;
&::before {
position: absolute;
top: 0;
left: 0;
}
} }
} }
} }
.mx_SpaceRoomDirectory_roomTile_info { .mx_SpaceRoomDirectory_roomTile_info {
font-size: $font-12px; font-size: $font-14px;
line-height: $font-15px; line-height: $font-18px;
color: $tertiary-fg-color; color: $secondary-fg-color;
grid-row: 2; grid-row: 2;
grid-column: 1/3; grid-column: 1/3;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
} }
.mx_SpaceRoomDirectory_actions { .mx_SpaceRoomDirectory_actions {
@ -232,9 +245,9 @@ limitations under the License.
grid-row: 1/3; grid-row: 1/3;
.mx_AccessibleButton { .mx_AccessibleButton {
padding: 6px 18px; padding: 8px 18px;
display: inline-block;
display: none; visibility: hidden;
} }
.mx_Checkbox { .mx_Checkbox {
@ -248,7 +261,7 @@ limitations under the License.
background-color: $groupFilterPanel-bg-color; background-color: $groupFilterPanel-bg-color;
.mx_AccessibleButton { .mx_AccessibleButton {
display: inline-block; visibility: visible;
} }
} }
} }

View file

@ -22,7 +22,7 @@ $SpaceRoomViewInnerWidth: 428px;
width: 432px; width: 432px;
box-sizing: border-box; box-sizing: border-box;
border-radius: 8px; border-radius: 8px;
border: 1px solid $input-darker-bg-color; border: 1px solid $input-border-color;
font-size: $font-15px; font-size: $font-15px;
margin: 20px 0; margin: 20px 0;
@ -89,7 +89,7 @@ $SpaceRoomViewInnerWidth: 428px;
width: $SpaceRoomViewInnerWidth; width: $SpaceRoomViewInnerWidth;
text-align: right; // button alignment right text-align: right; // button alignment right
.mx_FormButton { .mx_AccessibleButton_hasKind {
padding: 8px 22px; padding: 8px 22px;
margin-left: 16px; margin-left: 16px;
} }
@ -122,7 +122,6 @@ $SpaceRoomViewInnerWidth: 428px;
max-width: 480px; max-width: 480px;
box-sizing: border-box; box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color; box-shadow: 2px 15px 30px $dialog-shadow-color;
border: 1px solid $input-border-color;
border-radius: 8px; border-radius: 8px;
.mx_SpaceRoomView_preview_inviter { .mx_SpaceRoomView_preview_inviter {
@ -154,53 +153,6 @@ $SpaceRoomViewInnerWidth: 428px;
margin: 20px 0 !important; // override default margin from above margin: 20px 0 !important; // override default margin from above
} }
.mx_SpaceRoomView_preview_info {
color: $tertiary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_preview_info_public,
.mx_SpaceRoomView_preview_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_preview_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_preview_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}
.mx_SpaceRoomView_preview_topic { .mx_SpaceRoomView_preview_topic {
font-size: $font-14px; font-size: $font-14px;
line-height: $font-22px; line-height: $font-22px;
@ -254,36 +206,90 @@ $SpaceRoomViewInnerWidth: 428px;
vertical-align: middle; vertical-align: middle;
} }
} }
}
.mx_SpaceRoomView_landing_memberCount { .mx_SpaceRoomView_landing_info {
display: flex;
align-items: center;
.mx_SpaceRoomView_info {
display: inline-block;
margin: 0;
}
.mx_FacePile {
display: inline-block;
margin-left: auto;
margin-right: 12px;
.mx_FacePile_faces {
cursor: pointer;
> span:hover {
.mx_BaseAvatar {
filter: brightness(0.8);
}
}
> span:first-child {
position: relative;
.mx_BaseAvatar {
filter: brightness(0.8);
}
&::before {
content: "";
z-index: 1;
position: absolute;
top: 0;
left: 0;
height: 30px;
width: 30px;
background: #ffffff; // white icon fill
mask-position: center;
mask-size: 24px;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
}
}
}
}
.mx_SpaceRoomView_landing_inviteButton {
position: relative; position: relative;
margin-left: 24px; padding-left: 40px;
padding: 0 0 0 28px; height: min-content;
line-height: $font-24px;
vertical-align: text-bottom;
&::before { &::before {
position: absolute; position: absolute;
content: ''; content: "";
width: 24px; left: 8px;
height: 24px; height: 16px;
top: 0; width: 16px;
left: 0; background: #ffffff; // white icon fill
mask-position: center; mask-position: center;
mask-size: 16px;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: contain; mask-image: url('$(res)/img/element-icons/room/invite.svg');
background-color: $accent-color;
mask-image: url('$(res)/img/element-icons/community-members.svg');
} }
} }
} }
.mx_SpaceRoomView_landing_topic { .mx_SpaceRoomView_landing_topic {
font-size: $font-15px; font-size: $font-15px;
margin-top: 12px;
margin-bottom: 16px;
}
> hr {
border: none;
height: 1px;
background-color: $groupFilterPanel-bg-color;
} }
.mx_SpaceRoomView_landing_adminButtons { .mx_SpaceRoomView_landing_adminButtons {
margin-top: 32px; margin-top: 24px;
.mx_AccessibleButton { .mx_AccessibleButton {
position: relative; position: relative;
@ -292,9 +298,9 @@ $SpaceRoomViewInnerWidth: 428px;
box-sizing: border-box; box-sizing: border-box;
padding: 72px 16px 0; padding: 72px 16px 0;
border-radius: 12px; border-radius: 12px;
border: 1px solid $space-button-outline-color; border: 1px solid $input-border-color;
margin-right: 28px; margin-right: 28px;
margin-bottom: 28px; margin-bottom: 20px;
font-size: $font-14px; font-size: $font-14px;
display: inline-block; display: inline-block;
vertical-align: bottom; vertical-align: bottom;
@ -324,16 +330,6 @@ $SpaceRoomViewInnerWidth: 428px;
background: #ffffff; // white icon fill background: #ffffff; // white icon fill
} }
&.mx_SpaceRoomView_landing_inviteButton {
&::before {
background-color: $accent-color;
}
&::after {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
&.mx_SpaceRoomView_landing_addButton { &.mx_SpaceRoomView_landing_addButton {
&::before { &::before {
background-color: #ac3ba8; background-color: #ac3ba8;
@ -366,12 +362,8 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
.mx_SpaceRoomDirectory_list { .mx_SearchBox {
max-width: 600px; margin: 0 0 20px;
.mx_SpaceRoomDirectory_roomTile_actions {
display: none;
}
} }
} }
@ -424,3 +416,50 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
} }
.mx_SpaceRoomView_info {
color: $secondary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_info_public,
.mx_SpaceRoomView_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -158,6 +158,10 @@ limitations under the License.
} }
} }
.mx_Toast_detail {
color: $secondary-fg-color;
}
.mx_Toast_deviceID { .mx_Toast_deviceID {
font-size: $font-10px; font-size: $font-10px;
} }

View file

@ -14,14 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_ViewSource_label_left {
float: left;
}
.mx_ViewSource_label_right {
float: right;
}
.mx_ViewSource_separator { .mx_ViewSource_separator {
clear: both; clear: both;
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;

View file

@ -28,22 +28,23 @@ limitations under the License.
flex-direction: column; flex-direction: column;
flex-wrap: nowrap; flex-wrap: nowrap;
min-height: 0; min-height: 0;
height: 80vh;
.mx_Dialog_title { .mx_Dialog_title {
display: flex; display: flex;
.mx_BaseAvatar {
display: inline-flex;
margin: 5px 16px 5px 5px;
vertical-align: middle;
}
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
border-radius: 8px; border-radius: 8px;
margin: 0; margin: 0;
vertical-align: unset; vertical-align: unset;
} }
.mx_BaseAvatar {
display: inline-flex;
margin: 5px 16px 5px 5px;
vertical-align: middle;
}
> div { > div {
> h1 { > h1 {
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
@ -101,6 +102,7 @@ limitations under the License.
.mx_SearchBox { .mx_SearchBox {
margin: 0; margin: 0;
flex-grow: 0;
} }
.mx_AddExistingToSpaceDialog_errorText { .mx_AddExistingToSpaceDialog_errorText {
@ -112,7 +114,10 @@ limitations under the License.
} }
.mx_AddExistingToSpaceDialog_content { .mx_AddExistingToSpaceDialog_content {
flex-grow: 1;
.mx_AddExistingToSpaceDialog_noResults { .mx_AddExistingToSpaceDialog_noResults {
display: block;
margin-top: 24px; margin-top: 24px;
} }
} }
@ -162,8 +167,14 @@ limitations under the License.
> span { > span {
flex-grow: 1; flex-grow: 1;
font-size: $font-12px; font-size: $font-14px;
line-height: $font-15px; line-height: $font-15px;
font-weight: $font-semi-bold;
.mx_AccessibleButton {
font-size: inherit;
display: inline-block;
}
> * { > * {
vertical-align: middle; vertical-align: middle;

View file

@ -49,7 +49,7 @@ limitations under the License.
} }
} }
.mx_FormButton { .mx_AccessibleButton_hasKind {
padding: 8px 22px; padding: 8px 22px;
} }
} }

View file

@ -0,0 +1,42 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_FacePile {
.mx_FacePile_faces {
display: inline-flex;
flex-direction: row-reverse;
vertical-align: middle;
> span + span {
margin-right: -8px;
}
.mx_BaseAvatar_image {
border: 1px solid $primary-bg-color;
}
.mx_BaseAvatar_initial {
margin: 1px; // to offset the border on the image
}
}
> span {
margin-left: 12px;
font-size: $font-14px;
line-height: $font-24px;
color: $tertiary-fg-color;
}
}

View file

@ -33,8 +33,13 @@ limitations under the License.
.mx_AccessibleButton { .mx_AccessibleButton {
line-height: $font-24px; line-height: $font-24px;
display: inline-block;
&::before { & + .mx_AccessibleButton {
margin-left: 12px;
}
&:not(.mx_AccessibleButton_kind_primary_outline)::before {
content: ''; content: '';
display: inline-block; display: inline-block;
background-color: $button-fg-color; background-color: $button-fg-color;

View file

@ -27,6 +27,9 @@ limitations under the License.
.mx_RoomList_iconExplore::before { .mx_RoomList_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
} }
.mx_RoomList_iconBrowse::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
}
.mx_RoomList_iconDialpad::before { .mx_RoomList_iconDialpad::before {
mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg'); mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg');
} }
@ -34,29 +37,33 @@ limitations under the License.
.mx_RoomList_explorePrompt { .mx_RoomList_explorePrompt {
margin: 4px 12px 4px; margin: 4px 12px 4px;
padding-top: 12px; padding-top: 12px;
border-top: 1px solid $tertiary-fg-color; border-top: 1px solid $input-border-color;
font-size: $font-13px; font-size: $font-14px;
div:first-child { div:first-child {
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
line-height: $font-18px;
color: $primary-fg-color;
} }
.mx_AccessibleButton { .mx_AccessibleButton {
color: $secondary-fg-color; color: $primary-fg-color;
position: relative; position: relative;
padding: 0 0 0 24px; padding: 8px 8px 8px 32px;
font-size: inherit; font-size: inherit;
margin-top: 8px; margin-top: 12px;
display: block; display: block;
text-align: start; text-align: start;
background-color: $roomlist-button-bg-color;
border-radius: 4px;
&::before { &::before {
content: ''; content: '';
width: 16px; width: 16px;
height: 16px; height: 16px;
position: absolute; position: absolute;
top: 0; top: 8px;
left: 0; left: 8px;
background: $secondary-fg-color; background: $secondary-fg-color;
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
@ -70,5 +77,13 @@ limitations under the License.
&.mx_RoomList_explorePrompt_explore::before { &.mx_RoomList_explorePrompt_explore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
} }
&.mx_RoomList_explorePrompt_spaceInvite::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
&.mx_RoomList_explorePrompt_spaceExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/browse.svg');
}
} }
} }

View file

@ -79,7 +79,7 @@ $spacePanelWidth: 71px;
} }
} }
.mx_FormButton { .mx_AccessibleButton_kind_primary {
padding: 8px 22px; padding: 8px 22px;
margin-left: auto; margin-left: auto;
display: block; display: block;

View file

@ -0,0 +1,4 @@
<svg width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00262 5.60945C7.02444 6.31867 7.18204 6.99371 7.45029 7.60945H5.83106L5.49798 11.0235H8.60274L8.757 9.44233C9.29964 9.94168 9.94406 10.3321 10.6556 10.579L10.6122 11.0235H12.7966C13.3489 11.0235 13.7966 11.4712 13.7966 12.0235C13.7966 12.5758 13.3489 13.0235 12.7966 13.0235H10.4171L10.1823 15.4305C10.1287 15.9802 9.63959 16.3823 9.08991 16.3287C8.54024 16.2751 8.13811 15.786 8.19174 15.2363L8.40762 13.0235H5.30286L5.06803 15.4305C5.0144 15.9802 4.52533 16.3823 3.97565 16.3287C3.42598 16.2751 3.02385 15.786 3.07748 15.2363L3.29336 13.0235H1.6665C1.11422 13.0235 0.666504 12.5758 0.666504 12.0235C0.666504 11.4712 1.11422 11.0235 1.6665 11.0235H3.48848L3.82156 7.60945H2.26807C1.71578 7.60945 1.26807 7.16173 1.26807 6.60945C1.26807 6.05716 1.71578 5.60945 2.26807 5.60945H4.01668L4.28073 2.90297C4.33436 2.3533 4.82343 1.95117 5.37311 2.0048C5.92278 2.05842 6.32491 2.5475 6.27128 3.09717L6.02618 5.60945H7.00262Z" fill="#8D99A5"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4224 5.37843C14.4224 6.50754 13.5071 7.42287 12.3779 7.42287C11.2488 7.42287 10.3335 6.50754 10.3335 5.37843C10.3335 4.24931 11.2488 3.33398 12.3779 3.33398C13.5071 3.33398 14.4224 4.24931 14.4224 5.37843ZM15.8496 7.45454C16.2133 6.84764 16.4224 6.13745 16.4224 5.37843C16.4224 3.14474 14.6116 1.33398 12.3779 1.33398C10.1443 1.33398 8.3335 3.14474 8.3335 5.37843C8.3335 7.61211 10.1443 9.42287 12.3779 9.42287C13.1369 9.42287 13.8471 9.21381 14.454 8.85013C14.4853 8.89368 14.5205 8.93528 14.5597 8.97444L16.293 10.7078C16.6835 11.0983 17.3167 11.0983 17.7072 10.7078C18.0977 10.3172 18.0977 9.68408 17.7072 9.29356L15.9739 7.56023C15.9347 7.52107 15.8931 7.48584 15.8496 7.45454Z" fill="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -123,7 +123,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: rgba(141, 151, 165, 0.2);
$roomtile-preview-color: $secondary-fg-color; $roomtile-preview-color: $secondary-fg-color;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -120,7 +120,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: rgba(141, 151, 165, 0.2);
$roomtile-preview-color: #9e9e9e; $roomtile-preview-color: #9e9e9e;
$roomtile-default-badge-bg-color: #61708b; $roomtile-default-badge-bg-color: #61708b;

View file

@ -187,7 +187,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color; $voice-record-stop-symbol-color: $warning-color;

View file

@ -178,7 +178,6 @@ $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%); $roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
$space-button-outline-color: #E3E8F0;
$voice-record-stop-border-color: #E3E8F0; $voice-record-stop-border-color: #E3E8F0;
$voice-record-stop-symbol-color: $warning-color; $voice-record-stop-symbol-color: $warning-color;

View file

@ -212,6 +212,18 @@ export default abstract class BasePlatform {
throw new Error("Unimplemented"); throw new Error("Unimplemented");
} }
supportsWarnBeforeExit(): boolean {
return false;
}
async shouldWarnBeforeExit(): Promise<boolean> {
return false;
}
async setWarnBeforeExit(enabled: boolean): Promise<void> {
throw new Error("Unimplemented");
}
supportsAutoHideMenuBar(): boolean { supportsAutoHideMenuBar(): boolean {
return false; return false;
} }

407
src/KeyBindingsDefaults.ts Normal file
View file

@ -0,0 +1,407 @@
/*
Copyright 2021 Clemens Zeidler
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 { AutocompleteAction, IKeyBindingsProvider, KeyBinding, MessageComposerAction, NavigationAction, RoomAction,
RoomListAction } from "./KeyBindingsManager";
import { isMac, Key } from "./Keyboard";
import SettingsStore from "./settings/SettingsStore";
const messageComposerBindings = (): KeyBinding<MessageComposerAction>[] => {
const bindings: KeyBinding<MessageComposerAction>[] = [
{
action: MessageComposerAction.SelectPrevSendHistory,
keyCombo: {
key: Key.ARROW_UP,
altKey: true,
ctrlKey: true,
},
},
{
action: MessageComposerAction.SelectNextSendHistory,
keyCombo: {
key: Key.ARROW_DOWN,
altKey: true,
ctrlKey: true,
},
},
{
action: MessageComposerAction.EditPrevMessage,
keyCombo: {
key: Key.ARROW_UP,
},
},
{
action: MessageComposerAction.EditNextMessage,
keyCombo: {
key: Key.ARROW_DOWN,
},
},
{
action: MessageComposerAction.CancelEditing,
keyCombo: {
key: Key.ESCAPE,
},
},
{
action: MessageComposerAction.FormatBold,
keyCombo: {
key: Key.B,
ctrlOrCmd: true,
},
},
{
action: MessageComposerAction.FormatItalics,
keyCombo: {
key: Key.I,
ctrlOrCmd: true,
},
},
{
action: MessageComposerAction.FormatQuote,
keyCombo: {
key: Key.GREATER_THAN,
ctrlOrCmd: true,
shiftKey: true,
},
},
{
action: MessageComposerAction.EditUndo,
keyCombo: {
key: Key.Z,
ctrlOrCmd: true,
},
},
{
action: MessageComposerAction.MoveCursorToStart,
keyCombo: {
key: Key.HOME,
ctrlOrCmd: true,
},
},
{
action: MessageComposerAction.MoveCursorToEnd,
keyCombo: {
key: Key.END,
ctrlOrCmd: true,
},
},
];
if (isMac) {
bindings.push({
action: MessageComposerAction.EditRedo,
keyCombo: {
key: Key.Z,
ctrlOrCmd: true,
shiftKey: true,
},
});
} else {
bindings.push({
action: MessageComposerAction.EditRedo,
keyCombo: {
key: Key.Y,
ctrlOrCmd: true,
},
});
}
if (SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend')) {
bindings.push({
action: MessageComposerAction.Send,
keyCombo: {
key: Key.ENTER,
ctrlOrCmd: true,
},
});
bindings.push({
action: MessageComposerAction.NewLine,
keyCombo: {
key: Key.ENTER,
},
});
} else {
bindings.push({
action: MessageComposerAction.Send,
keyCombo: {
key: Key.ENTER,
},
});
bindings.push({
action: MessageComposerAction.NewLine,
keyCombo: {
key: Key.ENTER,
shiftKey: true,
},
});
if (isMac) {
bindings.push({
action: MessageComposerAction.NewLine,
keyCombo: {
key: Key.ENTER,
altKey: true,
},
});
}
}
return bindings;
}
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
return [
{
action: AutocompleteAction.ApplySelection,
keyCombo: {
key: Key.TAB,
},
},
{
action: AutocompleteAction.ApplySelection,
keyCombo: {
key: Key.TAB,
ctrlKey: true,
},
},
{
action: AutocompleteAction.ApplySelection,
keyCombo: {
key: Key.TAB,
shiftKey: true,
},
},
{
action: AutocompleteAction.ApplySelection,
keyCombo: {
key: Key.TAB,
ctrlKey: true,
shiftKey: true,
},
},
{
action: AutocompleteAction.Cancel,
keyCombo: {
key: Key.ESCAPE,
},
},
{
action: AutocompleteAction.PrevSelection,
keyCombo: {
key: Key.ARROW_UP,
},
},
{
action: AutocompleteAction.NextSelection,
keyCombo: {
key: Key.ARROW_DOWN,
},
},
];
}
const roomListBindings = (): KeyBinding<RoomListAction>[] => {
return [
{
action: RoomListAction.ClearSearch,
keyCombo: {
key: Key.ESCAPE,
},
},
{
action: RoomListAction.PrevRoom,
keyCombo: {
key: Key.ARROW_UP,
},
},
{
action: RoomListAction.NextRoom,
keyCombo: {
key: Key.ARROW_DOWN,
},
},
{
action: RoomListAction.SelectRoom,
keyCombo: {
key: Key.ENTER,
},
},
{
action: RoomListAction.CollapseSection,
keyCombo: {
key: Key.ARROW_LEFT,
},
},
{
action: RoomListAction.ExpandSection,
keyCombo: {
key: Key.ARROW_RIGHT,
},
},
];
}
const roomBindings = (): KeyBinding<RoomAction>[] => {
const bindings: KeyBinding<RoomAction>[] = [
{
action: RoomAction.ScrollUp,
keyCombo: {
key: Key.PAGE_UP,
},
},
{
action: RoomAction.RoomScrollDown,
keyCombo: {
key: Key.PAGE_DOWN,
},
},
{
action: RoomAction.DismissReadMarker,
keyCombo: {
key: Key.ESCAPE,
},
},
{
action: RoomAction.JumpToOldestUnread,
keyCombo: {
key: Key.PAGE_UP,
shiftKey: true,
},
},
{
action: RoomAction.UploadFile,
keyCombo: {
key: Key.U,
ctrlOrCmd: true,
shiftKey: true,
},
},
{
action: RoomAction.JumpToFirstMessage,
keyCombo: {
key: Key.HOME,
ctrlKey: true,
},
},
{
action: RoomAction.JumpToLatestMessage,
keyCombo: {
key: Key.END,
ctrlKey: true,
},
},
];
if (SettingsStore.getValue('ctrlFForSearch')) {
bindings.push({
action: RoomAction.FocusSearch,
keyCombo: {
key: Key.F,
ctrlOrCmd: true,
},
});
}
return bindings;
}
const navigationBindings = (): KeyBinding<NavigationAction>[] => {
return [
{
action: NavigationAction.FocusRoomSearch,
keyCombo: {
key: Key.K,
ctrlOrCmd: true,
},
},
{
action: NavigationAction.ToggleRoomSidePanel,
keyCombo: {
key: Key.PERIOD,
ctrlOrCmd: true,
},
},
{
action: NavigationAction.ToggleUserMenu,
// Ideally this would be CTRL+P for "Profile", but that's
// taken by the print dialog. CTRL+I for "Information"
// was previously chosen but conflicted with italics in
// composer, so CTRL+` it is
keyCombo: {
key: Key.BACKTICK,
ctrlOrCmd: true,
},
},
{
action: NavigationAction.ToggleShortCutDialog,
keyCombo: {
key: Key.SLASH,
ctrlOrCmd: true,
},
},
{
action: NavigationAction.ToggleShortCutDialog,
keyCombo: {
key: Key.SLASH,
ctrlOrCmd: true,
shiftKey: true,
},
},
{
action: NavigationAction.GoToHome,
keyCombo: {
key: Key.H,
ctrlOrCmd: true,
altKey: true,
},
},
{
action: NavigationAction.SelectPrevRoom,
keyCombo: {
key: Key.ARROW_UP,
altKey: true,
},
},
{
action: NavigationAction.SelectNextRoom,
keyCombo: {
key: Key.ARROW_DOWN,
altKey: true,
},
},
{
action: NavigationAction.SelectPrevUnreadRoom,
keyCombo: {
key: Key.ARROW_UP,
altKey: true,
shiftKey: true,
},
},
{
action: NavigationAction.SelectNextUnreadRoom,
keyCombo: {
key: Key.ARROW_DOWN,
altKey: true,
shiftKey: true,
},
},
];
}
export const defaultBindingsProvider: IKeyBindingsProvider = {
getMessageComposerBindings: messageComposerBindings,
getAutocompleteBindings: autocompleteBindings,
getRoomListBindings: roomListBindings,
getRoomBindings: roomBindings,
getNavigationBindings: navigationBindings,
}

266
src/KeyBindingsManager.ts Normal file
View file

@ -0,0 +1,266 @@
/*
Copyright 2021 Clemens Zeidler
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 { defaultBindingsProvider } from './KeyBindingsDefaults';
import { isMac } from './Keyboard';
/** Actions for the chat message composer component */
export enum MessageComposerAction {
/** Send a message */
Send = 'Send',
/** Go backwards through the send history and use the message in composer view */
SelectPrevSendHistory = 'SelectPrevSendHistory',
/** Go forwards through the send history */
SelectNextSendHistory = 'SelectNextSendHistory',
/** Start editing the user's last sent message */
EditPrevMessage = 'EditPrevMessage',
/** Start editing the user's next sent message */
EditNextMessage = 'EditNextMessage',
/** Cancel editing a message or cancel replying to a message */
CancelEditing = 'CancelEditing',
/** Set bold format the current selection */
FormatBold = 'FormatBold',
/** Set italics format the current selection */
FormatItalics = 'FormatItalics',
/** Format the current selection as quote */
FormatQuote = 'FormatQuote',
/** Undo the last editing */
EditUndo = 'EditUndo',
/** Redo editing */
EditRedo = 'EditRedo',
/** Insert new line */
NewLine = 'NewLine',
/** Move the cursor to the start of the message */
MoveCursorToStart = 'MoveCursorToStart',
/** Move the cursor to the end of the message */
MoveCursorToEnd = 'MoveCursorToEnd',
}
/** Actions for text editing autocompletion */
export enum AutocompleteAction {
/** Apply the current autocomplete selection */
ApplySelection = 'ApplySelection',
/** Cancel autocompletion */
Cancel = 'Cancel',
/** Move to the previous autocomplete selection */
PrevSelection = 'PrevSelection',
/** Move to the next autocomplete selection */
NextSelection = 'NextSelection',
}
/** Actions for the room list sidebar */
export enum RoomListAction {
/** Clear room list filter field */
ClearSearch = 'ClearSearch',
/** Navigate up/down in the room list */
PrevRoom = 'PrevRoom',
/** Navigate down in the room list */
NextRoom = 'NextRoom',
/** Select room from the room list */
SelectRoom = 'SelectRoom',
/** Collapse room list section */
CollapseSection = 'CollapseSection',
/** Expand room list section, if already expanded, jump to first room in the selection */
ExpandSection = 'ExpandSection',
}
/** Actions for the current room view */
export enum RoomAction {
/** Scroll up in the timeline */
ScrollUp = 'ScrollUp',
/** Scroll down in the timeline */
RoomScrollDown = 'RoomScrollDown',
/** Dismiss read marker and jump to bottom */
DismissReadMarker = 'DismissReadMarker',
/** Jump to oldest unread message */
JumpToOldestUnread = 'JumpToOldestUnread',
/** Upload a file */
UploadFile = 'UploadFile',
/** Focus search message in a room (must be enabled) */
FocusSearch = 'FocusSearch',
/** Jump to the first (downloaded) message in the room */
JumpToFirstMessage = 'JumpToFirstMessage',
/** Jump to the latest message in the room */
JumpToLatestMessage = 'JumpToLatestMessage',
}
/** Actions for navigating do various menus, dialogs or screens */
export enum NavigationAction {
/** Jump to room search (search for a room) */
FocusRoomSearch = 'FocusRoomSearch',
/** Toggle the room side panel */
ToggleRoomSidePanel = 'ToggleRoomSidePanel',
/** Toggle the user menu */
ToggleUserMenu = 'ToggleUserMenu',
/** Toggle the short cut help dialog */
ToggleShortCutDialog = 'ToggleShortCutDialog',
/** Got to the Element home screen */
GoToHome = 'GoToHome',
/** Select prev room */
SelectPrevRoom = 'SelectPrevRoom',
/** Select next room */
SelectNextRoom = 'SelectNextRoom',
/** Select prev room with unread messages */
SelectPrevUnreadRoom = 'SelectPrevUnreadRoom',
/** Select next room with unread messages */
SelectNextUnreadRoom = 'SelectNextUnreadRoom',
}
/**
* Represent a key combination.
*
* The combo is evaluated strictly, i.e. the KeyboardEvent must match exactly what is specified in the KeyCombo.
*/
export type KeyCombo = {
key?: string;
/** On PC: ctrl is pressed; on Mac: meta is pressed */
ctrlOrCmd?: boolean;
altKey?: boolean;
ctrlKey?: boolean;
metaKey?: boolean;
shiftKey?: boolean;
}
export type KeyBinding<T extends string> = {
action: T;
keyCombo: KeyCombo;
}
/**
* Helper method to check if a KeyboardEvent matches a KeyCombo
*
* Note, this method is only exported for testing.
*/
export function isKeyComboMatch(ev: KeyboardEvent | React.KeyboardEvent, combo: KeyCombo, onMac: boolean): boolean {
if (combo.key !== undefined) {
// When shift is pressed, letters are returned as upper case chars. In this case do a lower case comparison.
// This works for letter combos such as shift + U as well for none letter combos such as shift + Escape.
// If shift is not pressed, the toLowerCase conversion can be avoided.
if (ev.shiftKey) {
if (ev.key.toLowerCase() !== combo.key.toLowerCase()) {
return false;
}
} else if (ev.key !== combo.key) {
return false;
}
}
const comboCtrl = combo.ctrlKey ?? false;
const comboAlt = combo.altKey ?? false;
const comboShift = combo.shiftKey ?? false;
const comboMeta = combo.metaKey ?? false;
// Tests mock events may keep the modifiers undefined; convert them to booleans
const evCtrl = ev.ctrlKey ?? false;
const evAlt = ev.altKey ?? false;
const evShift = ev.shiftKey ?? false;
const evMeta = ev.metaKey ?? false;
// When ctrlOrCmd is set, the keys need do evaluated differently on PC and Mac
if (combo.ctrlOrCmd) {
if (onMac) {
if (!evMeta
|| evCtrl !== comboCtrl
|| evAlt !== comboAlt
|| evShift !== comboShift) {
return false;
}
} else {
if (!evCtrl
|| evMeta !== comboMeta
|| evAlt !== comboAlt
|| evShift !== comboShift) {
return false;
}
}
return true;
}
if (evMeta !== comboMeta
|| evCtrl !== comboCtrl
|| evAlt !== comboAlt
|| evShift !== comboShift) {
return false;
}
return true;
}
export type KeyBindingGetter<T extends string> = () => KeyBinding<T>[];
export interface IKeyBindingsProvider {
getMessageComposerBindings: KeyBindingGetter<MessageComposerAction>;
getAutocompleteBindings: KeyBindingGetter<AutocompleteAction>;
getRoomListBindings: KeyBindingGetter<RoomListAction>;
getRoomBindings: KeyBindingGetter<RoomAction>;
getNavigationBindings: KeyBindingGetter<NavigationAction>;
}
export class KeyBindingsManager {
/**
* List of key bindings providers.
*
* Key bindings from the first provider(s) in the list will have precedence over key bindings from later providers.
*
* To overwrite the default key bindings add a new providers before the default provider, e.g. a provider for
* customized key bindings.
*/
bindingsProviders: IKeyBindingsProvider[] = [
defaultBindingsProvider,
];
/**
* Finds a matching KeyAction for a given KeyboardEvent
*/
private getAction<T extends string>(getters: KeyBindingGetter<T>[], ev: KeyboardEvent | React.KeyboardEvent)
: T | undefined {
for (const getter of getters) {
const bindings = getter();
const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac));
if (binding) {
return binding.action;
}
}
return undefined;
}
getMessageComposerAction(ev: KeyboardEvent | React.KeyboardEvent): MessageComposerAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getMessageComposerBindings), ev);
}
getAutocompleteAction(ev: KeyboardEvent | React.KeyboardEvent): AutocompleteAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getAutocompleteBindings), ev);
}
getRoomListAction(ev: KeyboardEvent | React.KeyboardEvent): RoomListAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getRoomListBindings), ev);
}
getRoomAction(ev: KeyboardEvent | React.KeyboardEvent): RoomAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getRoomBindings), ev);
}
getNavigationAction(ev: KeyboardEvent | React.KeyboardEvent): NavigationAction | undefined {
return this.getAction(this.bindingsProviders.map(it => it.getNavigationBindings), ev);
}
}
const manager = new KeyBindingsManager();
export function getKeyBindingsManager(): KeyBindingsManager {
return manager;
}

View file

@ -49,11 +49,12 @@ export function showStartChatInviteDialog(initialText) {
); );
} }
export function showRoomInviteDialog(roomId) { export function showRoomInviteDialog(roomId, initialText = "") {
// This dialog handles the room creation internally - we don't need to worry about it. // This dialog handles the room creation internally - we don't need to worry about it.
Modal.createTrackedDialog( Modal.createTrackedDialog(
"Invite Users", "", InviteDialog, { "Invite Users", "", InviteDialog, {
kind: KIND_INVITE, kind: KIND_INVITE,
initialText,
roomId, roomId,
}, },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,

View file

@ -395,6 +395,8 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
} catch (e) { } catch (e) {
SecurityCustomisations.catchAccessSecretStorageError?.(e); SecurityCustomisations.catchAccessSecretStorageError?.(e);
console.error(e); console.error(e);
// Re-throw so that higher level logic can abort as needed
throw e;
} finally { } finally {
// Clear secret storage key cache now that work is complete // Clear secret storage key cache now that work is complete
secretStorageBeingAccessed = false; secretStorageBeingAccessed = false;

View file

@ -16,9 +16,11 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import { createRef } from "react";
import classNames from "classnames";
import { Room } from "matrix-js-sdk/src/models/room";
import GroupFilterPanel from "./GroupFilterPanel"; import GroupFilterPanel from "./GroupFilterPanel";
import CustomRoomTagPanel from "./CustomRoomTagPanel"; import CustomRoomTagPanel from "./CustomRoomTagPanel";
import classNames from "classnames";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import RoomList from "../views/rooms/RoomList"; import RoomList from "../views/rooms/RoomList";
@ -40,6 +42,7 @@ import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget"; import LeftPanelWidget from "./LeftPanelWidget";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import {mediaFromMxc} from "../../customisations/Media"; import {mediaFromMxc} from "../../customisations/Media";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -49,6 +52,7 @@ interface IProps {
interface IState { interface IState {
showBreadcrumbs: boolean; showBreadcrumbs: boolean;
showGroupFilterPanel: boolean; showGroupFilterPanel: boolean;
activeSpace?: Room;
} }
// List of CSS classes which should be included in keyboard navigation within the room list // List of CSS classes which should be included in keyboard navigation within the room list
@ -74,11 +78,13 @@ export default class LeftPanel extends React.Component<IProps, IState> {
this.state = { this.state = {
showBreadcrumbs: BreadcrumbsStore.instance.visible, showBreadcrumbs: BreadcrumbsStore.instance.visible,
showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'), showGroupFilterPanel: SettingsStore.getValue('TagPanel.enableTagPanel'),
activeSpace: SpaceStore.instance.activeSpace,
}; };
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
this.bgImageWatcherRef = SettingsStore.watchSetting( this.bgImageWatcherRef = SettingsStore.watchSetting(
"RoomList.backgroundImage", null, this.onBackgroundImageUpdate); "RoomList.backgroundImage", null, this.onBackgroundImageUpdate);
this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => { this.groupFilterPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
@ -96,9 +102,14 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace);
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize); this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
} }
private updateActiveSpace = (activeSpace: Room) => {
this.setState({ activeSpace });
};
private onExplore = () => { private onExplore = () => {
dis.fire(Action.ViewRoomDirectory); dis.fire(Action.ViewRoomDirectory);
}; };
@ -381,7 +392,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onEnter={this.onEnter} onEnter={this.onEnter}
/> />
<AccessibleTooltipButton <AccessibleTooltipButton
className="mx_LeftPanel_exploreButton" className={classNames("mx_LeftPanel_exploreButton", {
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
})}
onClick={this.onExplore} onClick={this.onExplore}
title={_t("Explore rooms")} title={_t("Explore rooms")}
/> />
@ -407,6 +420,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onBlur={this.onBlur} onBlur={this.onBlur}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}
onResize={this.onResize} onResize={this.onResize}
activeSpace={this.state.activeSpace}
/>; />;
const containerClasses = classNames({ const containerClasses = classNames({

View file

@ -21,7 +21,7 @@ import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isMac} from '../../Keyboard'; import {Key} from '../../Keyboard';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler'; import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
@ -55,6 +55,7 @@ import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal"; import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse"; import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer'; import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBindingsManager';
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";
@ -74,7 +75,6 @@ function canElementReceiveInput(el) {
interface IProps { interface IProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>; onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
viaServers?: string[];
hideToSRUsers: boolean; hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
@ -143,9 +143,6 @@ class LoggedInView extends React.Component<IProps, IState> {
// transitioned to PWLU) // transitioned to PWLU)
onRegistered: PropTypes.func, onRegistered: PropTypes.func,
// Used by the RoomView to handle joining rooms
viaServers: PropTypes.arrayOf(PropTypes.string),
// and lots and lots of other stuff. // and lots and lots of other stuff.
}; };
@ -440,86 +437,54 @@ class LoggedInView extends React.Component<IProps, IState> {
_onKeyDown = (ev) => { _onKeyDown = (ev) => {
let handled = false; let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
const modKey = isMac ? ev.metaKey : ev.ctrlKey;
switch (ev.key) { const roomAction = getKeyBindingsManager().getRoomAction(ev);
case Key.PAGE_UP: switch (roomAction) {
case Key.PAGE_DOWN: case RoomAction.ScrollUp:
if (!hasModifier && !isModifier) { case RoomAction.RoomScrollDown:
this._onScrollKeyPressed(ev); case RoomAction.JumpToFirstMessage:
handled = true; case RoomAction.JumpToLatestMessage:
} this._onScrollKeyPressed(ev);
handled = true;
break; break;
case RoomAction.FocusSearch:
dis.dispatch({
action: 'focus_search',
});
handled = true;
break;
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
return;
}
case Key.HOME: const navAction = getKeyBindingsManager().getNavigationAction(ev);
case Key.END: switch (navAction) {
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) { case NavigationAction.FocusRoomSearch:
this._onScrollKeyPressed(ev); dis.dispatch({
handled = true; action: 'focus_room_filter',
} });
handled = true;
break; break;
case Key.K: case NavigationAction.ToggleUserMenu:
if (ctrlCmdOnly) { dis.fire(Action.ToggleUserMenu);
dis.dispatch({ handled = true;
action: 'focus_room_filter',
});
handled = true;
}
break; break;
case Key.F: case NavigationAction.ToggleShortCutDialog:
if (ctrlCmdOnly && SettingsStore.getValue("ctrlFForSearch")) { KeyboardShortcuts.toggleDialog();
dis.dispatch({ handled = true;
action: 'focus_search',
});
handled = true;
}
break; break;
case Key.BACKTICK: case NavigationAction.GoToHome:
// Ideally this would be CTRL+P for "Profile", but that's dis.dispatch({
// taken by the print dialog. CTRL+I for "Information" action: 'view_home_page',
// was previously chosen but conflicted with italics in });
// composer, so CTRL+` it is Modal.closeCurrentModal("homeKeyboardShortcut");
handled = true;
if (ctrlCmdOnly) {
dis.fire(Action.ToggleUserMenu);
handled = true;
}
break; break;
case NavigationAction.ToggleRoomSidePanel:
case Key.SLASH: if (this.props.page_type === "room_view" || this.props.page_type === "group_view") {
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev)) {
KeyboardShortcuts.toggleDialog();
handled = true;
}
break;
case Key.H:
if (ev.altKey && modKey) {
dis.dispatch({
action: 'view_home_page',
});
Modal.closeCurrentModal("homeKeyboardShortcut");
handled = true;
}
break;
case Key.ARROW_UP:
case Key.ARROW_DOWN:
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: ev.key === Key.ARROW_UP ? -1 : 1,
unread: ev.shiftKey,
});
handled = true;
}
break;
case Key.PERIOD:
if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) {
dis.dispatch<ToggleRightPanelPayload>({ dis.dispatch<ToggleRightPanelPayload>({
action: Action.ToggleRightPanel, action: Action.ToggleRightPanel,
type: this.props.page_type === "room_view" ? "room" : "group", type: this.props.page_type === "room_view" ? "room" : "group",
@ -527,16 +492,48 @@ class LoggedInView extends React.Component<IProps, IState> {
handled = true; handled = true;
} }
break; break;
case NavigationAction.SelectPrevRoom:
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: -1,
unread: false,
});
handled = true;
break;
case NavigationAction.SelectNextRoom:
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: 1,
unread: false,
});
handled = true;
break;
case NavigationAction.SelectPrevUnreadRoom:
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: -1,
unread: true,
});
break;
case NavigationAction.SelectNextUnreadRoom:
dis.dispatch<ViewRoomDeltaPayload>({
action: Action.ViewRoomDelta,
delta: 1,
unread: true,
});
break;
default: default:
// if we do not have a handler for it, pass it to the platform which might // if we do not have a handler for it, pass it to the platform which might
handled = PlatformPeg.get().onKeyDown(ev); handled = PlatformPeg.get().onKeyDown(ev);
} }
if (handled) { if (handled) {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
} else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { return;
}
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
// The above condition is crafted to _allow_ characters with Shift // The above condition is crafted to _allow_ characters with Shift
// already pressed (but not the Shift key down itself). // already pressed (but not the Shift key down itself).
@ -625,11 +622,9 @@ class LoggedInView extends React.Component<IProps, IState> {
case PageTypes.RoomView: case PageTypes.RoomView:
pageElement = <RoomView pageElement = <RoomView
ref={this._roomView} ref={this._roomView}
autoJoin={this.props.autoJoin}
onRegistered={this.props.onRegistered} onRegistered={this.props.onRegistered}
threepidInvite={this.props.threepidInvite} threepidInvite={this.props.threepidInvite}
oobData={this.props.roomOobData} oobData={this.props.roomOobData}
viaServers={this.props.viaServers}
key={this.props.currentRoomId || 'roomview'} key={this.props.currentRoomId || 'roomview'}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
justCreatedOpts={this.props.roomJustCreatedOpts} justCreatedOpts={this.props.roomJustCreatedOpts}

View file

@ -80,10 +80,10 @@ import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import { shouldUseLoginForWelcome } from "../../utils/pages"; import { shouldUseLoginForWelcome } from "../../utils/pages";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import SpaceRoomDirectory from "./SpaceRoomDirectory";
import {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import RoomListStore from "../../stores/room-list/RoomListStore"; import RoomListStore from "../../stores/room-list/RoomListStore";
import {RoomUpdateCause} from "../../stores/room-list/models"; import {RoomUpdateCause} from "../../stores/room-list/models";
import defaultDispatcher from "../../dispatcher/dispatcher";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -202,7 +202,6 @@ interface IState {
ready: boolean; ready: boolean;
threepidInvite?: IThreepidInvite, threepidInvite?: IThreepidInvite,
roomOobData?: object; roomOobData?: object;
viaServers?: string[];
pendingInitialSync?: boolean; pendingInitialSync?: boolean;
justRegistered?: boolean; justRegistered?: boolean;
roomJustCreatedOpts?: IOpts; roomJustCreatedOpts?: IOpts;
@ -691,10 +690,10 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
case Action.ViewRoomDirectory: { case Action.ViewRoomDirectory: {
if (SpaceStore.instance.activeSpace) { if (SpaceStore.instance.activeSpace) {
Modal.createTrackedDialog("Space room directory", "", SpaceRoomDirectory, { defaultDispatcher.dispatch({
space: SpaceStore.instance.activeSpace, action: "view_room",
initialText: payload.initialText, room_id: SpaceStore.instance.activeSpace.roomId,
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true); });
} else { } else {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
Modal.createTrackedDialog('Room directory', '', RoomDirectory, { Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
@ -929,7 +928,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
page_type: PageTypes.RoomView, page_type: PageTypes.RoomView,
threepidInvite: roomInfo.threepid_invite, threepidInvite: roomInfo.threepid_invite,
roomOobData: roomInfo.oob_data, roomOobData: roomInfo.oob_data,
viaServers: roomInfo.via_servers,
ready: true, ready: true,
roomJustCreatedOpts: roomInfo.justCreatedOpts, roomJustCreatedOpts: roomInfo.justCreatedOpts,
}, () => { }, () => {
@ -1556,7 +1554,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else if (request.pending) { } else if (request.pending) {
ToastStore.sharedInstance().addOrReplaceToast({ ToastStore.sharedInstance().addOrReplaceToast({
key: 'verifreq_' + request.channel.transactionId, key: 'verifreq_' + request.channel.transactionId,
title: request.isSelfVerification ? _t("Self-verification request") : _t("Verification Request"), title: _t("Verification requested"),
icon: "verification", icon: "verification",
props: {request}, props: {request},
component: sdk.getComponent("toasts.VerificationRequestToast"), component: sdk.getComponent("toasts.VerificationRequestToast"),

View file

@ -46,6 +46,9 @@ function shouldFormContinuation(prevEvent, mxEvent) {
// check if within the max continuation period // check if within the max continuation period
if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false; if (mxEvent.getTs() - prevEvent.getTs() > CONTINUATION_MAX_INTERVAL) return false;
// As we summarise redactions, do not continue a redacted event onto a non-redacted one and vice-versa
if (mxEvent.isRedacted() !== prevEvent.isRedacted()) return false;
// Some events should appear as continuations from previous events of different types. // Some events should appear as continuations from previous events of different types.
if (mxEvent.getType() !== prevEvent.getType() && if (mxEvent.getType() !== prevEvent.getType() &&
(!continuedTypes.includes(mxEvent.getType()) || (!continuedTypes.includes(mxEvent.getType()) ||
@ -1125,7 +1128,7 @@ class RedactionGrouper {
} }
getNewPrevEvent() { getNewPrevEvent() {
return this.events[0]; return this.events[this.events.length - 1];
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -20,12 +20,13 @@ import classNames from "classnames";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { ActionPayload } from "../../dispatcher/payloads"; import { ActionPayload } from "../../dispatcher/payloads";
import { Key } from "../../Keyboard";
import AccessibleButton from "../views/elements/AccessibleButton"; import AccessibleButton from "../views/elements/AccessibleButton";
import { Action } from "../../dispatcher/actions"; 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 {replaceableComponent} from "../../utils/replaceableComponent"; import {replaceableComponent} from "../../utils/replaceableComponent";
import SpaceStore, {UPDATE_SELECTED_SPACE} from "../../stores/SpaceStore";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -53,6 +54,8 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
}; };
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
// clear filter when changing spaces, in future we may wish to maintain a filter per-space
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.clearInput);
} }
public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void { public componentDidUpdate(prevProps: Readonly<IProps>, prevState: Readonly<IState>): void {
@ -72,6 +75,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
public componentWillUnmount() { public componentWillUnmount() {
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.clearInput);
} }
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {
@ -108,18 +112,25 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
}; };
private onKeyDown = (ev: React.KeyboardEvent) => { private onKeyDown = (ev: React.KeyboardEvent) => {
if (ev.key === Key.ESCAPE) { const action = getKeyBindingsManager().getRoomListAction(ev);
this.clearInput(); switch (action) {
defaultDispatcher.fire(Action.FocusComposer); case RoomListAction.ClearSearch:
} else if (ev.key === Key.ARROW_UP || ev.key === Key.ARROW_DOWN) { this.clearInput();
this.props.onVerticalArrow(ev); defaultDispatcher.fire(Action.FocusComposer);
} else if (ev.key === Key.ENTER) { break;
const shouldClear = this.props.onEnter(ev); case RoomListAction.NextRoom:
if (shouldClear) { case RoomListAction.PrevRoom:
// wrap in set immediate to delay it so that we don't clear the filter & then change room this.props.onVerticalArrow(ev);
setImmediate(() => { break;
this.clearInput(); case RoomListAction.SelectRoom: {
}); const shouldClear = this.props.onEnter(ev);
if (shouldClear) {
// wrap in set immediate to delay it so that we don't clear the filter & then change room
setImmediate(() => {
this.clearInput();
});
}
break;
} }
} }
}; };

View file

@ -40,7 +40,6 @@ import Tinter from '../../Tinter';
import rateLimitedFunc from '../../ratelimitedfunc'; import rateLimitedFunc from '../../ratelimitedfunc';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching'; import eventSearch, { searchPagination } from '../../Searching';
import { isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key } from '../../Keyboard';
import MainSplit from './MainSplit'; import MainSplit from './MainSplit';
import RightPanel from './RightPanel'; import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
@ -79,6 +78,7 @@ import Notifier from "../../Notifier";
import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast"; import { showToast as showNotificationsToast } from "../../toasts/DesktopNotificationsToast";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore"; import { Container, WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects"; import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView"; import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
@ -112,10 +112,6 @@ interface IProps {
inviterName?: string; inviterName?: string;
}; };
// Servers the RoomView can use to try and assist joins
viaServers?: string[];
autoJoin?: boolean;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
justCreatedOpts?: IOpts; justCreatedOpts?: IOpts;
@ -450,9 +446,7 @@ export default class RoomView extends React.Component<IProps, IState> {
// now not joined because the js-sdk peeking API will clobber our historical room, // now not joined because the js-sdk peeking API will clobber our historical room,
// making it impossible to indicate a newly joined room. // making it impossible to indicate a newly joined room.
if (!joining && roomId) { if (!joining && roomId) {
if (this.props.autoJoin) { if (!room && shouldPeek) {
this.onJoinButtonClicked();
} else if (!room && shouldPeek) {
console.info("Attempting to peek into room %s", roomId); console.info("Attempting to peek into room %s", roomId);
this.setState({ this.setState({
peekLoading: true, peekLoading: true,
@ -668,26 +662,20 @@ export default class RoomView extends React.Component<IProps, IState> {
private onReactKeyDown = ev => { private onReactKeyDown = ev => {
let handled = false; let handled = false;
switch (ev.key) { const action = getKeyBindingsManager().getRoomAction(ev);
case Key.ESCAPE: switch (action) {
if (!ev.altKey && !ev.ctrlKey && !ev.shiftKey && !ev.metaKey) { case RoomAction.DismissReadMarker:
this.messagePanel.forgetReadMarker(); this.messagePanel.forgetReadMarker();
this.jumpToLiveTimeline(); this.jumpToLiveTimeline();
handled = true; handled = true;
}
break; break;
case Key.PAGE_UP: case RoomAction.JumpToOldestUnread:
if (!ev.altKey && !ev.ctrlKey && ev.shiftKey && !ev.metaKey) { this.jumpToReadMarker();
this.jumpToReadMarker(); handled = true;
handled = true;
}
break; break;
case Key.U: // Mac returns lowercase case RoomAction.UploadFile:
case Key.U.toUpperCase(): dis.dispatch({ action: "upload_file" }, true);
if (isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) && ev.shiftKey) { handled = true;
dis.dispatch({ action: "upload_file" }, true);
handled = true;
}
break; break;
} }
@ -1123,7 +1111,7 @@ export default class RoomView extends React.Component<IProps, IState> {
const signUrl = this.props.threepidInvite?.signUrl; const signUrl = this.props.threepidInvite?.signUrl;
dis.dispatch({ dis.dispatch({
action: 'join_room', action: 'join_room',
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers }, opts: { inviteSignUrl: signUrl },
_type: "unknown", // TODO: instrumentation _type: "unknown", // TODO: instrumentation
}); });
return Promise.resolve(); return Promise.resolve();

View file

@ -32,6 +32,8 @@ export default class SearchBox extends React.Component {
onKeyDown: PropTypes.func, onKeyDown: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
placeholder: PropTypes.string.isRequired, placeholder: PropTypes.string.isRequired,
autoFocus: PropTypes.bool,
initialValue: PropTypes.string,
// If true, the search box will focus and clear itself // If true, the search box will focus and clear itself
// on room search focus action (it would be nicer to take // on room search focus action (it would be nicer to take
@ -49,7 +51,7 @@ export default class SearchBox extends React.Component {
this._search = createRef(); this._search = createRef();
this.state = { this.state = {
searchTerm: "", searchTerm: this.props.initialValue || "",
blurred: true, blurred: true,
}; };
} }
@ -158,6 +160,7 @@ export default class SearchBox extends React.Component {
onBlur={this._onBlur} onBlur={this._onBlur}
placeholder={ placeholder } placeholder={ placeholder }
autoComplete="off" autoComplete="off"
autoFocus={this.props.autoFocus}
/> />
{ clearButton } { clearButton }
</div> </div>

View file

@ -15,7 +15,8 @@ limitations under the License.
*/ */
import React, {useMemo, useState} from "react"; import React, {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 {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
import classNames from "classnames"; import classNames from "classnames";
import {sortBy} from "lodash"; import {sortBy} from "lodash";
@ -39,10 +40,11 @@ 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";
interface IProps { interface IHierarchyProps {
space: Room; space: Room;
initialText?: string; initialText?: string;
onFinished(): void; refreshToken?: any;
showRoom(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin?: boolean): void;
} }
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -77,7 +79,6 @@ export interface ISpaceSummaryEvent {
interface ITileProps { interface ITileProps {
room: ISpaceSummaryRoom; room: ISpaceSummaryRoom;
editing?: boolean;
suggested?: boolean; suggested?: boolean;
selected?: boolean; selected?: boolean;
numChildRooms?: number; numChildRooms?: number;
@ -88,7 +89,6 @@ interface ITileProps {
const Tile: React.FC<ITileProps> = ({ const Tile: React.FC<ITileProps> = ({
room, room,
editing,
suggested, suggested,
selected, selected,
hasPermissions, hasPermissions,
@ -112,7 +112,7 @@ const Tile: React.FC<ITileProps> = ({
let button; let button;
if (myMembership === "join") { if (myMembership === "join") {
button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline"> button = <AccessibleButton onClick={onPreviewClick} kind="primary_outline">
{ _t("Open") } { _t("View") }
</AccessibleButton>; </AccessibleButton>;
} else if (onJoinClick) { } else if (onJoinClick) {
button = <AccessibleButton onClick={onJoinClick} kind="primary"> button = <AccessibleButton onClick={onJoinClick} kind="primary">
@ -170,12 +170,6 @@ const Tile: React.FC<ITileProps> = ({
</div> </div>
</React.Fragment>; </React.Fragment>;
if (editing) {
return <div className="mx_SpaceRoomDirectory_roomTile">
{ content }
</div>
}
let childToggle; let childToggle;
let childSection; let childSection;
if (children) { if (children) {
@ -201,7 +195,7 @@ const Tile: React.FC<ITileProps> = ({
className={classNames("mx_SpaceRoomDirectory_roomTile", { className={classNames("mx_SpaceRoomDirectory_roomTile", {
mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space, mx_SpaceRoomDirectory_subspace: room.room_type === RoomType.Space,
})} })}
onClick={hasPermissions ? onToggleClick : onPreviewClick} onClick={(hasPermissions && onToggleClick) ? onToggleClick : onPreviewClick}
> >
{ content } { content }
{ childToggle } { childToggle }
@ -240,7 +234,7 @@ export const showRoom = (room: ISpaceSummaryRoom, viaServers?: string[], autoJoi
interface IHierarchyLevelProps { interface IHierarchyLevelProps {
spaceId: string; spaceId: string;
rooms: Map<string, ISpaceSummaryRoom>; rooms: Map<string, ISpaceSummaryRoom>;
relations: EnhancedMap<string, Map<string, ISpaceSummaryEvent>>; relations: Map<string, Map<string, ISpaceSummaryEvent>>;
parents: Set<string>; parents: Set<string>;
selectedMap?: Map<string, Set<string>>; selectedMap?: Map<string, Set<string>>;
onViewRoomClick(roomId: string, autoJoin: boolean): void; onViewRoomClick(roomId: string, autoJoin: boolean): void;
@ -258,9 +252,9 @@ export const HierarchyLevel = ({
}: IHierarchyLevelProps) => { }: IHierarchyLevelProps) => {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
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 sortedChildren = sortBy([...(relations.get(spaceId)?.values() || [])], ev => ev.content.order || null);
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;
@ -316,23 +310,15 @@ export const HierarchyLevel = ({
</React.Fragment> </React.Fragment>
}; };
const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinished }) => { // mutate argument refreshToken to force a reload
export const useSpaceSummary = (cli: MatrixClient, space: Room, refreshToken?: any): [
ISpaceSummaryRoom[],
Map<string, Map<string, ISpaceSummaryEvent>>,
Map<string, Set<string>>,
Map<string, Set<string>>,
] | [] => {
// TODO pagination // TODO pagination
const cli = MatrixClientPeg.get(); return useAsyncMemo(async () => {
const userId = cli.getUserId();
const [query, setQuery] = useState(initialText);
const onCreateRoomClick = () => {
dis.dispatch({
action: 'view_create_room',
public: true,
});
onFinished();
};
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
const [rooms, parentChildMap, childParentMap, viaMap] = useAsyncMemo(async () => {
try { try {
const data = await cli.getSpaceSummary(space.roomId); const data = await cli.getSpaceSummary(space.roomId);
@ -350,13 +336,29 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
} }
}); });
return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, childParentRelations, viaMap]; return [data.rooms as ISpaceSummaryRoom[], parentChildRelations, viaMap, childParentRelations];
} catch (e) { } catch (e) {
console.error(e); // TODO console.error(e); // TODO
} }
return []; return [];
}, [space], []); }, [space, refreshToken], []);
};
export const SpaceHierarchy: React.FC<IHierarchyProps> = ({
space,
initialText = "",
showRoom,
refreshToken,
children,
}) => {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const [query, setQuery] = useState(initialText);
const [selected, setSelected] = useState(new Map<string, Set<string>>()); // Map<parentId, Set<childId>>
const [rooms, parentChildMap, viaMap, childParentMap] = useSpaceSummary(cli, space, refreshToken);
const roomsMap = useMemo(() => { const roomsMap = useMemo(() => {
if (!rooms) return null; if (!rooms) return null;
@ -391,21 +393,6 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
return roomsMap; return roomsMap;
}, [rooms, childParentMap, query]); }, [rooms, childParentMap, query]);
const title = <React.Fragment>
<RoomAvatar room={space} height={32} width={32} />
<div>
<h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div>
</div>
</React.Fragment>;
const explanation =
_t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.", null,
{a: sub => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}},
);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [removing, setRemoving] = useState(false); const [removing, setRemoving] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -500,6 +487,8 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
let results; let results;
if (roomsMap.size) { if (roomsMap.size) {
const hasPermissions = space?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId());
results = <> results = <>
<HierarchyLevel <HierarchyLevel
spaceId={space.roomId} spaceId={space.roomId}
@ -507,7 +496,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
relations={parentChildMap} relations={parentChildMap}
parents={new Set()} parents={new Set()}
selectedMap={selected} selectedMap={selected}
onToggleClick={(parentId, childId) => { onToggleClick={hasPermissions ? (parentId, childId) => {
setError(""); setError("");
if (!selected.has(parentId)) { if (!selected.has(parentId)) {
setSelected(new Map(selected.set(parentId, new Set([childId])))); setSelected(new Map(selected.set(parentId, new Set([childId]))));
@ -522,13 +511,12 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
parentSet.delete(childId); parentSet.delete(childId);
setSelected(new Map(selected.set(parentId, new Set(parentSet)))); setSelected(new Map(selected.set(parentId, new Set(parentSet))));
}} } : undefined}
onViewRoomClick={(roomId, autoJoin) => { onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin); showRoom(roomsMap.get(roomId), Array.from(viaMap.get(roomId) || []), autoJoin);
onFinished();
}} }}
/> />
<hr /> { children && <hr /> }
</>; </>;
} else { } else {
results = <div className="mx_SpaceRoomDirectory_noResults"> results = <div className="mx_SpaceRoomDirectory_noResults">
@ -547,32 +535,78 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
</div> } </div> }
<AutoHideScrollbar className="mx_SpaceRoomDirectory_list"> <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
{ results } { results }
<AccessibleButton { children }
onClick={onCreateRoomClick}
kind="primary"
className="mx_SpaceRoomDirectory_createRoom"
>
{ _t("Create room") }
</AccessibleButton>
</AutoHideScrollbar> </AutoHideScrollbar>
</>; </>;
} else { } else if (!rooms) {
content = <Spinner />; content = <Spinner />;
} else {
content = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
} }
// TODO loading state/error state // TODO loading state/error state
return <>
<SearchBox
className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Search names and description") }
onSearch={setQuery}
autoFocus={true}
initialValue={initialText}
/>
{ content }
</>;
};
interface IProps {
space: Room;
initialText?: string;
onFinished(): void;
}
const SpaceRoomDirectory: React.FC<IProps> = ({ space, onFinished, initialText }) => {
const onCreateRoomClick = () => {
dis.dispatch({
action: 'view_create_room',
public: true,
});
onFinished();
};
const title = <React.Fragment>
<RoomAvatar room={space} height={32} width={32} />
<div>
<h1>{ _t("Explore rooms") }</h1>
<div><RoomName room={space} /></div>
</div>
</React.Fragment>;
return ( return (
<BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}> <BaseDialog className="mx_SpaceRoomDirectory" hasCancel={true} onFinished={onFinished} title={title}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
{ explanation } { _t("If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
null,
{a: sub => {
return <AccessibleButton kind="link" onClick={onCreateRoomClick}>{sub}</AccessibleButton>;
}},
) }
<SearchBox <SpaceHierarchy
className="mx_textinput_icon mx_textinput_search" space={space}
placeholder={ _t("Search names and description") } showRoom={(room: ISpaceSummaryRoom, viaServers?: string[], autoJoin = false) => {
onSearch={setQuery} showRoom(room, viaServers, autoJoin);
/> onFinished();
}}
{ content } initialText={initialText}
>
<AccessibleButton
onClick={onCreateRoomClick}
kind="primary"
className="mx_SpaceRoomDirectory_createRoom"
>
{ _t("Create room") }
</AccessibleButton>
</SpaceHierarchy>
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
import React, {RefObject, useContext, useRef, useState} from "react"; import React, {RefObject, useContext, useRef, useState} from "react";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import {EventType} 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 {EventSubscription} from "fbemitter"; import {EventSubscription} from "fbemitter";
@ -26,7 +26,6 @@ import AccessibleButton from "../views/elements/AccessibleButton";
import RoomName from "../views/elements/RoomName"; import RoomName from "../views/elements/RoomName";
import RoomTopic from "../views/elements/RoomTopic"; import RoomTopic from "../views/elements/RoomTopic";
import InlineSpinner from "../views/elements/InlineSpinner"; import InlineSpinner from "../views/elements/InlineSpinner";
import FormButton from "../views/elements/FormButton";
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite"; import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
import {useRoomMembers} from "../../hooks/useRoomMembers"; import {useRoomMembers} from "../../hooks/useRoomMembers";
import createRoom, {IOpts, Preset} from "../../createRoom"; import createRoom, {IOpts, Preset} from "../../createRoom";
@ -47,13 +46,11 @@ import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanel
import {useStateArray} from "../../hooks/useStateArray"; import {useStateArray} from "../../hooks/useStateArray";
import SpacePublicShare from "../views/spaces/SpacePublicShare"; import SpacePublicShare from "../views/spaces/SpacePublicShare";
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space"; import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
import {HierarchyLevel, ISpaceSummaryEvent, ISpaceSummaryRoom, showRoom} from "./SpaceRoomDirectory"; import {showRoom, SpaceHierarchy} from "./SpaceRoomDirectory";
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
import {EnhancedMap} from "../../utils/maps";
import AutoHideScrollbar from "./AutoHideScrollbar";
import MemberAvatar from "../views/avatars/MemberAvatar"; import MemberAvatar from "../views/avatars/MemberAvatar";
import {useStateToggle} from "../../hooks/useStateToggle"; import {useStateToggle} from "../../hooks/useStateToggle";
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import FacePile from "../views/elements/FacePile";
interface IProps { interface IProps {
space: Room; space: Room;
@ -95,6 +92,41 @@ const useMyRoomMembership = (room: Room) => {
return membership; return membership;
}; };
const SpaceInfo = ({ space }) => {
const joinRule = space.getJoinRule();
let visibilitySection;
if (joinRule === "public") {
visibilitySection = <span className="mx_SpaceRoomView_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_info">
{ visibilitySection }
{ joinRule === "public" && <RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount> }
</div>
};
const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
@ -124,30 +156,36 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
} }
joinButtons = <> joinButtons = <>
<FormButton <AccessibleButton
label={_t("Reject")}
kind="secondary" kind="secondary"
onClick={() => { onClick={() => {
setBusy(true); setBusy(true);
onRejectButtonClicked(); onRejectButtonClicked();
}} /> }}
<FormButton >
label={_t("Accept")} { _t("Reject") }
</AccessibleButton>
<AccessibleButton
kind="primary"
onClick={() => { onClick={() => {
setBusy(true); setBusy(true);
onJoinButtonClicked(); onJoinButtonClicked();
}} }}
/> >
{ _t("Accept") }
</AccessibleButton>
</>; </>;
} else { } else {
joinButtons = ( joinButtons = (
<FormButton <AccessibleButton
label={_t("Join")} kind="primary"
onClick={() => { onClick={() => {
setBusy(true); setBusy(true);
onJoinButtonClicked(); onJoinButtonClicked();
}} }}
/> >
{ _t("Join") }
</AccessibleButton>
) )
} }
@ -155,43 +193,13 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
joinButtons = <InlineSpinner />; joinButtons = <InlineSpinner />;
} }
let visibilitySection;
if (space.getJoinRule() === "public") {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_preview"> return <div className="mx_SpaceRoomView_preview">
{ inviterSection } { inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name"> <h1 className="mx_SpaceRoomView_preview_name">
<RoomName room={space} /> <RoomName room={space} />
</h1> </h1>
<div className="mx_SpaceRoomView_preview_info"> <SpaceInfo space={space} />
{ visibilitySection }
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_preview_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div>
<RoomTopic room={space}> <RoomTopic room={space}>
{(topic, ref) => {(topic, ref) =>
<div className="mx_SpaceRoomView_preview_topic" ref={ref}> <div className="mx_SpaceRoomView_preview_topic" ref={ref}>
@ -199,6 +207,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
</div> </div>
} }
</RoomTopic> </RoomTopic>
{ space.getJoinRule() === "public" && <FacePile room={space} /> }
<div className="mx_SpaceRoomView_preview_joinButtons"> <div className="mx_SpaceRoomView_preview_joinButtons">
{ joinButtons } { joinButtons }
</div> </div>
@ -213,17 +222,21 @@ const SpaceLanding = ({ space }) => {
let inviteButton; let inviteButton;
if (myMembership === "join" && space.canInvite(userId)) { if (myMembership === "join" && space.canInvite(userId)) {
inviteButton = ( inviteButton = (
<AccessibleButton className="mx_SpaceRoomView_landing_inviteButton" onClick={() => { <AccessibleButton
showRoomInviteDialog(space.roomId); kind="primary"
}}> className="mx_SpaceRoomView_landing_inviteButton"
{ _t("Invite people") } onClick={() => {
showRoomInviteDialog(space.roomId);
}}
>
{ _t("Invite") }
</AccessibleButton> </AccessibleButton>
); );
} }
const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId); const canAddRooms = myMembership === "join" && space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
const [_, forceUpdate] = useStateToggle(false); // TODO const [refreshToken, forceUpdate] = useStateToggle(false);
let addRoomButtons; let addRoomButtons;
if (canAddRooms) { if (canAddRooms) {
@ -253,49 +266,13 @@ const SpaceLanding = ({ space }) => {
</AccessibleButton>; </AccessibleButton>;
} }
const [loading, roomsMap, relations, numRooms] = useAsyncMemo(async () => { const onMembersClick = () => {
try { defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
const data = await cli.getSpaceSummary(space.roomId, undefined, myMembership !== "join"); action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
const parentChildRelations = new EnhancedMap<string, Map<string, ISpaceSummaryEvent>>(); refireParams: { space },
data.events.map((ev: ISpaceSummaryEvent) => { });
if (ev.type === EventType.SpaceChild) { };
parentChildRelations.getOrCreate(ev.room_id, new Map()).set(ev.state_key, ev);
}
});
const roomsMap = new Map<string, ISpaceSummaryRoom>(data.rooms.map(r => [r.room_id, r]));
const numRooms = data.rooms.filter(r => r.room_type !== RoomType.Space).length;
return [false, roomsMap, parentChildRelations, numRooms];
} catch (e) {
console.error(e); // TODO
}
return [false];
}, [space, _], [true]);
let previewRooms;
if (roomsMap) {
previewRooms = <AutoHideScrollbar className="mx_SpaceRoomDirectory_list">
<div className="mx_SpaceRoomDirectory_roomCount">
<h3>{ myMembership === "join" ? _t("Rooms") : _t("Default Rooms")}</h3>
<span>{ numRooms }</span>
</div>
<HierarchyLevel
spaceId={space.roomId}
rooms={roomsMap}
relations={relations}
parents={new Set()}
onViewRoomClick={(roomId, autoJoin) => {
showRoom(roomsMap.get(roomId), [], autoJoin);
}}
/>
</AutoHideScrollbar>;
} else if (loading) {
previewRooms = <InlineSpinner />;
} else {
previewRooms = <p>{_t("Your server does not support showing space hierarchies.")}</p>;
}
return <div className="mx_SpaceRoomView_landing"> return <div className="mx_SpaceRoomView_landing">
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} /> <RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
@ -304,45 +281,26 @@ const SpaceLanding = ({ space }) => {
{(name) => { {(name) => {
const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow"> const tags = { name: () => <div className="mx_SpaceRoomView_landing_nameRow">
<h1>{ name }</h1> <h1>{ name }</h1>
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_landing_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div> }; </div> };
if (shouldShowSpaceSettings(cli, space)) {
if (space.getJoinRule() === "public") {
return _t("Your public space <name/>", {}, tags) as JSX.Element;
} else {
return _t("Your private space <name/>", {}, tags) as JSX.Element;
}
}
return _t("Welcome to <name/>", {}, tags) as JSX.Element; return _t("Welcome to <name/>", {}, tags) as JSX.Element;
}} }}
</RoomName> </RoomName>
</div> </div>
<div className="mx_SpaceRoomView_landing_info">
<SpaceInfo space={space} />
<FacePile room={space} onlyKnownUsers={false} numShown={7} onClick={onMembersClick} />
{ inviteButton }
</div>
<div className="mx_SpaceRoomView_landing_topic"> <div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} /> <RoomTopic room={space} />
</div> </div>
<hr />
<div className="mx_SpaceRoomView_landing_adminButtons"> <div className="mx_SpaceRoomView_landing_adminButtons">
{ inviteButton }
{ addRoomButtons } { addRoomButtons }
{ settingsButton } { settingsButton }
</div> </div>
{ previewRooms } <SpaceHierarchy space={space} showRoom={showRoom} refreshToken={refreshToken} />
</div>; </div>;
}; };
@ -407,11 +365,13 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
{ fields } { fields }
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<FormButton <AccessibleButton
label={buttonLabel} kind="primary"
disabled={busy} disabled={busy}
onClick={onClick} onClick={onClick}
/> >
{ buttonLabel }
</AccessibleButton>
</div> </div>
</div>; </div>;
}; };
@ -419,14 +379,16 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => {
const SpaceSetupPublicShare = ({ space, onFinished }) => { const SpaceSetupPublicShare = ({ space, onFinished }) => {
return <div className="mx_SpaceRoomView_publicShare"> return <div className="mx_SpaceRoomView_publicShare">
<h1>{ _t("Share %(name)s", { name: space.name }) }</h1> <h1>{ _t("Share %(name)s", { name: space.name }) }</h1>
<div className="mx_SpacePublicShare_description"> <div className="mx_SpaceRoomView_description">
{ _t("It's just you at the moment, it will be even better with others.") } { _t("It's just you at the moment, it will be even better with others.") }
</div> </div>
<SpacePublicShare space={space} onFinished={onFinished} /> <SpacePublicShare space={space} />
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<FormButton label={_t("Go to my first room")} onClick={onFinished} /> <AccessibleButton kind="primary" onClick={onFinished}>
{ _t("Go to my first room") }
</AccessibleButton>
</div> </div>
</div>; </div>;
}; };
@ -545,7 +507,9 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
</div> </div>
<div className="mx_SpaceRoomView_buttons"> <div className="mx_SpaceRoomView_buttons">
<FormButton label={buttonLabel} disabled={busy} onClick={onClick} /> <AccessibleButton kind="primary" disabled={busy} onClick={onClick}>
{ buttonLabel }
</AccessibleButton>
</div> </div>
</div>; </div>;
}; };
@ -630,6 +594,8 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
}; };
private goToFirstRoom = async () => { private goToFirstRoom = async () => {
// TODO actually go to the first room
const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId); const childRooms = SpaceStore.instance.getChildRooms(this.props.space.roomId);
if (childRooms.length) { if (childRooms.length) {
const room = childRooms[0]; const room = childRooms[0];
@ -677,9 +643,13 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
case Phase.PublicCreateRooms: case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}
title={_t("What are some things you want to discuss?")} title={_t("What are some things you want to discuss in %(spaceName)s?", {
description={_t("Let's create a room for each of them. " + spaceName: this.props.space.name,
"You can add more later too, including already existing ones.")} })}
description={
_t("Let's create a room for each of them.") + "\n" +
_t("You can add more later too, including already existing ones.")
}
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>; />;
case Phase.PublicShare: case Phase.PublicShare:

View file

@ -43,7 +43,11 @@ export default class UploadBar extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {uploadsHere: []};
// Set initial state to any available upload in this room - we might be mounting
// earlier than the first progress event, so should show something relevant.
const uploadsHere = this.getUploadsInRoom();
this.state = {currentUpload: uploadsHere[0], uploadsHere};
} }
componentDidMount() { componentDidMount() {
@ -56,6 +60,11 @@ export default class UploadBar extends React.Component<IProps, IState> {
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
private getUploadsInRoom(): IUpload[] {
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
return uploads.filter(u => u.roomId === this.props.room.roomId);
}
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {
switch (payload.action) { switch (payload.action) {
case Action.UploadStarted: case Action.UploadStarted:
@ -64,8 +73,7 @@ export default class UploadBar extends React.Component<IProps, IState> {
case Action.UploadCanceled: case Action.UploadCanceled:
case Action.UploadFailed: { case Action.UploadFailed: {
if (!this.mounted) return; if (!this.mounted) return;
const uploads = ContentMessages.sharedInstance().getCurrentUploads(); const uploadsHere = this.getUploadsInRoom();
const uploadsHere = uploads.filter(u => u.roomId === this.props.room.roomId);
this.setState({currentUpload: uploadsHere[0], uploadsHere}); this.setState({currentUpload: uploadsHere[0], uploadsHere});
break; break;
} }

View file

@ -176,8 +176,8 @@ export default class ViewSource extends React.Component {
return ( return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}> <BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t("View Source")}>
<div> <div>
<div className="mx_ViewSource_label_left">Room ID: {roomId}</div> <div>Room ID: {roomId}</div>
<div className="mx_ViewSource_label_left">Event ID: {eventId}</div> <div>Event ID: {eventId}</div>
<div className="mx_ViewSource_separator" /> <div className="mx_ViewSource_separator" />
{isEditing ? this.editSourceContent() : this.viewSourceContent()} {isEditing ? this.editSourceContent() : this.viewSourceContent()}
</div> </div>

View file

@ -155,15 +155,14 @@ export default class SetupEncryptionBody extends React.Component {
let verifyButton; let verifyButton;
if (store.hasDevicesToVerifyAgainst) { if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}> verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
{ _t("Verify with another session") } { _t("Use another login") }
</AccessibleButton>; </AccessibleButton>;
} }
return ( return (
<div> <div>
<p>{_t( <p>{_t(
"Verify this login to access your encrypted messages and " + "Verify your identity to access encrypted messages and prove your identity to others.",
"prove to others that this login is really you.",
)}</p> )}</p>
<div className="mx_CompleteSecurity_actionRow"> <div className="mx_CompleteSecurity_actionRow">
@ -205,8 +204,8 @@ export default class SetupEncryptionBody extends React.Component {
return ( return (
<div> <div>
<p>{_t( <p>{_t(
"Without completing security on this session, it wont have " + "Without verifying, you wont have access to all your messages " +
"access to encrypted messages.", "and may appear as untrusted to others.",
)}</p> )}</p>
<div className="mx_CompleteSecurity_actionRow"> <div className="mx_CompleteSecurity_actionRow">
<AccessibleButton <AccessibleButton

View file

@ -22,7 +22,6 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
import {_t} from '../../../languageHandler'; import {_t} from '../../../languageHandler';
import {IDialogProps} from "./IDialogProps"; import {IDialogProps} from "./IDialogProps";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import FormButton from "../elements/FormButton";
import Dropdown from "../elements/Dropdown"; import Dropdown from "../elements/Dropdown";
import SearchBox from "../../structures/SearchBox"; import SearchBox from "../../structures/SearchBox";
import SpaceStore from "../../../stores/SpaceStore"; import SpaceStore from "../../../stores/SpaceStore";
@ -110,7 +109,7 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
const title = <React.Fragment> const title = <React.Fragment>
<RoomAvatar room={selectedSpace} height={40} width={40} /> <RoomAvatar room={selectedSpace} height={40} width={40} />
<div> <div>
<h1>{ _t("Add existing spaces/rooms") }</h1> <h1>{ _t("Add existing rooms") }</h1>
{ spaceOptionSection } { spaceOptionSection }
</div> </div>
</React.Fragment>; </React.Fragment>;
@ -128,29 +127,9 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
placeholder={ _t("Filter your rooms and spaces") } placeholder={ _t("Filter your rooms and spaces") }
onSearch={setQuery} onSearch={setQuery}
autoComplete={true}
/> />
<AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog"> <AutoHideScrollbar className="mx_AddExistingToSpaceDialog_content" id="mx_AddExistingToSpaceDialog">
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
<h3>{ _t("Spaces") }</h3>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(space);
} else {
selectedToAdd.delete(space);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : null }
{ rooms.length > 0 ? ( { rooms.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section"> <div className="mx_AddExistingToSpaceDialog_section">
<h3>{ _t("Rooms") }</h3> <h3>{ _t("Rooms") }</h3>
@ -172,6 +151,27 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</div> </div>
) : undefined } ) : undefined }
{ spaces.length > 0 ? (
<div className="mx_AddExistingToSpaceDialog_section mx_AddExistingToSpaceDialog_section_spaces">
<h3>{ _t("Spaces") }</h3>
{ spaces.map(space => {
return <Entry
key={space.roomId}
room={space}
checked={selectedToAdd.has(space)}
onChange={(checked) => {
if (checked) {
selectedToAdd.add(space);
} else {
selectedToAdd.delete(space);
}
setSelectedToAdd(new Set(selectedToAdd));
}}
/>;
}) }
</div>
) : null }
{ spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults"> { spaces.length + rooms.length < 1 ? <span className="mx_AddExistingToSpaceDialog_noResults">
{ _t("No results") } { _t("No results") }
</span> : undefined } </span> : undefined }
@ -185,8 +185,8 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
</AccessibleButton> </AccessibleButton>
</span> </span>
<FormButton <AccessibleButton
label={busy ? _t("Applying...") : _t("Apply")} kind="primary"
disabled={busy || selectedToAdd.size < 1} disabled={busy || selectedToAdd.size < 1}
onClick={async () => { onClick={async () => {
setBusy(true); setBusy(true);
@ -200,7 +200,9 @@ const AddExistingToSpaceDialog: React.FC<IProps> = ({ matrixClient: cli, space,
} }
setBusy(false); setBusy(false);
}} }}
/> >
{ busy ? _t("Adding...") : _t("Add") }
</AccessibleButton>
</div> </div>
</BaseDialog>; </BaseDialog>;
}; };

View file

@ -43,6 +43,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media"; import {mediaFromMxc} from "../../../customisations/Media";
import {getAddressType} from "../../../UserAddress";
// we have a number of types defined from the Matrix spec which can't reasonably be altered here. // we have a number of types defined from the Matrix spec which can't reasonably be altered here.
/* eslint-disable camelcase */ /* eslint-disable camelcase */
@ -673,19 +674,20 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
console.error(err); console.error(err);
this.setState({ this.setState({
busy: false, busy: false,
errorText: _t("We couldn't create your DM. Please check the users you want to invite and try again."), errorText: _t("We couldn't create your DM."),
}); });
}); });
}; };
_inviteUsers = () => { _inviteUsers = async () => {
const startTime = CountlyAnalytics.getTimestamp(); const startTime = CountlyAnalytics.getTimestamp();
this.setState({busy: true}); this.setState({busy: true});
this._convertFilter(); this._convertFilter();
const targets = this._convertFilter(); const targets = this._convertFilter();
const targetIds = targets.map(t => t.userId); const targetIds = targets.map(t => t.userId);
const room = MatrixClientPeg.get().getRoom(this.props.roomId); const cli = MatrixClientPeg.get();
const room = cli.getRoom(this.props.roomId);
if (!room) { if (!room) {
console.error("Failed to find the room to invite users to"); console.error("Failed to find the room to invite users to");
this.setState({ this.setState({
@ -695,12 +697,34 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return; return;
} }
inviteMultipleToRoom(this.props.roomId, targetIds).then(result => { try {
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length); CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too if (!this._shouldAbortAfterInviteError(result)) { // handles setting error message too
this.props.onFinished(); this.props.onFinished();
} }
}).catch(err => {
if (cli.isRoomEncrypted(this.props.roomId) &&
SettingsStore.getValue("feature_room_history_key_sharing")) {
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",
);
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
if (visibility == "world_readable" || visibility == "shared") {
const invitedUsers = [];
for (const [addr, state] of Object.entries(result.states)) {
if (state === "invited" && getAddressType(addr) === "mx-user-id") {
invitedUsers.push(addr);
}
}
console.log("Sharing history with", invitedUsers);
cli.sendSharedHistoryKeys(
this.props.roomId, invitedUsers,
);
}
}
} catch (err) {
console.error(err); console.error(err);
this.setState({ this.setState({
busy: false, busy: false,
@ -708,7 +732,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
"We couldn't invite those users. Please check the users you want to invite and try again.", "We couldn't invite those users. Please check the users you want to invite and try again.",
), ),
}); });
}); }
}; };
_transferCall = async () => { _transferCall = async () => {
@ -886,19 +910,21 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}; };
_toggleMember = (member: Member) => { _toggleMember = (member: Member) => {
let filterText = this.state.filterText; if (!this.state.busy) {
const targets = this.state.targets.map(t => t); // cheap clone for mutation let filterText = this.state.filterText;
const idx = targets.indexOf(member); const targets = this.state.targets.map(t => t); // cheap clone for mutation
if (idx >= 0) { const idx = targets.indexOf(member);
targets.splice(idx, 1); if (idx >= 0) {
} else { targets.splice(idx, 1);
targets.push(member); } else {
filterText = ""; // clear the filter when the user accepts a suggestion targets.push(member);
} filterText = ""; // clear the filter when the user accepts a suggestion
this.setState({targets, filterText}); }
this.setState({targets, filterText});
if (this._editorRef && this._editorRef.current) { if (this._editorRef && this._editorRef.current) {
this._editorRef.current.focus(); this._editorRef.current.focus();
}
} }
}; };
@ -1189,10 +1215,12 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
let helpText; let helpText;
let buttonText; let buttonText;
let goButtonFn; let goButtonFn;
let keySharingWarning = <span />;
const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer); const identityServersEnabled = SettingsStore.getValue(UIFeature.IdentityServer);
const userId = MatrixClientPeg.get().getUserId(); const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
if (this.props.kind === KIND_DM) { if (this.props.kind === KIND_DM) {
title = _t("Direct Messages"); title = _t("Direct Messages");
@ -1288,6 +1316,25 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
buttonText = _t("Invite"); buttonText = _t("Invite");
goButtonFn = this._inviteUsers; goButtonFn = this._inviteUsers;
if (SettingsStore.getValue("feature_room_history_key_sharing") &&
cli.isRoomEncrypted(this.props.roomId)) {
const room = cli.getRoom(this.props.roomId);
const visibilityEvent = room.currentState.getStateEvents(
"m.room.history_visibility", "",
);
const visibility = visibilityEvent && visibilityEvent.getContent() &&
visibilityEvent.getContent().history_visibility;
if (visibility === "world_readable" || visibility === "shared") {
keySharingWarning =
<p className='mx_InviteDialog_helpText'>
<img
src={require("../../../../res/img/element-icons/info.svg")}
width={14} height={14} />
{" " + _t("Invited people will be able to read old messages.")}
</p>;
}
}
} else if (this.props.kind === KIND_CALL_TRANSFER) { } else if (this.props.kind === KIND_CALL_TRANSFER) {
title = _t("Transfer"); title = _t("Transfer");
buttonText = _t("Transfer"); buttonText = _t("Transfer");
@ -1321,6 +1368,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
{spinner} {spinner}
</div> </div>
</div> </div>
{keySharingWarning}
{this._renderIdentityServerWarning()} {this._renderIdentityServerWarning()}
<div className='error'>{this.state.errorText}</div> <div className='error'>{this.state.errorText}</div>
<div className='mx_InviteDialog_userSections'> <div className='mx_InviteDialog_userSections'>

View file

@ -28,7 +28,6 @@ import {getTopic} from "../elements/RoomTopic";
import {avatarUrlForRoom} from "../../../Avatar"; import {avatarUrlForRoom} from "../../../Avatar";
import ToggleSwitch from "../elements/ToggleSwitch"; import ToggleSwitch from "../elements/ToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import FormButton from "../elements/FormButton";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import {allSettled} from "../../../utils/promise"; import {allSettled} from "../../../utils/promise";
@ -127,23 +126,24 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<div> <div>
{ _t("Make this space private") } { _t("Make this space private") }
<ToggleSwitch <ToggleSwitch
checked={joinRule === "private"} checked={joinRule !== "public"}
onChange={checked => setJoinRule(checked ? "private" : "invite")} onChange={checked => setJoinRule(checked ? "invite" : "public")}
disabled={!canSetJoinRule} disabled={!canSetJoinRule}
aria-label={_t("Make this space private")} aria-label={_t("Make this space private")}
/> />
</div> </div>
<FormButton <AccessibleButton
kind="danger" kind="danger"
label={_t("Leave Space")}
onClick={() => { onClick={() => {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: "leave_room", action: "leave_room",
room_id: space.roomId, room_id: space.roomId,
}); });
}} }}
/> >
{ _t("Leave Space") }
</AccessibleButton>
<div className="mx_SpaceSettingsDialog_buttons"> <div className="mx_SpaceSettingsDialog_buttons">
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}> <AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
@ -152,7 +152,9 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
<AccessibleButton onClick={onFinished} disabled={busy} kind="link"> <AccessibleButton onClick={onFinished} disabled={busy} kind="link">
{ _t("Cancel") } { _t("Cancel") }
</AccessibleButton> </AccessibleButton>
<FormButton onClick={onSave} disabled={busy} label={busy ? _t("Saving...") : _t("Save Changes")} /> <AccessibleButton onClick={onSave} disabled={busy} kind="primary">
{ busy ? _t("Saving...") : _t("Save Changes") }
</AccessibleButton>
</div> </div>
</div> </div>
</BaseDialog>; </BaseDialog>;

View file

@ -50,7 +50,7 @@ export default class VerificationRequestDialog extends React.Component {
const member = this.props.member || const member = this.props.member ||
otherUserId && MatrixClientPeg.get().getUser(otherUserId); otherUserId && MatrixClientPeg.get().getUser(otherUserId);
const title = request && request.isSelfVerification ? const title = request && request.isSelfVerification ?
_t("Verify other session") : _t("Verification Request"); _t("Verify other login") : _t("Verification Request");
return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished} return <BaseDialog className="mx_InfoDialog" onFinished={this.props.onFinished}
contentId="mx_Dialog_content" contentId="mx_Dialog_content"

View file

@ -1,6 +1,5 @@
/* /*
Copyright 2018, 2019 New Vector Ltd Copyright 2018-2021 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,14 +16,15 @@ limitations under the License.
import {debounce} from "lodash"; import {debounce} from "lodash";
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React, {ChangeEvent, FormEvent} from 'react';
import PropTypes from "prop-types"; import {ISecretStorageKeyInfo} from "matrix-js-sdk/src";
import * as sdk from '../../../../index'; import * as sdk from '../../../../index';
import {MatrixClientPeg} from '../../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../../MatrixClientPeg';
import Field from '../../elements/Field'; import Field from '../../elements/Field';
import AccessibleButton from '../../elements/AccessibleButton'; import AccessibleButton from '../../elements/AccessibleButton';
import {_t} from '../../../../languageHandler';
import { _t } from '../../../../languageHandler'; import {IDialogProps} from "../IDialogProps";
// Maximum acceptable size of a key file. It's 59 characters including the spaces we encode, // Maximum acceptable size of a key file. It's 59 characters including the spaces we encode,
// so this should be plenty and allow for people putting extra whitespace in the file because // so this should be plenty and allow for people putting extra whitespace in the file because
@ -34,22 +34,30 @@ const KEY_FILE_MAX_SIZE = 128;
// Don't shout at the user that their key is invalid every time they type a key: wait a short time // Don't shout at the user that their key is invalid every time they type a key: wait a short time
const VALIDATION_THROTTLE_MS = 200; const VALIDATION_THROTTLE_MS = 200;
interface IProps extends IDialogProps {
keyInfo: ISecretStorageKeyInfo;
checkPrivateKey: (k: {passphrase?: string, recoveryKey?: string}) => boolean;
}
interface IState {
recoveryKey: string;
recoveryKeyValid: boolean | null;
recoveryKeyCorrect: boolean | null;
recoveryKeyFileError: boolean | null;
forceRecoveryKey: boolean;
passPhrase: string;
keyMatches: boolean | null;
}
/* /*
* Access Secure Secret Storage by requesting the user's passphrase. * Access Secure Secret Storage by requesting the user's passphrase.
*/ */
export default class AccessSecretStorageDialog extends React.PureComponent { export default class AccessSecretStorageDialog extends React.PureComponent<IProps, IState> {
static propTypes = { private fileUpload = React.createRef<HTMLInputElement>();
// { passphrase, pubkey }
keyInfo: PropTypes.object.isRequired,
// Function from one of { passphrase, recoveryKey } -> boolean
checkPrivateKey: PropTypes.func.isRequired,
}
constructor(props) { constructor(props) {
super(props); super(props);
this._fileUpload = React.createRef();
this.state = { this.state = {
recoveryKey: "", recoveryKey: "",
recoveryKeyValid: null, recoveryKeyValid: null,
@ -61,21 +69,21 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
}; };
} }
_onCancel = () => { private onCancel = () => {
this.props.onFinished(false); this.props.onFinished(false);
} };
_onUseRecoveryKeyClick = () => { private onUseRecoveryKeyClick = () => {
this.setState({ this.setState({
forceRecoveryKey: true, forceRecoveryKey: true,
}); });
} };
_validateRecoveryKeyOnChange = debounce(() => { private validateRecoveryKeyOnChange = debounce(async () => {
this._validateRecoveryKey(); await this.validateRecoveryKey();
}, VALIDATION_THROTTLE_MS); }, VALIDATION_THROTTLE_MS);
async _validateRecoveryKey() { private async validateRecoveryKey() {
if (this.state.recoveryKey === '') { if (this.state.recoveryKey === '') {
this.setState({ this.setState({
recoveryKeyValid: null, recoveryKeyValid: null,
@ -102,27 +110,27 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} }
} }
_onRecoveryKeyChange = (e) => { private onRecoveryKeyChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
recoveryKey: e.target.value, recoveryKey: ev.target.value,
recoveryKeyFileError: null, recoveryKeyFileError: null,
}); });
// also clear the file upload control so that the user can upload the same file // also clear the file upload control so that the user can upload the same file
// the did before (otherwise the onchange wouldn't fire) // the did before (otherwise the onchange wouldn't fire)
if (this._fileUpload.current) this._fileUpload.current.value = null; if (this.fileUpload.current) this.fileUpload.current.value = null;
// We don't use Field's validation here because a) we want it in a separate place rather // We don't use Field's validation here because a) we want it in a separate place rather
// than in a tooltip and b) we want it to display feedback based on the uploaded file // than in a tooltip and b) we want it to display feedback based on the uploaded file
// as well as the text box. Ideally we would refactor Field's validation logic so we could // as well as the text box. Ideally we would refactor Field's validation logic so we could
// re-use some of it. // re-use some of it.
this._validateRecoveryKeyOnChange(); this.validateRecoveryKeyOnChange();
} };
_onRecoveryKeyFileChange = async e => { private onRecoveryKeyFileChange = async (ev: ChangeEvent<HTMLInputElement>) => {
if (e.target.files.length === 0) return; if (ev.target.files.length === 0) return;
const f = e.target.files[0]; const f = ev.target.files[0];
if (f.size > KEY_FILE_MAX_SIZE) { if (f.size > KEY_FILE_MAX_SIZE) {
this.setState({ this.setState({
@ -140,7 +148,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
recoveryKeyFileError: null, recoveryKeyFileError: null,
recoveryKey: contents.trim(), recoveryKey: contents.trim(),
}); });
this._validateRecoveryKey(); await this.validateRecoveryKey();
} else { } else {
this.setState({ this.setState({
recoveryKeyFileError: true, recoveryKeyFileError: true,
@ -150,14 +158,14 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
}); });
} }
} }
};
private onRecoveryKeyFileUploadClick = () => {
this.fileUpload.current.click();
} }
_onRecoveryKeyFileUploadClick = () => { private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement>) => {
this._fileUpload.current.click(); ev.preventDefault();
}
_onPassPhraseNext = async (e) => {
e.preventDefault();
if (this.state.passPhrase.length <= 0) return; if (this.state.passPhrase.length <= 0) return;
@ -169,10 +177,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else { } else {
this.setState({ keyMatches }); this.setState({ keyMatches });
} }
} };
_onRecoveryKeyNext = async (e) => { private onRecoveryKeyNext = async (ev: FormEvent<HTMLFormElement>) => {
e.preventDefault(); ev.preventDefault();
if (!this.state.recoveryKeyValid) return; if (!this.state.recoveryKeyValid) return;
@ -184,16 +192,16 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} else { } else {
this.setState({ keyMatches }); this.setState({ keyMatches });
} }
} };
_onPassPhraseChange = (e) => { private onPassPhraseChange = (ev: ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
passPhrase: e.target.value, passPhrase: ev.target.value,
keyMatches: null, keyMatches: null,
}); });
} };
getKeyValidationText() { private getKeyValidationText(): string {
if (this.state.recoveryKeyFileError) { if (this.state.recoveryKeyFileError) {
return _t("Wrong file type"); return _t("Wrong file type");
} else if (this.state.recoveryKeyCorrect) { } else if (this.state.recoveryKeyCorrect) {
@ -208,7 +216,8 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
} }
render() { render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); // Caution: Making this an import will break tests.
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
const hasPassphrase = ( const hasPassphrase = (
this.props.keyInfo && this.props.keyInfo &&
@ -244,18 +253,18 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{ {
button: s => <AccessibleButton className="mx_linkButton" button: s => <AccessibleButton className="mx_linkButton"
element="span" element="span"
onClick={this._onUseRecoveryKeyClick} onClick={this.onUseRecoveryKeyClick}
> >
{s} {s}
</AccessibleButton>, </AccessibleButton>,
}, },
)}</p> )}</p>
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}> <form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this.onPassPhraseNext}>
<input <input
type="password" type="password"
className="mx_AccessSecretStorageDialog_passPhraseInput" className="mx_AccessSecretStorageDialog_passPhraseInput"
onChange={this._onPassPhraseChange} onChange={this.onPassPhraseChange}
value={this.state.passPhrase} value={this.state.passPhrase}
autoFocus={true} autoFocus={true}
autoComplete="new-password" autoComplete="new-password"
@ -264,9 +273,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{keyStatus} {keyStatus}
<DialogButtons <DialogButtons
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onPassPhraseNext} onPrimaryButtonClick={this.onPassPhraseNext}
hasCancel={true} hasCancel={true}
onCancel={this._onCancel} onCancel={this.onCancel}
focus={false} focus={false}
primaryDisabled={this.state.passPhrase.length === 0} primaryDisabled={this.state.passPhrase.length === 0}
/> />
@ -291,7 +300,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
<form <form
className="mx_AccessSecretStorageDialog_primaryContainer" className="mx_AccessSecretStorageDialog_primaryContainer"
onSubmit={this._onRecoveryKeyNext} onSubmit={this.onRecoveryKeyNext}
spellCheck={false} spellCheck={false}
autoComplete="off" autoComplete="off"
> >
@ -301,7 +310,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
type="password" type="password"
label={_t('Security Key')} label={_t('Security Key')}
value={this.state.recoveryKey} value={this.state.recoveryKey}
onChange={this._onRecoveryKeyChange} onChange={this.onRecoveryKeyChange}
forceValidity={this.state.recoveryKeyCorrect} forceValidity={this.state.recoveryKeyCorrect}
autoComplete="off" autoComplete="off"
/> />
@ -312,10 +321,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
<div> <div>
<input type="file" <input type="file"
className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput" className="mx_AccessSecretStorageDialog_recoveryKeyEntry_fileInput"
ref={this._fileUpload} ref={this.fileUpload}
onChange={this._onRecoveryKeyFileChange} onChange={this.onRecoveryKeyFileChange}
/> />
<AccessibleButton kind="primary" onClick={this._onRecoveryKeyFileUploadClick}> <AccessibleButton kind="primary" onClick={this.onRecoveryKeyFileUploadClick}>
{_t("Upload")} {_t("Upload")}
</AccessibleButton> </AccessibleButton>
</div> </div>
@ -323,11 +332,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
{recoveryKeyFeedback} {recoveryKeyFeedback}
<DialogButtons <DialogButtons
primaryButton={_t('Continue')} primaryButton={_t('Continue')}
onPrimaryButtonClick={this._onRecoveryKeyNext} onPrimaryButtonClick={this.onRecoveryKeyNext}
hasCancel={true} hasCancel={true}
cancelButton={_t("Go Back")} cancelButton={_t("Go Back")}
cancelButtonClass='danger' cancelButtonClass='danger'
onCancel={this._onCancel} onCancel={this.onCancel}
focus={false} focus={false}
primaryDisabled={!this.state.recoveryKeyValid} primaryDisabled={!this.state.recoveryKeyValid}
/> />
@ -341,9 +350,9 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
title={title} title={title}
titleClass={titleClass} titleClass={titleClass}
> >
<div> <div>
{content} {content}
</div> </div>
</BaseDialog> </BaseDialog>
); );
} }

View file

@ -0,0 +1,66 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { HTMLAttributes } from "react";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { sortBy } from "lodash";
import MemberAvatar from "../avatars/MemberAvatar";
import { _t } from "../../../languageHandler";
import DMRoomMap from "../../../utils/DMRoomMap";
import TextWithTooltip from "../elements/TextWithTooltip";
import { useRoomMembers } from "../../../hooks/useRoomMembers";
const DEFAULT_NUM_FACES = 5;
interface IProps extends HTMLAttributes<HTMLSpanElement> {
room: Room;
onlyKnownUsers?: boolean;
numShown?: number;
}
const isKnownMember = (member: RoomMember) => !!DMRoomMap.shared().getDMRoomsForUserId(member.userId)?.length;
const FacePile = ({ room, onlyKnownUsers = true, numShown = DEFAULT_NUM_FACES, ...props }: IProps) => {
let members = useRoomMembers(room);
// sort users with an explicit avatar first
const iteratees = [member => !!member.getMxcAvatarUrl()];
if (onlyKnownUsers) {
members = members.filter(isKnownMember);
} else {
// sort known users first
iteratees.unshift(member => isKnownMember(member));
}
if (members.length < 1) return null;
const shownMembers = sortBy(members, iteratees).slice(0, numShown);
return <div {...props} className="mx_FacePile">
<div className="mx_FacePile_faces">
{ shownMembers.map(member => {
return <TextWithTooltip key={member.userId} tooltip={member.name}>
<MemberAvatar member={member} width={28} height={28} />
</TextWithTooltip>;
}) }
</div>
{ onlyKnownUsers && <span>
{ _t("%(count)s people you know have already joined", { count: members.length }) }
</span> }
</div>
};
export default FacePile;

View file

@ -73,7 +73,7 @@ const SSOButton: React.FC<ISSOButtonProps> = ({
brandClass = `mx_SSOButton_brand_${brandName}`; brandClass = `mx_SSOButton_brand_${brandName}`;
icon = <img src={brandIcon} height="24" width="24" alt={brandName} />; icon = <img src={brandIcon} height="24" width="24" alt={brandName} />;
} else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) { } else if (typeof idp?.icon === "string" && idp.icon.startsWith("mxc://")) {
const src = mediaFromMxc(idp.icon).getSquareThumbnailHttp(24); const src = mediaFromMxc(idp.icon, matrixClient).getSquareThumbnailHttp(24);
icon = <img src={src} height="24" width="24" alt={idp.name} />; icon = <img src={src} height="24" width="24" alt={idp.name} />;
} }

View file

@ -216,12 +216,12 @@ export default class TextualBody extends React.Component {
} }
_addLineNumbers(pre) { _addLineNumbers(pre) {
// Calculate number of lines in pre
const number = pre.innerHTML.replace(/\n(<\/code>)?$/, "").split(/\n/).length;
pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>'; pre.innerHTML = '<span class="mx_EventTile_lineNumbers"></span>' + pre.innerHTML + '<span></span>';
const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0]; const lineNumbers = pre.getElementsByClassName("mx_EventTile_lineNumbers")[0];
// Calculate number of lines in pre
const number = pre.innerHTML.split(/\n/).length;
// Iterate through lines starting with 1 (number of the first line is 1) // Iterate through lines starting with 1 (number of the first line is 1)
for (let i = 1; i < number; i++) { for (let i = 1; i <= number; i++) {
lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>'; lineNumbers.innerHTML += '<span class="mx_EventTile_lineNumber">' + i + '</span>';
} }
} }

View file

@ -52,7 +52,7 @@ const EncryptionInfo: React.FC<IProps> = ({
let text: string; let text: string;
if (waitingForOtherParty) { if (waitingForOtherParty) {
if (isSelfVerification) { if (isSelfVerification) {
text = _t("Waiting for you to accept on your other session…"); text = _t("Accept on your other login…");
} else { } else {
text = _t("Waiting for %(displayName)s to accept…", { text = _t("Waiting for %(displayName)s to accept…", {
displayName: member.displayName || member.name || member.userId, displayName: member.displayName || member.name || member.userId,

View file

@ -46,6 +46,7 @@ import {IDiff} from "../../../editor/diff";
import AutocompleteWrapperModel from "../../../editor/autocomplete"; import AutocompleteWrapperModel from "../../../editor/autocomplete";
import DocumentPosition from "../../../editor/position"; import DocumentPosition from "../../../editor/position";
import {ICompletion} from "../../../autocomplete/Autocompleter"; import {ICompletion} from "../../../autocomplete/Autocompleter";
import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
// matches emoticons which follow the start of a line or whitespace // matches emoticons which follow the start of a line or whitespace
@ -422,98 +423,94 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
private onKeyDown = (event: React.KeyboardEvent) => { private onKeyDown = (event: React.KeyboardEvent) => {
const model = this.props.model; const model = this.props.model;
const modKey = IS_MAC ? event.metaKey : event.ctrlKey;
let handled = false; let handled = false;
// format bold const action = getKeyBindingsManager().getMessageComposerAction(event);
if (modKey && event.key === Key.B) { switch (action) {
this.onFormatAction(Formatting.Bold); case MessageComposerAction.FormatBold:
handled = true; this.onFormatAction(Formatting.Bold);
// format italics
} else if (modKey && event.key === Key.I) {
this.onFormatAction(Formatting.Italics);
handled = true;
// format quote
} else if (modKey && event.key === Key.GREATER_THAN) {
this.onFormatAction(Formatting.Quote);
handled = true;
// redo
} else if ((!IS_MAC && modKey && event.key === Key.Y) ||
(IS_MAC && modKey && event.shiftKey && event.key === Key.Z)) {
if (this.historyManager.canRedo()) {
const {parts, caret} = this.historyManager.redo();
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyRedo");
}
handled = true;
// undo
} else if (modKey && event.key === Key.Z) {
if (this.historyManager.canUndo()) {
const {parts, caret} = this.historyManager.undo(this.props.model);
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyUndo");
}
handled = true;
// insert newline on Shift+Enter
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
this.insertText("\n");
handled = true;
// move selection to start of composer
} else if (modKey && event.key === Key.HOME && !event.shiftKey) {
setSelection(this.editorRef.current, model, {
index: 0,
offset: 0,
});
handled = true;
// move selection to end of composer
} else if (modKey && event.key === Key.END && !event.shiftKey) {
setSelection(this.editorRef.current, model, {
index: model.parts.length - 1,
offset: model.parts[model.parts.length - 1].text.length,
});
handled = true;
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
} else {
const metaOrAltPressed = event.metaKey || event.altKey;
const modifierPressed = metaOrAltPressed || event.shiftKey;
if (model.autoComplete && model.autoComplete.hasCompletions()) {
const autoComplete = model.autoComplete;
switch (event.key) {
case Key.ARROW_UP:
if (!modifierPressed) {
autoComplete.onUpArrow(event);
handled = true;
}
break;
case Key.ARROW_DOWN:
if (!modifierPressed) {
autoComplete.onDownArrow(event);
handled = true;
}
break;
case Key.TAB:
if (!metaOrAltPressed) {
autoComplete.onTab(event);
handled = true;
}
break;
case Key.ESCAPE:
if (!modifierPressed) {
autoComplete.onEscape(event);
handled = true;
}
break;
default:
return; // don't preventDefault on anything else
}
} else if (event.key === Key.TAB) {
this.tabCompleteName(event);
handled = true; handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) { break;
this.formatBarRef.current.hide(); case MessageComposerAction.FormatItalics:
} this.onFormatAction(Formatting.Italics);
handled = true;
break;
case MessageComposerAction.FormatQuote:
this.onFormatAction(Formatting.Quote);
handled = true;
break;
case MessageComposerAction.EditRedo:
if (this.historyManager.canRedo()) {
const {parts, caret} = this.historyManager.redo();
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyRedo");
}
handled = true;
break;
case MessageComposerAction.EditUndo:
if (this.historyManager.canUndo()) {
const {parts, caret} = this.historyManager.undo(this.props.model);
// pass matching inputType so historyManager doesn't push echo
// when invoked from rerender callback.
model.reset(parts, caret, "historyUndo");
}
handled = true;
break;
case MessageComposerAction.NewLine:
this.insertText("\n");
handled = true;
break;
case MessageComposerAction.MoveCursorToStart:
setSelection(this.editorRef.current, model, {
index: 0,
offset: 0,
});
handled = true;
break;
case MessageComposerAction.MoveCursorToEnd:
setSelection(this.editorRef.current, model, {
index: model.parts.length - 1,
offset: model.parts[model.parts.length - 1].text.length,
});
handled = true;
break;
} }
if (handled) {
event.preventDefault();
event.stopPropagation();
return;
}
const autocompleteAction = getKeyBindingsManager().getAutocompleteAction(event);
if (model.autoComplete && model.autoComplete.hasCompletions()) {
const autoComplete = model.autoComplete;
switch (autocompleteAction) {
case AutocompleteAction.PrevSelection:
autoComplete.onUpArrow(event);
handled = true;
break;
case AutocompleteAction.NextSelection:
autoComplete.onDownArrow(event);
handled = true;
break;
case AutocompleteAction.ApplySelection:
autoComplete.onTab(event);
handled = true;
break;
case AutocompleteAction.Cancel:
autoComplete.onEscape(event);
handled = true;
break;
default:
return; // don't preventDefault on anything else
}
} else if (autocompleteAction === AutocompleteAction.ApplySelection) {
this.tabCompleteName(event);
handled = true;
} else if (event.key === Key.BACKSPACE || event.key === Key.DELETE) {
this.formatBarRef.current.hide();
}
if (handled) { if (handled) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

View file

@ -29,11 +29,10 @@ 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 {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
function _isReply(mxEvent) { function _isReply(mxEvent) {
@ -136,38 +135,41 @@ export default class EditMessageComposer extends React.Component {
if (this._editorRef.isComposing(event)) { if (this._editorRef.isComposing(event)) {
return; return;
} }
if (event.metaKey || event.altKey || event.shiftKey) { const action = getKeyBindingsManager().getMessageComposerAction(event);
return; switch (action) {
} case MessageComposerAction.Send:
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); this._sendEdit();
const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
: event.key === Key.ENTER;
if (send) {
this._sendEdit();
event.preventDefault();
} else if (event.key === Key.ESCAPE) {
this._cancelEdit();
} else if (event.key === Key.ARROW_UP) {
if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) {
return;
}
const previousEvent = findEditableEvent(this._getRoom(), false, this.props.editState.getEvent().getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault(); event.preventDefault();
break;
case MessageComposerAction.CancelEditing:
this._cancelEdit();
break;
case MessageComposerAction.EditPrevMessage: {
if (this._editorRef.isModified() || !this._editorRef.isCaretAtStart()) {
return;
}
const previousEvent = findEditableEvent(this._getRoom(), false,
this.props.editState.getEvent().getId());
if (previousEvent) {
dis.dispatch({action: 'edit_event', event: previousEvent});
event.preventDefault();
}
break;
} }
} else if (event.key === Key.ARROW_DOWN) { case MessageComposerAction.EditNextMessage: {
if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) { if (this._editorRef.isModified() || !this._editorRef.isCaretAtEnd()) {
return; return;
}
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.fire(Action.FocusComposer);
}
event.preventDefault();
break;
} }
const nextEvent = findEditableEvent(this._getRoom(), true, this.props.editState.getEvent().getId());
if (nextEvent) {
dis.dispatch({action: 'edit_event', event: nextEvent});
} else {
dis.dispatch({action: 'edit_event', event: null});
dis.fire(Action.FocusComposer);
}
event.preventDefault();
} }
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -28,6 +28,8 @@ import defaultDispatcher from "../../../dispatcher/dispatcher";
import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload"; import {ViewUserPayload} from "../../../dispatcher/payloads/ViewUserPayload";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SpaceStore from "../../../stores/SpaceStore";
import {showSpaceInvite} from "../../../utils/space";
const NewRoomIntro = () => { const NewRoomIntro = () => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
@ -100,17 +102,48 @@ const NewRoomIntro = () => {
}); });
} }
let buttons; let parentSpace;
if (room.canInvite(cli.getUserId())) { if (
const onInviteClick = () => { SpaceStore.instance.activeSpace?.canInvite(cli.getUserId()) &&
dis.dispatch({ action: "view_invite", roomId }); SpaceStore.instance.getSpaceFilteredRoomIds(SpaceStore.instance.activeSpace).has(room.roomId)
}; ) {
parentSpace = SpaceStore.instance.activeSpace;
}
let buttons;
if (parentSpace) {
buttons = <div className="mx_NewRoomIntro_buttons"> buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton className="mx_NewRoomIntro_inviteButton" kind="primary" onClick={onInviteClick}> <AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
showSpaceInvite(parentSpace);
}}
>
{_t("Invite to %(spaceName)s", { spaceName: parentSpace.name })}
</AccessibleButton>
{ room.canInvite(cli.getUserId()) && <AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary_outline"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to just this room")}
</AccessibleButton> }
</div>;
} else if (room.canInvite(cli.getUserId())) {
buttons = <div className="mx_NewRoomIntro_buttons">
<AccessibleButton
className="mx_NewRoomIntro_inviteButton"
kind="primary"
onClick={() => {
dis.dispatch({ action: "view_invite", roomId });
}}
>
{_t("Invite to this room")} {_t("Invite to this room")}
</AccessibleButton> </AccessibleButton>
</div> </div>;
} }
const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url; const avatarUrl = room.currentState.getStateEvents(EventType.RoomAvatar, "")?.getContent()?.url;

View file

@ -20,6 +20,7 @@ import React, { ReactComponentElement } from "react";
import { Dispatcher } from "flux"; import { Dispatcher } from "flux";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import * as fbEmitter from "fbemitter"; import * as fbEmitter from "fbemitter";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { _t, _td } from "../../../languageHandler"; import { _t, _td } from "../../../languageHandler";
import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex";
@ -48,9 +49,8 @@ import { IconizedContextMenuOption, IconizedContextMenuOptionList } from "../con
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore";
import CallHandler from "../../../CallHandler"; import CallHandler from "../../../CallHandler";
import SpaceStore, { SUGGESTED_ROOMS } from "../../../stores/SpaceStore"; import SpaceStore, {SUGGESTED_ROOMS} from "../../../stores/SpaceStore";
import { showAddExistingRooms, showCreateNewRoom } from "../../../utils/space"; import {showAddExistingRooms, showCreateNewRoom, showSpaceInvite} from "../../../utils/space";
import { EventType } from "matrix-js-sdk/src/@types/event";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory"; import { ISpaceSummaryRoom } from "../../structures/SpaceRoomDirectory";
@ -62,6 +62,7 @@ interface IProps {
onResize: () => void; onResize: () => void;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
isMinimized: boolean; isMinimized: boolean;
activeSpace: Room;
} }
interface IState { interface IState {
@ -194,8 +195,8 @@ const TAG_AESTHETICS: ITagAestheticsMap = {
: _t("You do not have permissions to add rooms to this space")} : _t("You do not have permissions to add rooms to this space")}
/> />
<IconizedContextMenuOption <IconizedContextMenuOption
label={_t("Explore space rooms")} label={_t("Explore rooms")}
iconClassName="mx_RoomList_iconExplore" iconClassName="mx_RoomList_iconBrowse"
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -424,6 +425,11 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
dis.dispatch({ action: Action.ViewRoomDirectory, initialText }); dis.dispatch({ action: Action.ViewRoomDirectory, initialText });
}; };
private onSpaceInviteClick = () => {
const initialText = RoomListStore.instance.getFirstNameFilterCondition()?.search;
showSpaceInvite(this.props.activeSpace, initialText);
};
private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] { private renderSuggestedRooms(): ReactComponentElement<typeof ExtraTile>[] {
return this.state.suggestedRooms.map(room => { return this.state.suggestedRooms.map(room => {
const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room"); const name = room.name || room.canonical_alias || room.aliases.pop() || _t("Empty room");
@ -569,7 +575,23 @@ export default class RoomList extends React.PureComponent<IProps, IState> {
kind="link" kind="link"
onClick={this.onExplore} onClick={this.onExplore}
> >
{_t("Explore all public rooms")} { this.props.activeSpace ? _t("Explore rooms") : _t("Explore all public rooms") }
</AccessibleButton>
</div>;
} else if (this.props.activeSpace) {
explorePrompt = <div className="mx_RoomList_explorePrompt">
<div>{ _t("Quick actions") }</div>
{ this.props.activeSpace.canInvite(MatrixClientPeg.get().getUserId()) && <AccessibleButton
className="mx_RoomList_explorePrompt_spaceInvite"
onClick={this.onSpaceInviteClick}
>
{_t("Invite people")}
</AccessibleButton> }
<AccessibleButton
className="mx_RoomList_explorePrompt_spaceExplore"
onClick={this.onExplore}
>
{_t("Explore rooms")}
</AccessibleButton> </AccessibleButton>
</div>; </div>;
} else if (Object.values(this.state.sublists).some(list => list.length > 0)) { } else if (Object.values(this.state.sublists).some(list => list.length > 0)) {

View file

@ -51,6 +51,7 @@ import { objectExcluding, objectHasDiff } from "../../../utils/objects";
import ExtraTile from "./ExtraTile"; import ExtraTile from "./ExtraTile";
import { ListNotificationState } from "../../../stores/notifications/ListNotificationState"; import { ListNotificationState } from "../../../stores/notifications/ListNotificationState";
import IconizedContextMenu from "../context_menus/IconizedContextMenu"; import IconizedContextMenu from "../context_menus/IconizedContextMenu";
import { getKeyBindingsManager, RoomListAction } from "../../../KeyBindingsManager";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS const SHOW_N_BUTTON_HEIGHT = 28; // As defined by CSS
@ -470,18 +471,19 @@ export default class RoomSublist extends React.Component<IProps, IState> {
}; };
private onHeaderKeyDown = (ev: React.KeyboardEvent) => { private onHeaderKeyDown = (ev: React.KeyboardEvent) => {
switch (ev.key) { const action = getKeyBindingsManager().getRoomListAction(ev);
case Key.ARROW_LEFT: switch (action) {
case RoomListAction.CollapseSection:
ev.stopPropagation(); ev.stopPropagation();
if (this.state.isExpanded) { if (this.state.isExpanded) {
// On ARROW_LEFT collapse the room sublist if it isn't already // Collapse the room sublist if it isn't already
this.toggleCollapsed(); this.toggleCollapsed();
} }
break; break;
case Key.ARROW_RIGHT: { case RoomListAction.ExpandSection: {
ev.stopPropagation(); ev.stopPropagation();
if (!this.state.isExpanded) { if (!this.state.isExpanded) {
// On ARROW_RIGHT expand the room sublist if it isn't already // Expand the room sublist if it isn't already
this.toggleCollapsed(); this.toggleCollapsed();
} else if (this.sublistRef.current) { } else if (this.sublistRef.current) {
// otherwise focus the first room // otherwise focus the first room

View file

@ -38,17 +38,17 @@ import * as sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {_t, _td} from '../../../languageHandler'; import {_t, _td} from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RateLimitedFunc from '../../../ratelimitedfunc'; import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {containsEmoji} from "../../../effects/utils"; import {containsEmoji} from "../../../effects/utils";
import {CHAT_EFFECTS} from '../../../effects'; import {CHAT_EFFECTS} from '../../../effects';
import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import EMOJI_REGEX from 'emojibase-regex'; import EMOJI_REGEX from 'emojibase-regex';
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import SettingsStore from '../../../settings/SettingsStore';
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent);
@ -148,60 +148,50 @@ export default class SendMessageComposer extends React.Component {
if (this._editorRef.isComposing(event)) { if (this._editorRef.isComposing(event)) {
return; return;
} }
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; const action = getKeyBindingsManager().getMessageComposerAction(event);
const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend'); switch (action) {
const send = ctrlEnterToSend case MessageComposerAction.Send:
? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event) this._sendMessage();
: event.key === Key.ENTER && !hasModifier; event.preventDefault();
if (send) { break;
this._sendMessage(); case MessageComposerAction.SelectPrevSendHistory:
event.preventDefault(); case MessageComposerAction.SelectNextSendHistory: {
} else if (event.key === Key.ARROW_UP) { // Try select composer history
this.onVerticalArrow(event, true); const selected = this.selectSendHistory(action === MessageComposerAction.SelectPrevSendHistory);
} else if (event.key === Key.ARROW_DOWN) { if (selected) {
this.onVerticalArrow(event, false); // We're selecting history, so prevent the key event from doing anything else
} else if (event.key === Key.ESCAPE) { event.preventDefault();
dis.dispatch({ }
action: 'reply_to_event', break;
event: null, }
}); case MessageComposerAction.EditPrevMessage:
} else if (this._prepareToEncrypt) { // selection must be collapsed and caret at start
// This needs to be last! if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
this._prepareToEncrypt(); const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
event.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
}
break;
case MessageComposerAction.CancelEditing:
dis.dispatch({
action: 'reply_to_event',
event: null,
});
break;
default:
if (this._prepareToEncrypt) {
// This needs to be last!
this._prepareToEncrypt();
}
} }
}; };
onVerticalArrow(e, up) {
// arrows from an initial-caret composer navigates recent messages to edit
// ctrl-alt-arrows navigate send history
if (e.shiftKey || e.metaKey) return;
const shouldSelectHistory = e.altKey && e.ctrlKey;
const shouldEditLastMessage = !e.altKey && !e.ctrlKey && up && !this.props.replyToEvent;
if (shouldSelectHistory) {
// Try select composer history
const selected = this.selectSendHistory(up);
if (selected) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
}
} else if (shouldEditLastMessage) {
// selection must be collapsed and caret at start
if (this._editorRef.isSelectionCollapsed() && this._editorRef.isCaretAtStart()) {
const editEvent = findEditableEvent(this.props.room, false);
if (editEvent) {
// We're selecting history, so prevent the key event from doing anything else
e.preventDefault();
dis.dispatch({
action: 'edit_event',
event: editEvent,
});
}
}
}
}
// we keep sent messages/commands in a separate history (separate from undo history) // we keep sent messages/commands in a separate history (separate from undo history)
// so you can alt+up/down in them // so you can alt+up/down in them
selectSendHistory(up) { selectSendHistory(up) {
@ -266,7 +256,7 @@ export default class SendMessageComposer extends React.Component {
const myReactionKeys = [...myReactionEvents] const myReactionKeys = [...myReactionEvents]
.filter(event => !event.isRedacted()) .filter(event => !event.isRedacted())
.map(event => event.getRelation().key); .map(event => event.getRelation().key);
shouldReact = !myReactionKeys.includes(reaction); shouldReact = !myReactionKeys.includes(reaction);
} }
if (shouldReact) { if (shouldReact) {
MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", { MatrixClientPeg.get().sendEvent(lastMessage.getRoomId(), "m.reaction", {
@ -472,12 +462,17 @@ export default class SendMessageComposer extends React.Component {
} }
} }
// should save state when editor has contents or reply is open
_shouldSaveStoredEditorState = () => {
return !this.model.isEmpty || this.props.replyToEvent;
}
_saveStoredEditorState = () => { _saveStoredEditorState = () => {
if (this.model.isEmpty) { if (this._shouldSaveStoredEditorState()) {
this._clearStoredEditorState();
} else {
const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent); const item = SendHistoryManager.createItem(this.model, this.props.replyToEvent);
localStorage.setItem(this._editorStateKey, JSON.stringify(item)); localStorage.setItem(this._editorStateKey, JSON.stringify(item));
} else {
this._clearStoredEditorState();
} }
} }
@ -521,7 +516,7 @@ export default class SendMessageComposer extends React.Component {
_insertQuotedMessage(event) { _insertQuotedMessage(event) {
const {model} = this; const {model} = this;
const {partCreator} = model; const {partCreator} = model;
const quoteParts = parseEvent(event, partCreator, { isQuotedMessage: true }); const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true});
// add two newlines // add two newlines
quoteParts.push(partCreator.newline()); quoteParts.push(partCreator.newline());
quoteParts.push(partCreator.newline()); quoteParts.push(partCreator.newline());

View file

@ -190,7 +190,7 @@ export default class EventIndexPanel extends React.Component {
} }
</div> </div>
); );
} else { } else if (!EventIndexPeg.platformHasSupport()) {
eventIndexingSettings = ( eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'> <div className='mx_SettingsTab_subsectionText'>
{ {
@ -208,6 +208,23 @@ export default class EventIndexPanel extends React.Component {
} }
</div> </div>
); );
} else {
eventIndexingSettings = (
<div className='mx_SettingsTab_subsectionText'>
<p>
{_t("Message search initilisation failed")}
</p>
{EventIndexPeg.error && (
<details>
<summary>{_t("Advanced")}</summary>
<code>
{EventIndexPeg.error.message}
</code>
</details>
)}
</div>
);
} }
return eventIndexingSettings; return eventIndexingSettings;

View file

@ -206,10 +206,10 @@ export default class GeneralUserSettingsTab extends React.Component {
_onPasswordChangeError = (err) => { _onPasswordChangeError = (err) => {
// TODO: Figure out a design that doesn't involve replacing the current dialog // TODO: Figure out a design that doesn't involve replacing the current dialog
let errMsg = err.error || ""; let errMsg = err.error || err.message || "";
if (err.httpStatus === 403) { if (err.httpStatus === 403) {
errMsg = _t("Failed to change password. Is your password correct?"); errMsg = _t("Failed to change password. Is your password correct?");
} else if (err.httpStatus) { } else if (!errMsg) {
errMsg += ` (HTTP status ${err.httpStatus})`; errMsg += ` (HTTP status ${err.httpStatus})`;
} }
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");

View file

@ -74,6 +74,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
this.state = { this.state = {
autoLaunch: false, autoLaunch: false,
autoLaunchSupported: false, autoLaunchSupported: false,
warnBeforeExit: true,
warnBeforeExitSupported: false,
alwaysShowMenuBar: true, alwaysShowMenuBar: true,
alwaysShowMenuBarSupported: false, alwaysShowMenuBarSupported: false,
minimizeToTray: true, minimizeToTray: true,
@ -96,6 +98,12 @@ export default class PreferencesUserSettingsTab extends React.Component {
autoLaunch = await platform.getAutoLaunchEnabled(); autoLaunch = await platform.getAutoLaunchEnabled();
} }
const warnBeforeExitSupported = await platform.supportsWarnBeforeExit();
let warnBeforeExit = false;
if (warnBeforeExitSupported) {
warnBeforeExit = await platform.shouldWarnBeforeExit();
}
const alwaysShowMenuBarSupported = await platform.supportsAutoHideMenuBar(); const alwaysShowMenuBarSupported = await platform.supportsAutoHideMenuBar();
let alwaysShowMenuBar = true; let alwaysShowMenuBar = true;
if (alwaysShowMenuBarSupported) { if (alwaysShowMenuBarSupported) {
@ -111,6 +119,8 @@ export default class PreferencesUserSettingsTab extends React.Component {
this.setState({ this.setState({
autoLaunch, autoLaunch,
autoLaunchSupported, autoLaunchSupported,
warnBeforeExit,
warnBeforeExitSupported,
alwaysShowMenuBarSupported, alwaysShowMenuBarSupported,
alwaysShowMenuBar, alwaysShowMenuBar,
minimizeToTraySupported, minimizeToTraySupported,
@ -122,6 +132,10 @@ export default class PreferencesUserSettingsTab extends React.Component {
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked})); PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({autoLaunch: checked}));
}; };
_onWarnBeforeExitChange = (checked) => {
PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({warnBeforeExit: checked}));
}
_onAlwaysShowMenuBarChange = (checked) => { _onAlwaysShowMenuBarChange = (checked) => {
PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked})); PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({alwaysShowMenuBar: checked}));
}; };
@ -161,6 +175,14 @@ export default class PreferencesUserSettingsTab extends React.Component {
label={_t('Start automatically after system login')} />; label={_t('Start automatically after system login')} />;
} }
let warnBeforeExitOption = null;
if (this.state.warnBeforeExitSupported) {
warnBeforeExitOption = <LabelledToggleSwitch
value={this.state.warnBeforeExit}
onChange={this._onWarnBeforeExitChange}
label={_t('Warn before quitting')} />;
}
let autoHideMenuOption = null; let autoHideMenuOption = null;
if (this.state.alwaysShowMenuBarSupported) { if (this.state.alwaysShowMenuBarSupported) {
autoHideMenuOption = <LabelledToggleSwitch autoHideMenuOption = <LabelledToggleSwitch
@ -202,6 +224,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
{minimizeToTrayOption} {minimizeToTrayOption}
{autoHideMenuOption} {autoHideMenuOption}
{autoLaunchOption} {autoLaunchOption}
{warnBeforeExitOption}
<Field <Field
label={_t('Autocomplete delay (ms)')} label={_t('Autocomplete delay (ms)')}
type='number' type='number'

View file

@ -21,7 +21,6 @@ import {EventType, RoomType, RoomCreateTypeField} from "matrix-js-sdk/src/@types
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import {ChevronFace, ContextMenu} from "../../structures/ContextMenu"; import {ChevronFace, ContextMenu} from "../../structures/ContextMenu";
import FormButton from "../elements/FormButton";
import createRoom, {IStateEvent, Preset} from "../../../createRoom"; import createRoom, {IStateEvent, Preset} from "../../../createRoom";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import SpaceBasicSettings from "./SpaceBasicSettings"; import SpaceBasicSettings from "./SpaceBasicSettings";
@ -89,6 +88,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
power_level_content_override: { power_level_content_override: {
// Only allow Admins to write to the timeline to prevent hidden sync spam // Only allow Admins to write to the timeline to prevent hidden sync spam
events_default: 100, events_default: 100,
...Visibility.Public ? { invite: 0 } : {},
}, },
}, },
spinner: false, spinner: false,
@ -148,11 +148,9 @@ const SpaceCreateMenu = ({ onFinished }) => {
<SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} /> <SpaceBasicSettings setAvatar={setAvatar} name={name} setName={setName} topic={topic} setTopic={setTopic} />
<FormButton <AccessibleButton kind="primary" onClick={onSpaceCreateClick} disabled={!name || busy}>
label={busy ? _t("Creating...") : _t("Create")} { busy ? _t("Creating...") : _t("Create") }
onClick={onSpaceCreateClick} </AccessibleButton>
disabled={!name && !busy}
/>
</React.Fragment>; </React.Fragment>;
} }

View file

@ -26,7 +26,7 @@ import {showRoomInviteDialog} from "../../../RoomInvite";
interface IProps { interface IProps {
space: Room; space: Room;
onFinished(): void; onFinished?(): void;
} }
const SpacePublicShare = ({ space, onFinished }: IProps) => { const SpacePublicShare = ({ space, onFinished }: IProps) => {
@ -54,7 +54,7 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => {
className="mx_SpacePublicShare_inviteButton" className="mx_SpacePublicShare_inviteButton"
onClick={() => { onClick={() => {
showRoomInviteDialog(space.roomId); showRoomInviteDialog(space.roomId);
onFinished(); if (onFinished) onFinished();
}} }}
> >
<h3>{ _t("Invite people") }</h3> <h3>{ _t("Invite people") }</h3>

View file

@ -30,20 +30,21 @@ import IconizedContextMenu, {
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton"; import {ContextMenuTooltipButton} from "../../../accessibility/context_menu/ContextMenuTooltipButton";
import {toRightOf} from "../../structures/ContextMenu"; import {toRightOf} from "../../structures/ContextMenu";
import {shouldShowSpaceSettings, showCreateNewRoom, showSpaceSettings} from "../../../utils/space"; import {
shouldShowSpaceSettings,
showAddExistingRooms,
showCreateNewRoom,
showSpaceInvite,
showSpaceSettings,
} from "../../../utils/space";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {ButtonEvent} from "../elements/AccessibleButton"; import AccessibleButton, {ButtonEvent} from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import Modal from "../../../Modal";
import SpacePublicShare from "./SpacePublicShare";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import RoomViewStore from "../../../stores/RoomViewStore"; import RoomViewStore from "../../../stores/RoomViewStore";
import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload"; import {SetRightPanelPhasePayload} from "../../../dispatcher/payloads/SetRightPanelPhasePayload";
import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; import {RightPanelPhases} from "../../../stores/RightPanelStorePhases";
import {showRoomInviteDialog} from "../../../RoomInvite";
import InfoDialog from "../dialogs/InfoDialog";
import {EventType} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
import SpaceRoomDirectory from "../../structures/SpaceRoomDirectory";
interface IItemProps { interface IItemProps {
space?: Room; space?: Room;
@ -110,36 +111,11 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.setState({contextMenuPosition: null}); this.setState({contextMenuPosition: null});
}; };
private onHomeClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
defaultDispatcher.dispatch({
action: "view_room",
room_id: this.props.space.roomId,
});
this.setState({contextMenuPosition: null}); // also close the menu
};
private onInviteClick = (ev: ButtonEvent) => { private onInviteClick = (ev: ButtonEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
if (this.props.space.getJoinRule() === "public") { showSpaceInvite(this.props.space);
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite members"),
description: <React.Fragment>
<span>{ _t("Share your public space") }</span>
<SpacePublicShare space={this.props.space} onFinished={() => modal.close()} />
</React.Fragment>,
fixedWidth: false,
button: false,
className: "mx_SpacePanel_sharePublicSpace",
hasCloseButton: true,
});
} else {
showRoomInviteDialog(this.props.space.roomId);
}
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
@ -170,6 +146,14 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
private onAddExistingRoomClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
showAddExistingRooms(this.context, this.props.space);
this.setState({contextMenuPosition: null}); // also close the menu
};
private onMembersClick = (ev: ButtonEvent) => { private onMembersClick = (ev: ButtonEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -193,9 +177,10 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
Modal.createTrackedDialog("Space room directory", "Space panel", SpaceRoomDirectory, { defaultDispatcher.dispatch({
space: this.props.space, action: "view_room",
}, "mx_SpaceRoomDirectory_dialogWrapper", false, true); room_id: this.props.space.roomId,
});
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
@ -236,15 +221,22 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
</IconizedContextMenuOptionList>; </IconizedContextMenuOptionList>;
} }
let newRoomOption; const canAddRooms = this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId);
let newRoomSection;
if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) { if (this.props.space.currentState.maySendStateEvent(EventType.SpaceChild, userId)) {
newRoomOption = ( newRoomSection = <IconizedContextMenuOptionList first>
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconPlus" iconClassName="mx_SpacePanel_iconPlus"
label={_t("New room")} label={_t("Create new room")}
onClick={this.onNewRoomClick} onClick={this.onNewRoomClick}
/> />
); <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHash"
label={_t("Add existing room")}
onClick={this.onAddExistingRoomClick}
/>
</IconizedContextMenuOptionList>;
} }
contextMenu = <IconizedContextMenu contextMenu = <IconizedContextMenu
@ -258,11 +250,6 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
</div> </div>
<IconizedContextMenuOptionList first> <IconizedContextMenuOptionList first>
{ inviteOption } { inviteOption }
<IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconHome"
label={_t("Space Home")}
onClick={this.onHomeClick}
/>
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconMembers" iconClassName="mx_SpacePanel_iconMembers"
label={_t("Members")} label={_t("Members")}
@ -271,11 +258,11 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
{ settingsOption } { settingsOption }
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_SpacePanel_iconExplore" iconClassName="mx_SpacePanel_iconExplore"
label={_t("Explore rooms")} label={canAddRooms ? _t("Manage & explore rooms") : _t("Explore rooms")}
onClick={this.onExploreRoomsClick} onClick={this.onExploreRoomsClick}
/> />
{ newRoomOption }
</IconizedContextMenuOptionList> </IconizedContextMenuOptionList>
{ newRoomSection }
{ leaveSection } { leaveSection }
</IconizedContextMenu>; </IconizedContextMenu>;
} }
@ -335,7 +322,7 @@ export class SpaceItem extends React.PureComponent<IItemProps, IItemState> {
const avatarSize = isNested ? 24 : 32; const avatarSize = isNested ? 24 : 32;
const toggleCollapseButton = childSpaces && childSpaces.length ? const toggleCollapseButton = childSpaces && childSpaces.length ?
<button <AccessibleButton
className="mx_SpaceButton_toggleCollapse" className="mx_SpaceButton_toggleCollapse"
onClick={evt => this.toggleCollapse(evt)} onClick={evt => this.toggleCollapse(evt)}
/> : null; /> : null;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ import {XOR} from "../../../@types/common";
export interface IProps { export interface IProps {
description: ReactNode; description: ReactNode;
detail?: ReactNode;
acceptLabel: string; acceptLabel: string;
onAccept(); onAccept();
@ -33,14 +34,20 @@ interface IPropsExtended extends IProps {
const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({ const GenericToast: React.FC<XOR<IPropsExtended, IProps>> = ({
description, description,
detail,
acceptLabel, acceptLabel,
rejectLabel, rejectLabel,
onAccept, onAccept,
onReject, onReject,
}) => { }) => {
const detailContent = detail ? <div className="mx_Toast_detail">
{detail}
</div> : null;
return <div> return <div>
<div className="mx_Toast_description"> <div className="mx_Toast_description">
{ description } {description}
{detailContent}
</div> </div>
<div className="mx_Toast_buttons" aria-live="off"> <div className="mx_Toast_buttons" aria-live="off">
{onReject && rejectLabel && <FormButton label={rejectLabel} kind="danger" onClick={onReject} /> } {onReject && rejectLabel && <FormButton label={rejectLabel} kind="danger" onClick={onReject} /> }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2019 The Matrix.org Foundation C.I.C. Copyright 2019-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -140,11 +140,12 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
render() { render() {
const {request} = this.props; const {request} = this.props;
let nameLabel; let description;
let detail;
if (request.isSelfVerification) { if (request.isSelfVerification) {
if (this.state.device) { if (this.state.device) {
nameLabel = _t("From %(deviceName)s (%(deviceId)s) at %(ip)s", { description = this.state.device.getDisplayName();
deviceName: this.state.device.getDisplayName(), detail = _t("%(deviceId)s from %(ip)s", {
deviceId: this.state.device.deviceId, deviceId: this.state.device.deviceId,
ip: this.state.ip, ip: this.state.ip,
}); });
@ -152,13 +153,13 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
} else { } else {
const userId = request.otherUserId; const userId = request.otherUserId;
const roomId = request.channel.roomId; const roomId = request.channel.roomId;
nameLabel = roomId ? userLabelForEventRoom(userId, roomId) : userId; description = roomId ? userLabelForEventRoom(userId, roomId) : userId;
// for legacy to_device verification requests // for legacy to_device verification requests
if (nameLabel === userId) { if (description === userId) {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const user = client.getUser(userId); const user = client.getUser(userId);
if (user && user.displayName) { if (user && user.displayName) {
nameLabel = _t("%(name)s (%(userId)s)", {name: user.displayName, userId}); description = _t("%(name)s (%(userId)s)", {name: user.displayName, userId});
} }
} }
} }
@ -167,7 +168,8 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
_t("Decline (%(counter)s)", {counter: this.state.counter}); _t("Decline (%(counter)s)", {counter: this.state.counter});
return <GenericToast return <GenericToast
description={nameLabel} description={description}
detail={detail}
acceptLabel={_t("Accept")} acceptLabel={_t("Accept")}
onAccept={this.accept} onAccept={this.accept}
rejectLabel={declineLabel} rejectLabel={declineLabel}

View file

@ -17,6 +17,7 @@
import {MatrixClientPeg} from "../MatrixClientPeg"; import {MatrixClientPeg} from "../MatrixClientPeg";
import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent"; import {IMediaEventContent, IPreparedMedia, prepEventContentAsMedia} from "./models/IMediaEventContent";
import {ResizeMethod} from "../Avatar"; import {ResizeMethod} from "../Avatar";
import {MatrixClient} from "matrix-js-sdk/src/client";
// Populate this class with the details of your customisations when copying it. // Populate this class with the details of your customisations when copying it.
@ -30,8 +31,14 @@ import {ResizeMethod} from "../Avatar";
* "thumbnail media", derived from event contents or external sources. * "thumbnail media", derived from event contents or external sources.
*/ */
export class Media { export class Media {
private client: MatrixClient;
// Per above, this constructor signature can be whatever is helpful for you. // Per above, this constructor signature can be whatever is helpful for you.
constructor(private prepared: IPreparedMedia) { constructor(private prepared: IPreparedMedia, client?: MatrixClient) {
this.client = client ?? MatrixClientPeg.get();
if (!this.client) {
throw new Error("No possible MatrixClient for media resolution. Please provide one or log in.");
}
} }
/** /**
@ -67,7 +74,7 @@ export class Media {
* The HTTP URL for the source media. * The HTTP URL for the source media.
*/ */
public get srcHttp(): string { public get srcHttp(): string {
return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc); return this.client.mxcUrlToHttp(this.srcMxc);
} }
/** /**
@ -76,7 +83,7 @@ export class Media {
*/ */
public get thumbnailHttp(): string | undefined | null { public get thumbnailHttp(): string | undefined | null {
if (!this.hasThumbnail) return null; if (!this.hasThumbnail) return null;
return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc); return this.client.mxcUrlToHttp(this.thumbnailMxc);
} }
/** /**
@ -89,7 +96,7 @@ export class Media {
*/ */
public getThumbnailHttp(width: number, height: number, mode: ResizeMethod = "scale"): string | null | undefined { public getThumbnailHttp(width: number, height: number, mode: ResizeMethod = "scale"): string | null | undefined {
if (!this.hasThumbnail) return null; if (!this.hasThumbnail) return null;
return MatrixClientPeg.get().mxcUrlToHttp(this.thumbnailMxc, width, height, mode); return this.client.mxcUrlToHttp(this.thumbnailMxc, width, height, mode);
} }
/** /**
@ -100,7 +107,7 @@ export class Media {
* @returns {string} The HTTP URL which points to the thumbnail. * @returns {string} The HTTP URL which points to the thumbnail.
*/ */
public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMethod = "scale"): string { public getThumbnailOfSourceHttp(width: number, height: number, mode: ResizeMethod = "scale"): string {
return MatrixClientPeg.get().mxcUrlToHttp(this.srcMxc, width, height, mode); return this.client.mxcUrlToHttp(this.srcMxc, width, height, mode);
} }
/** /**
@ -128,17 +135,19 @@ export class Media {
/** /**
* Creates a media object from event content. * Creates a media object from event content.
* @param {IMediaEventContent} content The event content. * @param {IMediaEventContent} content The event content.
* @param {MatrixClient} client? Optional client to use.
* @returns {Media} The media object. * @returns {Media} The media object.
*/ */
export function mediaFromContent(content: IMediaEventContent): Media { export function mediaFromContent(content: IMediaEventContent, client?: MatrixClient): Media {
return new Media(prepEventContentAsMedia(content)); return new Media(prepEventContentAsMedia(content), client);
} }
/** /**
* Creates a media object from an MXC URI. * Creates a media object from an MXC URI.
* @param {string} mxc The MXC URI. * @param {string} mxc The MXC URI.
* @param {MatrixClient} client? Optional client to use.
* @returns {Media} The media object. * @returns {Media} The media object.
*/ */
export function mediaFromMxc(mxc: string): Media { export function mediaFromMxc(mxc: string, client?: MatrixClient): Media {
return mediaFromContent({url: mxc}); return mediaFromContent({url: mxc}, client);
} }

View file

@ -723,13 +723,15 @@
"Common names and surnames are easy to guess": "Common names and surnames are easy to guess", "Common names and surnames are easy to guess": "Common names and surnames are easy to guess",
"Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess", "Straight rows of keys are easy to guess": "Straight rows of keys are easy to guess",
"Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess", "Short keyboard patterns are easy to guess": "Short keyboard patterns are easy to guess",
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Share your public space": "Share your public space",
"Unknown App": "Unknown App", "Unknown App": "Unknown App",
"Help us improve %(brand)s": "Help us improve %(brand)s", "Help us improve %(brand)s": "Help us improve %(brand)s",
"Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.", "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.": "Send <UsageDataLink>anonymous usage data</UsageDataLink> which helps us improve %(brand)s. This will use a <PolicyLink>cookie</PolicyLink>.",
"Yes": "Yes", "Yes": "Yes",
"No": "No", "No": "No",
"You have unverified logins": "You have unverified logins", "You have unverified logins": "You have unverified logins",
"Verify all your sessions to ensure your account & messages are safe": "Verify all your sessions to ensure your account & messages are safe", "Review to ensure your account is safe": "Review to ensure your account is safe",
"Review": "Review", "Review": "Review",
"Later": "Later", "Later": "Later",
"Don't miss a reply": "Don't miss a reply", "Don't miss a reply": "Don't miss a reply",
@ -753,7 +755,7 @@
"Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data", "Safeguard against losing access to encrypted messages & data": "Safeguard against losing access to encrypted messages & data",
"Other users may not trust it": "Other users may not trust it", "Other users may not trust it": "Other users may not trust it",
"New login. Was this you?": "New login. Was this you?", "New login. Was this you?": "New login. Was this you?",
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s": "A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s", "%(deviceId)s from %(ip)s": "%(deviceId)s from %(ip)s",
"Check your devices": "Check your devices", "Check your devices": "Check your devices",
"What's new?": "What's new?", "What's new?": "What's new?",
"What's New": "What's New", "What's New": "What's New",
@ -797,6 +799,7 @@
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs", "Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms", "Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices", "Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
"Share decryption keys for room history when inviting users": "Share decryption keys for room history when inviting users",
"Enable advanced debugging for the room list": "Enable advanced debugging for the room list", "Enable advanced debugging for the room list": "Enable advanced debugging for the room list",
"Show info about bridges in room settings": "Show info about bridges in room settings", "Show info about bridges in room settings": "Show info about bridges in room settings",
"Font size": "Font size", "Font size": "Font size",
@ -982,7 +985,6 @@
"Folder": "Folder", "Folder": "Folder",
"Pin": "Pin", "Pin": "Pin",
"Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.", "Your server isn't responding to some <a>requests</a>.": "Your server isn't responding to some <a>requests</a>.",
"From %(deviceName)s (%(deviceId)s) at %(ip)s": "From %(deviceName)s (%(deviceId)s) at %(ip)s",
"Decline (%(counter)s)": "Decline (%(counter)s)", "Decline (%(counter)s)": "Decline (%(counter)s)",
"Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:", "Accept <policyLink /> to continue:": "Accept <policyLink /> to continue:",
"Delete": "Delete", "Delete": "Delete",
@ -1012,13 +1014,12 @@
"Share invite link": "Share invite link", "Share invite link": "Share invite link",
"Invite people": "Invite people", "Invite people": "Invite people",
"Invite with email or username": "Invite with email or username", "Invite with email or username": "Invite with email or username",
"Invite members": "Invite members",
"Share your public space": "Share your public space",
"Settings": "Settings", "Settings": "Settings",
"Leave space": "Leave space", "Leave space": "Leave space",
"New room": "New room", "Create new room": "Create new room",
"Space Home": "Space Home", "Add existing room": "Add existing room",
"Members": "Members", "Members": "Members",
"Manage & explore rooms": "Manage & explore rooms",
"Explore rooms": "Explore rooms", "Explore rooms": "Explore rooms",
"Space options": "Space options", "Space options": "Space options",
"Remove": "Remove", "Remove": "Remove",
@ -1082,6 +1083,7 @@
"Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.", "Securely cache encrypted messages locally for them to appear in search results.": "Securely cache encrypted messages locally for them to appear in search results.",
"%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.", "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.": "%(brand)s is missing some components required for securely caching encrypted messages locally. If you'd like to experiment with this feature, build a custom %(brand)s Desktop with <nativeLink>search components added</nativeLink>.",
"%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.", "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.": "%(brand)s can't securely cache encrypted messages locally while running in a web browser. Use <desktopLink>%(brand)s Desktop</desktopLink> for encrypted messages to appear in search results.",
"Message search initilisation failed": "Message search initilisation failed",
"Connecting to integration manager...": "Connecting to integration manager...", "Connecting to integration manager...": "Connecting to integration manager...",
"Cannot connect to integration manager": "Cannot connect to integration manager", "Cannot connect to integration manager": "Cannot connect to integration manager",
"The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.", "The integration manager is offline or it cannot reach your homeserver.": "The integration manager is offline or it cannot reach your homeserver.",
@ -1285,6 +1287,7 @@
"Room ID or address of ban list": "Room ID or address of ban list", "Room ID or address of ban list": "Room ID or address of ban list",
"Subscribe": "Subscribe", "Subscribe": "Subscribe",
"Start automatically after system login": "Start automatically after system login", "Start automatically after system login": "Start automatically after system login",
"Warn before quitting": "Warn before quitting",
"Always show the window menu bar": "Always show the window menu bar", "Always show the window menu bar": "Always show the window menu bar",
"Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close", "Show tray icon and minimize window to it on close": "Show tray icon and minimize window to it on close",
"Preferences": "Preferences", "Preferences": "Preferences",
@ -1479,6 +1482,7 @@
"<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.", "<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.",
"You created this room.": "You created this room.", "You created this room.": "You created this room.",
"%(displayName)s created this room.": "%(displayName)s created this room.", "%(displayName)s created this room.": "%(displayName)s created this room.",
"Invite to just this room": "Invite to just this room",
"Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.", "Add a photo, so people can easily spot your room.": "Add a photo, so people can easily spot your room.",
"This is the start of <roomName/>.": "This is the start of <roomName/>.", "This is the start of <roomName/>.": "This is the start of <roomName/>.",
"No pinned messages.": "No pinned messages.", "No pinned messages.": "No pinned messages.",
@ -1525,11 +1529,8 @@
"Start chat": "Start chat", "Start chat": "Start chat",
"Rooms": "Rooms", "Rooms": "Rooms",
"Add room": "Add room", "Add room": "Add room",
"Create new room": "Create new room",
"You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space", "You do not have permissions to create new rooms in this space": "You do not have permissions to create new rooms in this space",
"Add existing room": "Add existing room",
"You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space", "You do not have permissions to add rooms to this space": "You do not have permissions to add rooms to this space",
"Explore space rooms": "Explore space rooms",
"Explore community rooms": "Explore community rooms", "Explore community rooms": "Explore community rooms",
"Explore public rooms": "Explore public rooms", "Explore public rooms": "Explore public rooms",
"Low priority": "Low priority", "Low priority": "Low priority",
@ -1541,6 +1542,7 @@
"Can't see what youre looking for?": "Can't see what youre looking for?", "Can't see what youre looking for?": "Can't see what youre looking for?",
"Start a new chat": "Start a new chat", "Start a new chat": "Start a new chat",
"Explore all public rooms": "Explore all public rooms", "Explore all public rooms": "Explore all public rooms",
"Quick actions": "Quick actions",
"Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below", "Use the + to make a new room or explore existing ones below": "Use the + to make a new room or explore existing ones below",
"%(count)s results|other": "%(count)s results", "%(count)s results|other": "%(count)s results",
"%(count)s results|one": "%(count)s result", "%(count)s results|one": "%(count)s result",
@ -1677,7 +1679,7 @@
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.", "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.": "In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
"When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.", "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.": "When someone puts a URL in their message, a URL preview can be shown to give more information about that link such as the title, description, and an image from the website.",
"Back": "Back", "Back": "Back",
"Waiting for you to accept on your other session…": "Waiting for you to accept on your other session…", "Accept on your other login…": "Accept on your other login…",
"Waiting for %(displayName)s to accept…": "Waiting for %(displayName)s to accept…", "Waiting for %(displayName)s to accept…": "Waiting for %(displayName)s to accept…",
"Accepting…": "Accepting…", "Accepting…": "Accepting…",
"Start Verification": "Start Verification", "Start Verification": "Start Verification",
@ -1909,6 +1911,8 @@
"Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.", "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.": "Please <newIssueLink>create a new issue</newIssueLink> on GitHub so that we can investigate this bug.",
"collapse": "collapse", "collapse": "collapse",
"expand": "expand", "expand": "expand",
"%(count)s people you know have already joined|other": "%(count)s people you know have already joined",
"%(count)s people you know have already joined|one": "%(count)s person you know has already joined",
"You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)", "You cannot delete this image. (%(code)s)": "You cannot delete this image. (%(code)s)",
"Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s", "Uploaded on %(date)s by %(user)s": "Uploaded on %(date)s by %(user)s",
"Rotate Left": "Rotate Left", "Rotate Left": "Rotate Left",
@ -2004,14 +2008,13 @@
"%(networkName)s rooms": "%(networkName)s rooms", "%(networkName)s rooms": "%(networkName)s rooms",
"Matrix rooms": "Matrix rooms", "Matrix rooms": "Matrix rooms",
"Space selection": "Space selection", "Space selection": "Space selection",
"Add existing spaces/rooms": "Add existing spaces/rooms", "Add existing rooms": "Add existing rooms",
"Filter your rooms and spaces": "Filter your rooms and spaces", "Filter your rooms and spaces": "Filter your rooms and spaces",
"Spaces": "Spaces", "Spaces": "Spaces",
"Don't want to add an existing room?": "Don't want to add an existing room?", "Don't want to add an existing room?": "Don't want to add an existing room?",
"Create a new room": "Create a new room", "Create a new room": "Create a new room",
"Applying...": "Applying...",
"Apply": "Apply",
"Failed to add rooms to space": "Failed to add rooms to space", "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",
@ -2187,7 +2190,7 @@
"Click the button below to confirm your identity.": "Click the button below to confirm your identity.", "Click the button below to confirm your identity.": "Click the button below to confirm your identity.",
"Invite by email": "Invite by email", "Invite by email": "Invite by email",
"Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s", "Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s",
"We couldn't create your DM. Please check the users you want to invite and try again.": "We couldn't create your DM. Please check the users you want to invite and try again.", "We couldn't create your DM.": "We couldn't create your DM.",
"Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.", "Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.",
"We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.", "We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.",
"A call can only be transferred to a single user.": "A call can only be transferred to a single user.", "A call can only be transferred to a single user.": "A call can only be transferred to a single user.",
@ -2203,13 +2206,13 @@
"Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).", "Start a conversation with someone using their name or username (like <userId/>).": "Start a conversation with someone using their name or username (like <userId/>).",
"This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>", "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>": "This won't invite them to %(communityName)s. To invite someone to %(communityName)s, click <a>here</a>",
"Go": "Go", "Go": "Go",
"Invite to %(spaceName)s": "Invite to %(spaceName)s",
"Unnamed Space": "Unnamed Space", "Unnamed Space": "Unnamed Space",
"Invite to %(roomName)s": "Invite to %(roomName)s", "Invite to %(roomName)s": "Invite to %(roomName)s",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.", "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.", "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this space</a>.",
"Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.", "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, email address, username (like <userId/>) or <a>share this room</a>.",
"Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.", "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.": "Invite someone using their name, username (like <userId/>) or <a>share this room</a>.",
"Invited people will be able to read old messages.": "Invited people will be able to read old messages.",
"Transfer": "Transfer", "Transfer": "Transfer",
"a new master key signature": "a new master key signature", "a new master key signature": "a new master key signature",
"a new cross-signing key signature": "a new cross-signing key signature", "a new cross-signing key signature": "a new cross-signing key signature",
@ -2351,7 +2354,7 @@
"Upload %(count)s other files|one": "Upload %(count)s other file", "Upload %(count)s other files|one": "Upload %(count)s other file",
"Cancel All": "Cancel All", "Cancel All": "Cancel All",
"Upload Error": "Upload Error", "Upload Error": "Upload Error",
"Verify other session": "Verify other session", "Verify other login": "Verify other login",
"Verification Request": "Verification Request", "Verification Request": "Verification Request",
"Approve widget permissions": "Approve widget permissions", "Approve widget permissions": "Approve widget permissions",
"This widget would like to:": "This widget would like to:", "This widget would like to:": "This widget would like to:",
@ -2552,7 +2555,7 @@
"Review terms and conditions": "Review terms and conditions", "Review terms and conditions": "Review terms and conditions",
"Old cryptography data detected": "Old cryptography data detected", "Old cryptography data detected": "Old cryptography data detected",
"Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.", "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.": "Data from an older version of %(brand)s has been detected. This will have caused end-to-end cryptography to malfunction in the older version. End-to-end encrypted messages exchanged recently whilst using the older version may not be decryptable in this version. This may also cause messages exchanged with this version to fail. If you experience problems, log out and back in again. To retain message history, export and re-import your keys.",
"Self-verification request": "Self-verification request", "Verification requested": "Verification requested",
"Logout": "Logout", "Logout": "Logout",
"%(creator)s created this DM.": "%(creator)s created this DM.", "%(creator)s created this DM.": "%(creator)s created this DM.",
"%(creator)s created and configured the room.": "%(creator)s created and configured the room.", "%(creator)s created and configured the room.": "%(creator)s created and configured the room.",
@ -2610,7 +2613,6 @@
"Drop file here to upload": "Drop file here to upload", "Drop file here to upload": "Drop file here to upload",
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
"Open": "Open",
"You don't have permission": "You don't have permission", "You don't have permission": "You don't have permission",
"%(count)s members|other": "%(count)s members", "%(count)s members|other": "%(count)s members",
"%(count)s members|one": "%(count)s member", "%(count)s members|one": "%(count)s member",
@ -2618,7 +2620,6 @@
"%(count)s rooms|one": "%(count)s room", "%(count)s rooms|one": "%(count)s room",
"This room is suggested as a good one to join": "This room is suggested as a good one to join", "This room is suggested as a good one to join": "This room is suggested as a good one to join",
"Suggested": "Suggested", "Suggested": "Suggested",
"If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces", "%(count)s rooms and %(numSpaces)s spaces|other": "%(count)s rooms and %(numSpaces)s spaces",
"%(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",
@ -2629,16 +2630,14 @@
"Mark as suggested": "Mark as suggested", "Mark as suggested": "Mark as suggested",
"No results found": "No results found", "No results found": "No results found",
"You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.", "You may want to try a different search or check for typos.": "You may want to try a different search or check for typos.",
"Create room": "Create room", "Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"Search names and description": "Search names and description", "Search names and description": "Search names and description",
"<inviter/> invites you": "<inviter/> invites you", "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.": "If you can't find the room you're looking for, ask for an invite or <a>create a new room</a>.",
"Create room": "Create room",
"Public space": "Public space", "Public space": "Public space",
"Private space": "Private space", "Private space": "Private space",
"<inviter/> invites you": "<inviter/> invites you",
"Add existing rooms & spaces": "Add existing rooms & spaces", "Add existing rooms & spaces": "Add existing rooms & spaces",
"Default Rooms": "Default Rooms",
"Your server does not support showing space hierarchies.": "Your server does not support showing space hierarchies.",
"Your public space <name/>": "Your public space <name/>",
"Your private space <name/>": "Your private space <name/>",
"Welcome to <name/>": "Welcome to <name/>", "Welcome to <name/>": "Welcome to <name/>",
"Random": "Random", "Random": "Random",
"Support": "Support", "Support": "Support",
@ -2660,8 +2659,9 @@
"Invite your teammates": "Invite your teammates", "Invite your teammates": "Invite your teammates",
"Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.", "Make sure the right people have access. You can invite more later.": "Make sure the right people have access. You can invite more later.",
"Invite by username": "Invite by username", "Invite by username": "Invite by username",
"What are some things you want to discuss?": "What are some things you want to discuss?", "What are some things you want to discuss in %(spaceName)s?": "What are some things you want to discuss in %(spaceName)s?",
"Let's create a room for each of them. You can add more later too, including already existing ones.": "Let's create a room for each of them. You can add more later too, including already existing ones.", "Let's create a room for each of them.": "Let's create a room for each of them.",
"You can add more later too, including already existing ones.": "You can add more later too, including already existing ones.",
"What projects are you working on?": "What projects are you working on?", "What projects are you working on?": "What projects are you working on?",
"We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.", "We'll create rooms for each of them. You can add more later too, including already existing ones.": "We'll create rooms for each of them. You can add more later too, including already existing ones.",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
@ -2742,11 +2742,11 @@
"Decide where your account is hosted": "Decide where your account is hosted", "Decide where your account is hosted": "Decide where your account is hosted",
"Use Security Key or Phrase": "Use Security Key or Phrase", "Use Security Key or Phrase": "Use Security Key or Phrase",
"Use Security Key": "Use Security Key", "Use Security Key": "Use Security Key",
"Verify with another session": "Verify with another session", "Use another login": "Use another login",
"Verify this login to access your encrypted messages and prove to others that this login is really you.": "Verify this login to access your encrypted messages and prove to others that this login is really you.", "Verify your identity to access encrypted messages and prove your identity to others.": "Verify your identity to access encrypted messages and prove your identity to others.",
"Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.", "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.": "Your new session is now verified. It has access to your encrypted messages, and other users will see it as trusted.",
"Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.", "Your new session is now verified. Other users will see it as trusted.": "Your new session is now verified. Other users will see it as trusted.",
"Without completing security on this session, it wont have access to encrypted messages.": "Without completing security on this session, it wont have access to encrypted messages.", "Without verifying, you wont have access to all your messages and may appear as untrusted to others.": "Without verifying, you wont have access to all your messages and may appear as untrusted to others.",
"Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem", "Failed to re-authenticate due to a homeserver problem": "Failed to re-authenticate due to a homeserver problem",
"Incorrect password": "Incorrect password", "Incorrect password": "Incorrect password",
"Failed to re-authenticate": "Failed to re-authenticate", "Failed to re-authenticate": "Failed to re-authenticate",

View file

@ -31,6 +31,7 @@ class EventIndexPeg {
constructor() { constructor() {
this.index = null; this.index = null;
this._supportIsInstalled = false; this._supportIsInstalled = false;
this.error = null;
} }
/** /**
@ -96,6 +97,7 @@ class EventIndexPeg {
await index.init(); await index.init();
} catch (e) { } catch (e) {
console.log("EventIndex: Error initializing the event index", e); console.log("EventIndex: Error initializing the event index", e);
this.error = e;
return false; return false;
} }

View file

@ -220,6 +220,12 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_FEATURE, supportedLevels: LEVELS_FEATURE,
default: false, default: false,
}, },
"feature_room_history_key_sharing": {
isFeature: true,
displayName: _td("Share decryption keys for room history when inviting users"),
supportedLevels: LEVELS_FEATURE,
default: false,
},
"advancedRoomListLogging": { "advancedRoomListLogging": {
// TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231 // TODO: Remove flag before launch: https://github.com/vector-im/element-web/issues/14231
displayName: _td("Enable advanced debugging for the room list"), displayName: _td("Enable advanced debugging for the room list"),

View file

@ -273,7 +273,10 @@ class RoomViewStore extends Store<ActionPayload> {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
const address = this.state.roomAlias || this.state.roomId; const address = this.state.roomAlias || this.state.roomId;
try { try {
await retry<void, MatrixError>(() => cli.joinRoom(address, payload.opts), NUM_JOIN_RETRY, (err) => { await retry<void, MatrixError>(() => cli.joinRoom(address, {
viaServers: payload.via_servers,
...payload.opts,
}), NUM_JOIN_RETRY, (err) => {
// if we received a Gateway timeout then retry // if we received a Gateway timeout then retry
return err.httpStatus === 504; return err.httpStatus === 504;
}); });

View file

@ -121,21 +121,16 @@ export class SetupEncryptionStore extends EventEmitter {
// on the first trust check, and the key backup restore will happen // on the first trust check, and the key backup restore will happen
// in the background. // in the background.
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
try { accessSecretStorage(async () => {
accessSecretStorage(async () => { await cli.checkOwnCrossSigningTrust();
await cli.checkOwnCrossSigningTrust(); resolve();
resolve(); if (backupInfo) {
if (backupInfo) { // A complete restore can take many minutes for large
// A complete restore can take many minutes for large // accounts / slow servers, so we allow the dialog
// accounts / slow servers, so we allow the dialog // to advance before this.
// to advance before this. await cli.restoreKeyBackupWithSecretStorage(backupInfo);
await cli.restoreKeyBackupWithSecretStorage(backupInfo); }
} }).catch(reject);
}).catch(reject);
} catch (e) {
console.error(e);
reject(e);
}
}); });
if (cli.getCrossSigningId()) { if (cli.getCrossSigningId()) {

View file

@ -34,6 +34,7 @@ import {setHasDiff} from "../utils/sets";
import {objectDiff} from "../utils/objects"; import {objectDiff} from "../utils/objects";
import {arrayHasDiff} from "../utils/arrays"; import {arrayHasDiff} from "../utils/arrays";
import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory"; import {ISpaceSummaryEvent, ISpaceSummaryRoom} from "../components/structures/SpaceRoomDirectory";
import RoomViewStore from "./RoomViewStore";
type SpaceKey = string | symbol; type SpaceKey = string | symbol;
@ -121,7 +122,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const data = await this.fetchSuggestedRooms(space); const data = await this.fetchSuggestedRooms(space);
if (this._activeSpace === space) { if (this._activeSpace === space) {
this._suggestedRooms = data.rooms.filter(roomInfo => { this._suggestedRooms = data.rooms.filter(roomInfo => {
return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id); return roomInfo.room_type !== RoomType.Space
&& this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join";
}); });
this.emit(SUGGESTED_ROOMS, this._suggestedRooms); this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
} }
@ -195,15 +197,18 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set(); return this.spaceFilteredRooms.get(space?.roomId || HOME_SPACE) || new Set();
}; };
public rebuild = throttle(() => { // exported for tests private rebuild = throttle(() => {
const visibleRooms = this.matrixClient.getVisibleRooms(); // get all most-upgraded rooms & spaces except spaces which have been left (historical)
const visibleRooms = this.matrixClient.getVisibleRooms().filter(r => {
// Sort spaces by room ID to force the loop breaking to be deterministic return !r.isSpaceRoom() || r.getMyMembership() === "join";
const spaces = sortBy(this.getSpaces(), space => space.roomId); });
const unseenChildren = new Set<Room>([...visibleRooms, ...spaces]);
const unseenChildren = new Set<Room>(visibleRooms);
const backrefs = new EnhancedMap<string, Set<string>>(); const backrefs = new EnhancedMap<string, Set<string>>();
// Sort spaces by room ID to force the cycle breaking to be deterministic
const spaces = sortBy(visibleRooms.filter(r => r.isSpaceRoom()), space => space.roomId);
// TODO handle cleaning up links when a Space is removed // TODO handle cleaning up links when a Space is removed
spaces.forEach(space => { spaces.forEach(space => {
const children = this.getChildren(space.roomId); const children = this.getChildren(space.roomId);
@ -216,7 +221,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren)); const [rootSpaces, orphanedRooms] = partitionSpacesAndRooms(Array.from(unseenChildren));
// untested algorithm to handle full-cycles // somewhat algorithm to handle full-cycles
const detachedNodes = new Set<Room>(spaces); const detachedNodes = new Set<Room>(spaces);
const markTreeChildren = (rootSpace: Room, unseen: Set<Room>) => { const markTreeChildren = (rootSpace: Room, unseen: Set<Room>) => {
@ -290,6 +295,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
} }
}; };
private onSpaceMembersChange = (ev: MatrixEvent) => {
// skip this update if we do not have a DM with this user
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
this.onRoomsUpdate();
};
private onRoomsUpdate = throttle(() => { private onRoomsUpdate = throttle(() => {
// TODO resolve some updates as deltas // TODO resolve some updates as deltas
const visibleRooms = this.matrixClient.getVisibleRooms(); const visibleRooms = this.matrixClient.getVisibleRooms();
@ -365,10 +376,17 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
this.onRoomsUpdate(); this.onRoomsUpdate();
} }
const numSuggestedRooms = this._suggestedRooms.length; // if the user was looking at the room and then joined select that space
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId); if (room.getMyMembership() === "join" && room.roomId === RoomViewStore.getRoomId()) {
if (numSuggestedRooms !== this._suggestedRooms.length) { this.setActiveSpace(room);
this.emit(SUGGESTED_ROOMS, this._suggestedRooms); }
if (room.getMyMembership() === "join") {
const numSuggestedRooms = this._suggestedRooms.length;
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
if (numSuggestedRooms !== this._suggestedRooms.length) {
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
}
} }
}; };
@ -376,18 +394,30 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
const room = this.matrixClient.getRoom(ev.getRoomId()); const room = this.matrixClient.getRoom(ev.getRoomId());
if (!room) return; if (!room) return;
if (ev.getType() === EventType.SpaceChild && room.isSpaceRoom()) { switch (ev.getType()) {
this.onSpaceUpdate(); case EventType.SpaceChild:
this.emit(room.roomId); if (room.isSpaceRoom()) {
} else if (ev.getType() === EventType.SpaceParent) { this.onSpaceUpdate();
// TODO rebuild the space parent and not the room - check permissions? this.emit(room.roomId);
// TODO confirm this after implementing parenting behaviour }
if (room.isSpaceRoom()) { break;
this.onSpaceUpdate();
} else { case EventType.SpaceParent:
this.onRoomUpdate(room); // TODO rebuild the space parent and not the room - check permissions?
} // TODO confirm this after implementing parenting behaviour
this.emit(room.roomId); if (room.isSpaceRoom()) {
this.onSpaceUpdate();
} else {
this.onRoomUpdate(room);
}
this.emit(room.roomId);
break;
case EventType.RoomMember:
if (room.isSpaceRoom()) {
this.onSpaceMembersChange(ev);
}
break;
} }
}; };

View file

@ -42,7 +42,7 @@ export const showToast = (deviceIds: Set<string>) => {
title: _t("You have unverified logins"), title: _t("You have unverified logins"),
icon: "verification_warning", icon: "verification_warning",
props: { props: {
description: _t("Verify all your sessions to ensure your account & messages are safe"), description: _t("Review to ensure your account is safe"),
acceptLabel: _t("Review"), acceptLabel: _t("Review"),
onAccept, onAccept,
rejectLabel: _t("Later"), rejectLabel: _t("Later"),

View file

@ -49,13 +49,11 @@ export const showToast = async (deviceId: string) => {
title: _t("New login. Was this you?"), title: _t("New login. Was this you?"),
icon: "verification_warning", icon: "verification_warning",
props: { props: {
description: _t( description: device.display_name,
"A new login is accessing your account: %(name)s (%(deviceID)s) at %(ip)s", { detail: _t("%(deviceId)s from %(ip)s", {
name: device.display_name, deviceId,
deviceID: deviceId, ip: device.last_seen_ip,
ip: device.last_seen_ip, }),
},
),
acceptLabel: _t("Check your devices"), acceptLabel: _t("Check your devices"),
onAccept, onAccept,
rejectLabel: _t("Later"), rejectLabel: _t("Later"),

View file

@ -111,17 +111,10 @@ export default class MultiInviter {
} }
if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) { if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) {
try { const profile = await MatrixClientPeg.get().getProfileInfo(addr);
const profile = await MatrixClientPeg.get().getProfileInfo(addr); if (!profile) {
if (!profile) { // noinspection ExceptionCaughtLocallyJS
// noinspection ExceptionCaughtLocallyJS throw new Error("User has no profile");
throw new Error("User has no profile");
}
} catch (e) {
throw {
errcode: "RIOT.USER_NOT_FOUND",
error: "User does not have a profile or does not exist."
};
} }
} }
@ -171,7 +164,7 @@ export default class MultiInviter {
this._doInvite(address, ignoreProfile).then(resolve, reject); this._doInvite(address, ignoreProfile).then(resolve, reject);
}, 5000); }, 5000);
return; return;
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'RIOT.USER_NOT_FOUND'].includes(err.errcode)) { } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) {
errorText = _t("User %(user_id)s does not exist", {user_id: address}); errorText = _t("User %(user_id)s does not exist", {user_id: address});
} else if (err.errcode === 'M_PROFILE_UNDISCLOSED') { } else if (err.errcode === 'M_PROFILE_UNDISCLOSED') {
errorText = _t("User %(user_id)s may or may not exist", {user_id: address}); errorText = _t("User %(user_id)s may or may not exist", {user_id: address});
@ -212,7 +205,7 @@ export default class MultiInviter {
if (Object.keys(this.errors).length > 0 && !this.groupId) { if (Object.keys(this.errors).length > 0 && !this.groupId) {
// There were problems inviting some people - see if we can invite them // There were problems inviting some people - see if we can invite them
// without caring if they exist or not. // without caring if they exist or not.
const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND', 'RIOT.USER_NOT_FOUND']; const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND'];
const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode)); const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));
if (unknownProfileUsers.length > 0) { if (unknownProfileUsers.length > 0) {
@ -228,7 +221,7 @@ export default class MultiInviter {
const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog"); const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
console.log("Showing failed to invite dialog..."); console.log("Showing failed to invite dialog...");
Modal.createTrackedDialog('Failed to invite the following users to the room', '', AskInviteAnywayDialog, { Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, {
unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}), unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}),
onInviteAnyways: () => inviteUnknowns(), onInviteAnyways: () => inviteUnknowns(),
onGiveUp: () => { onGiveUp: () => {

View file

@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React 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} from "matrix-js-sdk/src/@types/event"; import {EventType} from "matrix-js-sdk/src/@types/event";
@ -24,6 +25,10 @@ import SpaceSettingsDialog from "../components/views/dialogs/SpaceSettingsDialog
import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog"; import AddExistingToSpaceDialog from "../components/views/dialogs/AddExistingToSpaceDialog";
import CreateRoomDialog from "../components/views/dialogs/CreateRoomDialog"; import CreateRoomDialog from "../components/views/dialogs/CreateRoomDialog";
import createRoom, {IOpts} from "../createRoom"; import createRoom, {IOpts} from "../createRoom";
import {_t} from "../languageHandler";
import SpacePublicShare from "../components/views/spaces/SpacePublicShare";
import InfoDialog from "../components/views/dialogs/InfoDialog";
import { showRoomInviteDialog } from "../RoomInvite";
export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => { export const shouldShowSpaceSettings = (cli: MatrixClient, space: Room) => {
const userId = cli.getUserId(); const userId = cli.getUserId();
@ -79,3 +84,21 @@ export const showCreateNewRoom = async (cli: MatrixClient, space: Room) => {
await createRoom(opts); await createRoom(opts);
} }
}; };
export const showSpaceInvite = (space: Room, initialText = "") => {
if (space.getJoinRule() === "public") {
const modal = Modal.createTrackedDialog("Space Invite", "User Menu", InfoDialog, {
title: _t("Invite to %(spaceName)s", { spaceName: space.name }),
description: <React.Fragment>
<span>{ _t("Share your public space") }</span>
<SpacePublicShare space={space} onFinished={() => modal.close()} />
</React.Fragment>,
fixedWidth: false,
button: false,
className: "mx_SpacePanel_sharePublicSpace",
hasCloseButton: true,
});
} else {
showRoomInviteDialog(space.roomId, initialText);
}
};

View file

@ -48,9 +48,6 @@ export class VoiceRecorder {
private async makeRecorder() { private async makeRecorder() {
this.recorderStream = await navigator.mediaDevices.getUserMedia({ this.recorderStream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {
// specify some audio settings so we're feeding the recorder with the
// best possible values. The browser will handle resampling for us.
sampleRate: SAMPLE_RATE,
channelCount: CHANNELS, channelCount: CHANNELS,
noiseSuppression: true, // browsers ignore constraints they can't honour noiseSuppression: true, // browsers ignore constraints they can't honour
deviceId: CallMediaHandler.getAudioInput(), deviceId: CallMediaHandler.getAudioInput(),
@ -58,7 +55,6 @@ export class VoiceRecorder {
}); });
this.recorderContext = new AudioContext({ this.recorderContext = new AudioContext({
// latencyHint: "interactive", // we don't want a latency hint (this causes data smoothing) // latencyHint: "interactive", // we don't want a latency hint (this causes data smoothing)
sampleRate: SAMPLE_RATE, // once again, the browser will resample for us
}); });
this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream); this.recorderSource = this.recorderContext.createMediaStreamSource(this.recorderStream);
this.recorderFFT = this.recorderContext.createAnalyser(); this.recorderFFT = this.recorderContext.createAnalyser();

View file

@ -0,0 +1,153 @@
/*
Copyright 2021 Clemens Zeidler
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 { isKeyComboMatch, KeyCombo } from '../src/KeyBindingsManager';
const assert = require('assert');
function mockKeyEvent(key: string, modifiers?: {
ctrlKey?: boolean,
altKey?: boolean,
shiftKey?: boolean,
metaKey?: boolean
}): KeyboardEvent {
return {
key,
ctrlKey: modifiers?.ctrlKey ?? false,
altKey: modifiers?.altKey ?? false,
shiftKey: modifiers?.shiftKey ?? false,
metaKey: modifiers?.metaKey ?? false
} as KeyboardEvent;
}
describe('KeyBindingsManager', () => {
it('should match basic key combo', () => {
const combo1: KeyCombo = {
key: 'k',
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo1, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n'), combo1, false), false);
});
it('should match key + modifier key combo', () => {
const combo: KeyCombo = {
key: 'k',
ctrlKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, metaKey: true }), combo, false), false);
const combo2: KeyCombo = {
key: 'k',
metaKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo2, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { metaKey: true }), combo2, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo2, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true, metaKey: true }), combo2, false), false);
const combo3: KeyCombo = {
key: 'k',
altKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { altKey: true }), combo3, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { altKey: true }), combo3, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo3, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo3, false), false);
const combo4: KeyCombo = {
key: 'k',
shiftKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true }), combo4, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { shiftKey: true }), combo4, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k'), combo4, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { shiftKey: true, ctrlKey: true }), combo4, false), false);
});
it('should match key + multiple modifiers key combo', () => {
const combo: KeyCombo = {
key: 'k',
ctrlKey: true,
altKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, altKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true, shiftKey: true }), combo,
false), false);
const combo2: KeyCombo = {
key: 'k',
ctrlKey: true,
shiftKey: true,
altKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true, shiftKey: true, altKey: true }), combo2,
false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, metaKey: true }), combo2, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo2, false), false);
const combo3: KeyCombo = {
key: 'k',
ctrlKey: true,
shiftKey: true,
altKey: true,
metaKey: true,
};
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n',
{ ctrlKey: true, shiftKey: true, altKey: true, metaKey: true }), combo3, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k',
{ ctrlKey: true, shiftKey: true, altKey: true }), combo3, false), false);
});
it('should match ctrlOrMeta key combo', () => {
const combo: KeyCombo = {
key: 'k',
ctrlOrCmd: true,
};
// PC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, false), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, false), false);
// MAC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true }), combo, true), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true }), combo, true), false);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('n', { ctrlKey: true }), combo, true), false);
});
it('should match advanced ctrlOrMeta key combo', () => {
const combo: KeyCombo = {
key: 'k',
ctrlOrCmd: true,
altKey: true,
};
// PC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, false), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, false), false);
// MAC:
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { metaKey: true, altKey: true }), combo, true), true);
assert.strictEqual(isKeyComboMatch(mockKeyEvent('k', { ctrlKey: true, altKey: true }), combo, true), false);
});
});

View file

@ -37,7 +37,7 @@ describe("AccessSecretStorageDialog", function() {
recoveryKey: "a", recoveryKey: "a",
}); });
const e = { preventDefault: () => {} }; const e = { preventDefault: () => {} };
testInstance.getInstance()._onRecoveryKeyNext(e); testInstance.getInstance().onRecoveryKeyNext(e);
}); });
it("Considers a valid key to be valid", async function() { it("Considers a valid key to be valid", async function() {
@ -51,9 +51,9 @@ describe("AccessSecretStorageDialog", function() {
stubClient(); stubClient();
MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => 'a raw key'; MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => 'a raw key';
MatrixClientPeg.get().checkSecretStorageKey = () => true; MatrixClientPeg.get().checkSecretStorageKey = () => true;
testInstance.getInstance()._onRecoveryKeyChange(e); testInstance.getInstance().onRecoveryKeyChange(e);
// force a validation now because it debounces // force a validation now because it debounces
await testInstance.getInstance()._validateRecoveryKey(); await testInstance.getInstance().validateRecoveryKey();
const { recoveryKeyValid } = testInstance.getInstance().state; const { recoveryKeyValid } = testInstance.getInstance().state;
expect(recoveryKeyValid).toBe(true); expect(recoveryKeyValid).toBe(true);
}); });
@ -69,9 +69,9 @@ describe("AccessSecretStorageDialog", function() {
MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => { MatrixClientPeg.get().keyBackupKeyFromRecoveryKey = () => {
throw new Error("that's no key"); throw new Error("that's no key");
}; };
testInstance.getInstance()._onRecoveryKeyChange(e); testInstance.getInstance().onRecoveryKeyChange(e);
// force a validation now because it debounces // force a validation now because it debounces
await testInstance.getInstance()._validateRecoveryKey(); await testInstance.getInstance().validateRecoveryKey();
const { recoveryKeyValid, recoveryKeyCorrect } = testInstance.getInstance().state; const { recoveryKeyValid, recoveryKeyCorrect } = testInstance.getInstance().state;
expect(recoveryKeyValid).toBe(false); expect(recoveryKeyValid).toBe(false);
@ -98,8 +98,8 @@ describe("AccessSecretStorageDialog", function() {
const e = { target: { value: "a" } }; const e = { target: { value: "a" } };
stubClient(); stubClient();
MatrixClientPeg.get().isValidRecoveryKey = () => false; MatrixClientPeg.get().isValidRecoveryKey = () => false;
testInstance.getInstance()._onPassPhraseChange(e); testInstance.getInstance().onPassPhraseChange(e);
await testInstance.getInstance()._onPassPhraseNext({ preventDefault: () => {} }); await testInstance.getInstance().onPassPhraseNext({ preventDefault: () => {} });
const notification = testInstance.root.findByProps({ const notification = testInstance.root.findByProps({
className: "mx_AccessSecretStorageDialog_keyStatus", className: "mx_AccessSecretStorageDialog_keyStatus",
}); });

View file

@ -93,7 +93,7 @@ module.exports.acceptSasVerification = async function(session, name) {
// verify the toast is for verification // verify the toast is for verification
const toastHeader = await requestToast.$("h2"); const toastHeader = await requestToast.$("h2");
const toastHeaderText = await session.innerText(toastHeader); const toastHeaderText = await session.innerText(toastHeader);
assert.equal(toastHeaderText, 'Verification Request'); assert.equal(toastHeaderText, 'Verification requested');
const toastDescription = await requestToast.$(".mx_Toast_description"); const toastDescription = await requestToast.$(".mx_Toast_description");
const toastDescText = await session.innerText(toastDescription); const toastDescText = await session.innerText(toastDescription);
assert.equal(toastDescText.startsWith(name), true, assert.equal(toastDescText.startsWith(name), true,

View file

@ -5588,8 +5588,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 "9.9.0" version "9.10.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/cd38fb9b4c349eb31feac14e806e710bf6431b72" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/4204b2170a1e04f20067b87636bb2eddf95194c4"
dependencies: dependencies:
"@babel/runtime" "^7.12.5" "@babel/runtime" "^7.12.5"
another-json "^0.2.0" another-json "^0.2.0"